feat(web): add workspace management UI (M2 #12)

- Create workspace listing page at /settings/workspaces
  - List all user workspaces with role badges
  - Create new workspace functionality
  - Display member count per workspace

- Create workspace detail page at /settings/workspaces/[id]
  - Workspace settings (name, ID, created date)
  - Member management with role editing
  - Invite member functionality
  - Delete workspace (owner only)

- Add workspace components:
  - WorkspaceCard: Display workspace info with role badge
  - WorkspaceSettings: Edit workspace settings and delete
  - MemberList: Display and manage workspace members
  - InviteMember: Send invitations with role selection

- Add WorkspaceMemberWithUser type to shared package
- Follow existing app patterns for styling and structure
- Use mock data (ready for API integration)
This commit is contained in:
Jason Woltje
2026-01-29 16:59:26 -06:00
parent 287a0e2556
commit 5291fece26
43 changed files with 4152 additions and 99 deletions

View File

@@ -0,0 +1,2 @@
export * from "./update-preferences.dto";
export * from "./preferences-response.dto";

View File

@@ -0,0 +1,12 @@
/**
* Response DTO for user preferences
*/
export interface PreferencesResponseDto {
id: string;
userId: string;
theme: string;
locale: string;
timezone: string | null;
settings: Record<string, unknown>;
updatedAt: Date;
}

View File

@@ -0,0 +1,25 @@
import { IsString, IsOptional, IsObject, IsIn } from "class-validator";
/**
* DTO for updating user preferences
*/
export class UpdatePreferencesDto {
@IsOptional()
@IsString({ message: "theme must be a string" })
@IsIn(["light", "dark", "system"], {
message: "theme must be one of: light, dark, system",
})
theme?: string;
@IsOptional()
@IsString({ message: "locale must be a string" })
locale?: string;
@IsOptional()
@IsString({ message: "timezone must be a string" })
timezone?: string;
@IsOptional()
@IsObject({ message: "settings must be an object" })
settings?: Record<string, unknown>;
}

View File

@@ -0,0 +1,55 @@
import {
Controller,
Get,
Put,
Body,
UseGuards,
Request,
UnauthorizedException,
} from "@nestjs/common";
import { PreferencesService } from "./preferences.service";
import { UpdatePreferencesDto } from "./dto";
import { AuthGuard } from "../auth/guards/auth.guard";
/**
* Controller for user preferences endpoints
* All endpoints require authentication
*/
@Controller("users/me/preferences")
@UseGuards(AuthGuard)
export class PreferencesController {
constructor(private readonly preferencesService: PreferencesService) {}
/**
* GET /api/users/me/preferences
* Get current user's preferences
*/
@Get()
async getPreferences(@Request() req: any) {
const userId = req.user?.id;
if (!userId) {
throw new UnauthorizedException("Authentication required");
}
return this.preferencesService.getPreferences(userId);
}
/**
* PUT /api/users/me/preferences
* Update current user's preferences
*/
@Put()
async updatePreferences(
@Body() updatePreferencesDto: UpdatePreferencesDto,
@Request() req: any
) {
const userId = req.user?.id;
if (!userId) {
throw new UnauthorizedException("Authentication required");
}
return this.preferencesService.updatePreferences(userId, updatePreferencesDto);
}
}

View File

@@ -0,0 +1,104 @@
import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import type {
UpdatePreferencesDto,
PreferencesResponseDto,
} from "./dto";
/**
* Service for managing user preferences
*/
@Injectable()
export class PreferencesService {
constructor(private readonly prisma: PrismaService) {}
/**
* Get user preferences (create with defaults if not exists)
*/
async getPreferences(userId: string): Promise<PreferencesResponseDto> {
let preferences = await this.prisma.userPreference.findUnique({
where: { userId },
});
// Create default preferences if they don't exist
if (!preferences) {
preferences = await this.prisma.userPreference.create({
data: {
userId,
theme: "system",
locale: "en",
settings: {} as unknown as Prisma.InputJsonValue,
},
});
}
return {
id: preferences.id,
userId: preferences.userId,
theme: preferences.theme,
locale: preferences.locale,
timezone: preferences.timezone,
settings: preferences.settings as Record<string, unknown>,
updatedAt: preferences.updatedAt,
};
}
/**
* Update user preferences
*/
async updatePreferences(
userId: string,
updateDto: UpdatePreferencesDto
): Promise<PreferencesResponseDto> {
// Check if preferences exist
const existing = await this.prisma.userPreference.findUnique({
where: { userId },
});
let preferences;
if (existing) {
// Update existing preferences
preferences = await this.prisma.userPreference.update({
where: { userId },
data: {
...(updateDto.theme && { theme: updateDto.theme }),
...(updateDto.locale && { locale: updateDto.locale }),
...(updateDto.timezone !== undefined && {
timezone: updateDto.timezone,
}),
...(updateDto.settings && {
settings: updateDto.settings as unknown as Prisma.InputJsonValue,
}),
},
});
} else {
// Create new preferences
const createData: Prisma.UserPreferenceUncheckedCreateInput = {
userId,
theme: updateDto.theme || "system",
locale: updateDto.locale || "en",
settings: (updateDto.settings || {}) as unknown as Prisma.InputJsonValue,
};
if (updateDto.timezone !== undefined) {
createData.timezone = updateDto.timezone;
}
preferences = await this.prisma.userPreference.create({
data: createData,
});
}
return {
id: preferences.id,
userId: preferences.userId,
theme: preferences.theme,
locale: preferences.locale,
timezone: preferences.timezone,
settings: preferences.settings as Record<string, unknown>,
updatedAt: preferences.updatedAt,
};
}
}

View File

@@ -0,0 +1,16 @@
import { Module } from "@nestjs/common";
import { PreferencesController } from "./preferences.controller";
import { PreferencesService } from "./preferences.service";
import { PrismaModule } from "../prisma/prisma.module";
import { AuthModule } from "../auth/auth.module";
/**
* Module for user-related endpoints (preferences, etc.)
*/
@Module({
imports: [PrismaModule, AuthModule],
controllers: [PreferencesController],
providers: [PreferencesService],
exports: [PreferencesService],
})
export class UsersModule {}