Adds profile and settings pages

This commit is contained in:
2026-06-22 23:59:04 +02:00
parent 6976eb4337
commit 2544e20b3c
49 changed files with 2741 additions and 34 deletions
@@ -0,0 +1,279 @@
# Frontend Components — Unit 6: Profile, Settings & CMS Placeholder
## Component Hierarchy
```mermaid
graph TD
Auth["_authenticated.tsx\n(layout route)"]
Profile["profile.tsx\nProfilePage"]
PIF["ProfileInfoForm\n(name + email edit)"]
CPD["ChangePasswordDialog\n(dialog overlay)"]
RB["RoleDisplay\n(read-only badge)"]
Settings["settings.tsx\nSettingsPage\n[RoleGuard: Owner]"]
AS["AvailabilitySection\n(mode selector + message)"]
ASBI["AvailabilityStatusBadge\n(reused from Unit 4)"]
PM1["PlaceholderCard\nModule Management"]
PM2["PlaceholderCard\nSystem Configuration"]
PM3["PlaceholderCard\nBranding / Theme"]
Cms["cms.tsx\nCmsPage\n[RoleGuard: Owner]"]
P403["403.tsx\nAccessDeniedPage"]
P404["$404.tsx\nNotFoundPage"]
Auth --> Profile
Auth --> Settings
Auth --> Cms
Auth --> P403
Auth --> P404
Profile --> PIF
Profile --> CPD
Profile --> RB
Settings --> AS
AS --> ASBI
Settings --> PM1
Settings --> PM2
Settings --> PM3
classDef layout fill:#4CAF50,stroke:#2e7d32,color:#000
classDef page fill:#2196F3,stroke:#0d47a1,color:#000
classDef component fill:#9C27B0,stroke:#4a148c,color:#000
classDef reused fill:#FF9800,stroke:#e65100,color:#000
classDef error fill:#F44336,stroke:#b71c1c,color:#000
class Auth layout
class Profile page
class Settings page
class Cms page
class PIF component
class CPD component
class RB component
class AS component
class PM1 component
class PM2 component
class PM3 component
class ASBI reused
class P403 error
class P404 error
```
Text alternative: _authenticated layout route contains Profile (with ProfileInfoForm, ChangePasswordDialog, RoleDisplay), Settings (Owner-only: AvailabilitySection with badge + 3 PlaceholderCards), CmsPage (Owner-only), 403, and 404.
---
## Component Specifications
### `profile.tsx` — ProfilePage
**Purpose**: Displays and allows editing of the logged-in user's name and email. Shows role as read-only. Provides Change Password access.
**State**:
- Form state managed by `react-hook-form`
- `isSaving` — boolean (PUT /Users/me in flight)
- `isPasswordDialogOpen` — boolean
**Props**: None (reads from `useAuth()`)
**API integrations**:
- `PUT /api/v1/Users/me` — save profile edits (new hook: `useUpdateProfile`)
- `POST /api/v1/Auth/refresh` — called after successful profile save via `AuthContext.refresh()`
**Sections**:
1. **Profile info card**: Name (text input), Email (text input), Role (read-only badge), Save button
2. **Security card**: "Change Password" button → opens `ChangePasswordDialog`
**Validation** (via `zod`):
```
name: z.string().min(1, "Name is required")
email: z.string().email("Invalid email address")
```
---
### `ProfileInfoForm`
**Purpose**: Form fields for name and email editing. Embedded in ProfilePage.
**Props**:
- `defaultValues: { name: string; email: string }`
- `onSave: (data: UpdateProfileRequest) => Promise<void>`
- `isSaving: boolean`
---
### `ChangePasswordDialog`
**Purpose**: Modal dialog for changing password. Opened from ProfilePage.
**Props**:
- `open: boolean`
- `onClose: () => void`
**State** (internal, via `react-hook-form`):
- `currentPassword`, `newPassword`, `confirmPassword`
**API integration**: `POST /api/v1/Auth/change-password` (new hook: `useChangePassword`)
**Validation** (via `zod`):
```
currentPassword: z.string().min(1, "Required")
newPassword: z.string()
.min(8, "At least 8 characters")
.regex(/[A-Z]/, "At least 1 uppercase letter")
.regex(/[a-z]/, "At least 1 lowercase letter")
.regex(/[0-9]/, "At least 1 digit")
.regex(/[^a-zA-Z0-9]/, "At least 1 special character")
confirmPassword: z.string()
// .refine: confirmPassword === newPassword
```
**On success**: Close dialog, emit success toast.
---
### `RoleDisplay`
**Purpose**: Read-only badge showing the user's current role. Not editable.
**Props**:
- `role: string`
---
### `settings.tsx` — SettingsPage
**Purpose**: Owner-only management page. Contains availability controls and placeholder sections for future settings.
**State**:
- Availability data from `useAvailabilityStatus()` (reused from Unit 4)
- `selectedMode` — controlled select: `"Available" | "Maintenance" | "Unavailable"`
- `reason` — string (optional message input)
- `isSaving` — boolean
**Props**: None
**API integrations**:
- `GET /api/v1/Availability/status` — via `useAvailabilityStatus` (existing hook)
- `POST /api/v1/Availability/admin/status` — via new `useUpdateAvailability` mutation; on success invalidates `useAvailabilityStatus` query
**Sections**:
1. **Availability section** (`AvailabilitySection`): Current status badge + mode selector (radio or select) + optional message text area + Save button
2. **PlaceholderCard** — "Module Management" (locked, coming soon)
3. **PlaceholderCard** — "System Configuration" (locked, coming soon)
4. **PlaceholderCard** — "Branding / Theme" (locked, coming soon)
---
### `AvailabilitySection`
**Purpose**: Embedded in SettingsPage. Shows current availability and provides controls to change it.
**Props**:
- `currentStatus: AvailabilityStatus`
- `onSave: (request: UpdateAvailabilityRequest) => Promise<void>`
- `isSaving: boolean`
**Sub-component**: Renders `<AvailabilityStatusBadge>` (reused from Unit 4, imported from `components/shared/`)
---
### `PlaceholderCard`
**Purpose**: Reusable locked card for future settings sections.
**Props**:
- `title: string`
- `description?: string`
**Visual**: Dimmed card with lock icon and "Coming soon" label.
---
### `cms.tsx` — CmsPage
**Purpose**: Owner-only placeholder for the future CMS feature.
**Props**: None
**Content**: Icon (e.g. `LayoutGrid` from lucide-react) + heading "Content Management System" + brief description: "This is where you will manage your CMS content. This feature is coming soon."
**State**: None (pure presentational)
---
### `403.tsx` — AccessDeniedPage
**Purpose**: Shown when RoleGuard rejects access to a route.
**Props**: None
**Content**:
- Icon: `ShieldOff` or `Lock` (lucide-react)
- Heading: "Access Denied"
- Message: "You don't have permission to view this page."
- Button: "Back to Dashboard" → navigates to `/`
**State**: None (pure presentational)
---
### `$404.tsx` — NotFoundPage
**Purpose**: TanStack Router catch-all for unknown routes.
**Props**: None
**Content**:
- Icon: `FileQuestion` (lucide-react)
- Heading: "Page Not Found"
- Message: "The page you're looking for doesn't exist."
- Button: "Back to Dashboard" → navigates to `/`
**State**: None (pure presentational)
---
## New API Hooks
| Hook | File | Endpoint | Method |
|------|------|----------|--------|
| `useUpdateProfile` | `src/api/useProfile.ts` | `PUT /api/v1/Users/me` | useMutation |
| `useChangePassword` | `src/api/useProfile.ts` | `POST /api/v1/Auth/change-password` | useMutation |
| `useUpdateAvailability` | `src/api/useAvailability.ts` (extend existing) | `POST /api/v1/Availability/admin/status` | useMutation |
## New Backend Endpoints
| Endpoint | Method | Controller | Policy |
|----------|--------|------------|--------|
| `/api/v1/Users/me` | PUT | UsersController | Authenticated |
| `/api/v1/Auth/change-password` | POST | AuthController | Authenticated |
## i18n Keys Required
**Profile page**:
- `profile.title`, `profile.name`, `profile.email`, `profile.role`, `profile.save`, `profile.saving`, `profile.saveSuccess`
- `profile.changePassword`, `profile.changePassword.current`, `profile.changePassword.new`, `profile.changePassword.confirm`, `profile.changePassword.success`
**Settings page**:
- `settings.title`, `settings.availability.title`, `settings.availability.save`, `settings.availability.saveSuccess`
- `settings.availability.mode.available`, `settings.availability.mode.maintenance`, `settings.availability.mode.unavailable`
- `settings.availability.reason`, `settings.placeholder.comingSoon`
- `settings.modules.title`, `settings.systemConfig.title`, `settings.branding.title`
**Error pages**:
- `error.403.title`, `error.403.message`, `error.404.title`, `error.404.message`, `error.backToDashboard`
## Unit Test Scope (Q8: C — all pages)
| Component | Test focus |
|-----------|------------|
| ProfilePage | Renders AuthContext user data; name/email inputs; save triggers PUT; password dialog opens |
| ChangePasswordDialog | Validation (mismatch, policy); success flow; error handling |
| SettingsPage | Fetches and displays availability; mode change + save triggers POST; placeholder sections render |
| CmsPage | Renders heading and description |
| AccessDeniedPage (403) | Renders heading, message, Back to Dashboard navigates to `/` |
| NotFoundPage (404) | Renders heading, message, Back to Dashboard navigates to `/` |