Files
perauset-e2e-mrfzot2p/docs/frontend-architecture.md
2026-07-11 08:34:30 +02:00

853 lines
36 KiB
Markdown

# Perauset Frontend Architecture
Single Next.js application with Mantine v8, server-side rendering, and Pikku RPC for data fetching. Role-based UI adapts the experience from a focused guest view to a full operations dashboard for coordinators and admins.
---
## Table of Contents
1. [Roles and Permissions Summary](#1-roles-and-permissions-summary)
2. [Navigation Structure](#2-navigation-structure)
3. [Route Map](#3-route-map)
4. [Page Specifications](#4-page-specifications)
5. [Shared Components](#5-shared-components)
6. [Data Fetching Strategy](#6-data-fetching-strategy)
7. [Mobile Considerations](#7-mobile-considerations)
8. [Authentication Flow](#8-authentication-flow)
---
## 1. Roles and Permissions Summary
The backend defines 8 roles. The frontend groups them into three tiers for UI purposes:
| Tier | Roles | What they see |
|------|-------|---------------|
| **Admin/Coordinator** | `admin`, `coordinator` | Full operations dashboard: all domains, review queues, audit log, user management |
| **Operational** | `facilitator`, `boat_coordinator`, `kitchen_lead`, `staff` | Domain-specific management views plus personal tools. Each role sees expanded UI for their domain. |
| **Community** | `volunteer`, `community_member`, `guest` | Personal dashboard: own stay, own requests, claimable tasks, meal schedule, dietary profile |
The frontend does NOT enforce permissions -- the backend does via tags. The frontend uses the session's `roles` array to determine which navigation items, pages, and action buttons to render. If a user lacks the permission for a page, the server call will fail with 403, and the UI shows a "not authorized" state.
---
## 2. Navigation Structure
### Layout: Responsive AppShell
- **Desktop** (>= 768px): Collapsible sidebar (240px open, 60px collapsed) + top bar with notifications and user menu
- **Mobile** (< 768px): Bottom tab bar (5 primary items) + hamburger drawer for secondary navigation
### Sidebar Sections (Desktop)
```
[Logo / App Name]
-- PERSONAL --
Dashboard (all authenticated users)
My Stay (all authenticated users)
My Requests (all authenticated users)
Notifications (all authenticated users)
-- OPERATIONS -- (staff+ roles only)
Stays & Rooms (stays.view_all OR rooms.view_all)
Boat Schedule (boats.view_all OR boats.manage)
Task Board (tasks.view_all)
Retreats (retreats.view OR retreats.manage)
Kitchen (kitchen.view OR kitchen.manage)
Inventory (inventory.manage OR inventory.request)
-- ADMIN -- (admin, coordinator only)
Users (users.view)
Audit Log (audit_log.view)
Settings (admin only)
[User Avatar / Menu]
Profile & Dietary
Sign Out
```
### Mobile Bottom Tab Bar
| Tab | Icon | Route | Visible to |
|-----|------|-------|------------|
| Home | house | `/` | All |
| Boats | ship | `/boats` | All (request); operational roles see more |
| Tasks | check-square | `/tasks` | All (claimable); staff+ see board |
| Meals | utensils | `/kitchen` | All |
| More | menu | drawer | All (opens sidebar content) |
---
## 3. Route Map
All routes are under the Next.js App Router. Auth-gated routes use a middleware check; role-gated routes render a `<RoleGate>` wrapper that redirects or shows an empty state.
### Public Routes
| Route | Purpose |
|-------|---------|
| `/login` | Credentials login form (Auth.js) |
| `/auth/error` | Auth error display |
### Authenticated Routes -- Personal
| Route | Page Title | Min. Role | API Calls |
|-------|-----------|-----------|-----------|
| `/` | Dashboard | any | `listStays(userId=me)`, `listNotifications(limit=5)`, `listTasks(status=open, limit=5)`, `listBoatTrips(from=today, limit=3)` |
| `/stay` | My Stay | any | `listStays(userId=me, status=checked_in)`, `listStayRequests(userId=me)` |
| `/stay/request` | Request a Stay | `stays.request` | `requestStay` (mutation), `listRetreats(status=published)` |
| `/requests` | My Requests | any | `listStayRequests(userId=me)`, `listBoatRequests(userId=me)`, `listInventoryRequests(userId=me)` (Note: some of these filters need to be added to the backend or the frontend filters client-side) |
| `/notifications` | Notifications | any | `listNotifications`, `markNotificationRead` |
| `/profile` | Profile & Dietary | any | `getUser(userId=me)`, `upsertDietaryProfile` |
### Authenticated Routes -- Boats
| Route | Page Title | Min. Permission | API Calls |
|-------|-----------|-----------------|-----------|
| `/boats` | Boat Schedule | `boats.request` | `listBoatTrips(from=today)` |
| `/boats/trips/:tripId` | Trip Detail | `boats.request` | `listBoatTrips`, `listBoatRequests(tripId)` |
| `/boats/trips/:tripId/request` | Request Seat | `boats.request` | `requestBoatSeat` (mutation) |
| `/boats/manage` | Boat Management | `boats.manage` | `listBoatTrips`, `listBoatRequests` |
| `/boats/manage/trips/new` | Create Trip | `boats.manage` | `createBoatTrip` (mutation) |
| `/boats/manage/routes` | Route Management | `boats.manage` | `createBoatRoute` (mutation) |
| `/boats/manage/fleet` | Fleet (Boats) | `boats.manage` | `createBoat` (mutation) |
### Authenticated Routes -- Tasks
| Route | Page Title | Min. Permission | API Calls |
|-------|-----------|-----------------|-----------|
| `/tasks` | Task Board | `tasks.claim` | `listTasks` |
| `/tasks/:taskId` | Task Detail | `tasks.claim` | `listTasks`, `claimTask`, `startTask`, `completeTask`, `markTaskBlocked` |
| `/tasks/manage` | Task Management | `tasks.manage` | `listTasks`, `createTaskInstance`, `assignTask`, `updateTask` |
| `/tasks/manage/templates` | Task Templates | `tasks.manage` | `createTaskTemplate` |
### Authenticated Routes -- Retreats
| Route | Page Title | Min. Permission | API Calls |
|-------|-----------|-----------------|-----------|
| `/retreats` | Retreats List | `retreats.view` | `listRetreats` |
| `/retreats/:retreatId` | Retreat Detail | `retreats.view` | `listRetreats(id)`, schedule items, persons |
| `/retreats/manage` | Retreat Management | `retreats.manage` | `listRetreats`, `createRetreat`, `updateRetreat` |
| `/retreats/manage/:retreatId` | Edit Retreat | `retreats.manage` | `updateRetreat`, `addRetreatPerson`, `addRetreatScheduleItem`, `publishRetreat`, `cancelRetreat` |
### Authenticated Routes -- Kitchen
| Route | Page Title | Min. Permission | API Calls |
|-------|-----------|-----------------|-----------|
| `/kitchen` | Meal Schedule | `kitchen.view` | `listMealServices(from=today)` |
| `/kitchen/dietary` | My Dietary Profile | `kitchen.update_dietary` | `upsertDietaryProfile` |
| `/kitchen/manage` | Kitchen Board | `kitchen.manage` | `listMealServices`, `createMealService` |
### Authenticated Routes -- Stays & Rooms (Operational)
| Route | Page Title | Min. Permission | API Calls |
|-------|-----------|-----------------|-----------|
| `/stays` | All Stays | `stays.view_all` | `listStays`, `listStayRequests` |
| `/stays/requests` | Stay Request Queue | `stays.review` | `listStayRequests(status=submitted)`, `approveStayRequest`, `rejectStayRequest` |
| `/stays/:stayId` | Stay Detail | `stays.view_all` | `listStays(stayId)`, `checkInStay`, `checkOutStay`, `cancelStay` |
| `/rooms` | Room Overview | `rooms.view_all` | `listRooms`, `listRoomAllocations` |
| `/rooms/manage` | Room Management | `rooms.manage` | `createRoom`, `updateRoom`, `allocateRoom`, `releaseRoom` |
### Authenticated Routes -- Inventory
| Route | Page Title | Min. Permission | API Calls |
|-------|-----------|-----------------|-----------|
| `/inventory` | Inventory List | `inventory.manage` | `listInventory` |
| `/inventory/request` | Request Supplies | `inventory.request` | `requestInventory` (mutation) |
| `/inventory/requests` | Request Queue | `inventory.manage` | list requests, `reviewInventoryRequest`, `fulfillInventoryRequest` |
### Authenticated Routes -- Admin
| Route | Page Title | Min. Permission | API Calls |
|-------|-----------|-----------------|-----------|
| `/admin/users` | User Directory | `users.view` | `listUsers` |
| `/admin/users/:userId` | User Detail | `users.view` | `getUser`, `updateUserProfile`, `setUserRoles` |
| `/admin/audit` | Audit Log | `audit_log.view` | `listAuditLog` |
---
## 4. Page Specifications
### 4.1 Dashboard (`/`)
The dashboard is role-adaptive. Every user sees it, but the content blocks differ.
**Layout**: Single-column stack on mobile, 2-column grid on desktop.
**Blocks (conditional on role)**:
| Block | Component | Visible to | Data |
|-------|-----------|------------|------|
| Welcome banner | `WelcomeBanner` | All | Session user name, current date |
| Active stay card | `ActiveStayCard` | All | `listStays(userId=me, status=checked_in)` -- shows room, dates, check-out date |
| Upcoming boats | `UpcomingBoatsList` | All | `listBoatTrips(from=today, limit=3)` -- next departures |
| My tasks | `MyTasksList` | `tasks.claim` | `listTasks(assignedTo=me, status!=completed)` |
| Claimable tasks | `ClaimableTasksList` | `tasks.claim` | `listTasks(status=open, limit=5)` |
| Unread notifications | `NotificationPreview` | All | `listNotifications(limit=5, unreadOnly=true)` |
| Stay requests pending | `PendingStayRequestsBadge` | `stays.review` | `listStayRequests(status=submitted)` -- count |
| Boat requests pending | `PendingBoatRequestsBadge` | `boats.manage` | `listBoatRequests(status=submitted)` -- count |
| Inventory requests pending | `PendingInventoryBadge` | `inventory.manage` | count of submitted inventory requests |
| Today's meals | `TodayMealsSummary` | `kitchen.view` | `listMealServices(date=today)` |
| Occupancy summary | `OccupancySummary` | `rooms.view_all` | `listStays(status=checked_in)` count vs. `listRooms` total capacity |
| Retreat in progress | `ActiveRetreatCard` | `retreats.view` | `listRetreats(status=active)` |
**Key interactions**:
- Each card links to its detail page
- Notification items can be marked read inline
- Pending-request badges are clickable shortcuts to the review queue
---
### 4.2 My Stay (`/stay`)
**Purpose**: Guest/community view of their current or upcoming stay.
**Components**:
| Component | Description |
|-----------|-------------|
| `StayStatusTimeline` | Visual timeline: Requested > Approved > Checked In > Checked Out. Highlights current step. |
| `StayDetailCard` | Dates, room allocation (if any), stay type, associated retreat |
| `RequestStayButton` | Mantine `Button` linking to `/stay/request`. Shown when no active stay exists. |
| `StayRequestList` | Table of the user's past and pending stay requests with status badges |
**Data flow**:
- SSR: `listStays(userId=session.userId)` + `listStayRequests(userId=session.userId)` (Note: backend may need a "my requests" variant or the frontend filters by userId client-side after fetching)
---
### 4.3 Request a Stay (`/stay/request`)
**Purpose**: Form to submit a stay request.
**Components**:
| Component | Description |
|-----------|-------------|
| `StayRequestForm` | Mantine form: `requestType` (select), date range picker (`requestedStartAt`, `requestedEndAt`), optional retreat selector (populated from `listRetreats(status=published)`), notes textarea |
| `RequestConfirmation` | Success state after submission showing request ID and "submitted" status |
**Data flow**:
- SSR: `listRetreats(status=published)` for the retreat dropdown
- Client mutation: `requestStay`
---
### 4.4 Stay Request Queue (`/stays/requests`)
**Purpose**: Coordinators review incoming stay requests.
**Components**:
| Component | Description |
|-----------|-------------|
| `StayRequestTable` | Mantine `DataTable` with columns: requester name, type, dates, retreat, status, submitted date. Filterable by status. |
| `StayRequestReviewDrawer` | Slide-out drawer on row click. Shows full details, user profile link. Approve/Reject buttons with optional review notes. |
| `StayRequestStatusFilter` | Segmented control: All / Submitted / Approved / Rejected |
**Data flow**:
- SSR: `listStayRequests(status=submitted)`
- Client mutations: `approveStayRequest`, `rejectStayRequest`
- After approval: prompt to `createStayFromRequest`
---
### 4.5 All Stays (`/stays`)
**Purpose**: Operational view of all active and historical stays.
**Components**:
| Component | Description |
|-----------|-------------|
| `StayTable` | Paginated table: user, dates, type, status, room, check-in/out times |
| `StayFilterBar` | Status filter (segmented), date range picker, user search |
| `StayActions` | Per-row action menu: Check In, Check Out, Cancel, View Detail |
| `StayDetailDrawer` | Drawer with full stay info + room allocation + linked request |
**Data flow**:
- SSR: `listStays(limit=50)`, `listRoomAllocations`
- Client mutations: `checkInStay`, `checkOutStay`, `cancelStay`
---
### 4.6 Room Overview (`/rooms`)
**Purpose**: Visual display of all rooms and their current allocation status.
**Components**:
| Component | Description |
|-----------|-------------|
| `RoomGrid` | Card grid showing each room: name, code, type, capacity, current occupant(s). Color-coded by status (vacant=green, occupied=amber, blocked=red, maintenance=gray). |
| `RoomAllocationTimeline` | Optional Gantt-style timeline view of room allocations over a date range. Uses Mantine `Timeline` or a custom canvas component. |
| `RoomFilterBar` | Filter by type (dorm, private, shared, etc.), status |
| `RoomDetailDrawer` | Drawer showing room details, allocation history, block history |
**Data flow**:
- SSR: `listRooms`, `listRoomAllocations(from=today)`
---
### 4.7 Room Management (`/rooms/manage`)
**Purpose**: Create rooms, allocate guests to rooms, manage blocks.
**Components**:
| Component | Description |
|-----------|-------------|
| `CreateRoomForm` | Modal form: name, code, type, capacity, notes |
| `EditRoomForm` | Same fields, pre-populated |
| `AllocateRoomForm` | Modal: select room, select stay, date range, reason |
| `ReleaseRoomButton` | Action on an active allocation |
| `RoomBlockForm` | Create a room block (maintenance, admin hold, retreat reserved) |
**Data flow**:
- Client mutations: `createRoom`, `updateRoom`, `allocateRoom`, `releaseRoom`
---
### 4.8 Boat Schedule (`/boats`)
**Purpose**: View upcoming boat trips and request seats.
**Components**:
| Component | Description |
|-----------|-------------|
| `BoatScheduleList` | Card list (mobile) or table (desktop) of upcoming trips. Each shows: route (origin > destination), departure time, boat name, available seats indicator, status badge. |
| `TripCard` | Expanded view on click: full details + "Request Seat" button if status is `open` |
| `BoatScheduleCalendar` | Optional calendar view for desktop showing trips by day |
| `DateNavigator` | Day/week selector to browse the schedule |
**Data flow**:
- SSR: `listBoatTrips(from=today, status=open)`
---
### 4.9 Trip Detail (`/boats/trips/:tripId`)
**Components**:
| Component | Description |
|-----------|-------------|
| `TripHeader` | Route, departure/arrival times, boat, status |
| `ManifestList` | List of confirmed passengers (visible to `boats.view_all`) |
| `RequestSeatButton` | Opens inline form or navigates to `/boats/trips/:tripId/request` |
| `RequestsList` | (boats.manage) Table of all seat requests for this trip with confirm/decline actions |
**Data flow**:
- SSR: trip detail from `listBoatTrips`, `listBoatRequests(tripId)`
- Client mutations: `requestBoatSeat`, `confirmBoatRequest`, `declineBoatRequest`
---
### 4.10 Boat Management (`/boats/manage`)
**Purpose**: Full boat operations for boat coordinators.
**Components**:
| Component | Description |
|-----------|-------------|
| `TripManagementTable` | All trips (not just open). Status filter. Actions: open, update, cancel, complete. |
| `CreateTripForm` | Modal: select route, select boat, departure datetime, arrival datetime, notes |
| `RequestReviewPanel` | Pending seat requests across all trips. Bulk confirm/decline. |
| `RouteManagementTab` | CRUD for boat routes |
| `FleetManagementTab` | CRUD for boats (name, capacity, cargo capacity) |
**Tabs within page**: Trips | Requests | Routes | Fleet
**Data flow**:
- SSR: `listBoatTrips`, `listBoatRequests`
- Client mutations: `createBoatTrip`, `openBoatTrip`, `updateBoatTrip`, `cancelBoatTrip`, `completeBoatTrip`, `confirmBoatRequest`, `declineBoatRequest`, `createBoatRoute`, `createBoat`
---
### 4.11 Task Board (`/tasks`)
**Purpose**: Kanban-style board of tasks. Community users see claimable tasks. Staff+ sees all tasks.
**Components**:
| Component | Description |
|-----------|-------------|
| `TaskKanban` | Columns: Open, Assigned, In Progress, Blocked, Completed. Each card shows title, category badge, location, scheduled time. Drag-and-drop on desktop (optional). |
| `TaskListView` | Alternative list view (default on mobile). Sortable by category, status, date. |
| `TaskCard` | Title, category pill, time, location. Click opens detail drawer. |
| `TaskDetailDrawer` | Full task info. Actions based on status: Claim (if open), Start (if assigned to me), Complete, Mark Blocked. |
| `TaskCategoryFilter` | Filter chips: housekeeping, kitchen, maintenance, operations, retreat, transport |
| `ViewToggle` | Board view / List view toggle |
**Data flow**:
- SSR: `listTasks`
- Client mutations: `claimTask`, `startTask`, `completeTask`, `markTaskBlocked`
---
### 4.12 Task Management (`/tasks/manage`)
**Purpose**: Coordinators create tasks, assign to people, manage templates.
**Components**:
| Component | Description |
|-----------|-------------|
| `CreateTaskForm` | Modal: title, description, category, location, scheduled start/end, optional template, linked retreat/stay |
| `AssignTaskDrawer` | Select a user, add notes. Triggers `assignTask`. |
| `TaskTemplateTable` | List of templates with title, category, recurrence, claimable flag. Create/edit actions. |
| `CreateTemplateForm` | Modal: title, description, category, estimated minutes, default location, recurrence cron, isClaimable toggle |
**Data flow**:
- Client mutations: `createTaskInstance`, `assignTask`, `updateTask`, `createTaskTemplate`
---
### 4.13 Retreats List (`/retreats`)
**Purpose**: Browse retreats. Community users see published/active retreats. Facilitators+ see all statuses.
**Components**:
| Component | Description |
|-----------|-------------|
| `RetreatCardGrid` | Card per retreat: name, dates, status badge, capacity indicator, description excerpt |
| `RetreatStatusFilter` | Tabs or segmented: All / Published / Active / Completed / Draft (draft only for `retreats.manage`) |
**Data flow**:
- SSR: `listRetreats`
---
### 4.14 Retreat Detail (`/retreats/:retreatId`)
**Components**:
| Component | Description |
|-----------|-------------|
| `RetreatHeader` | Name, dates, status, capacity bar |
| `RetreatSchedule` | Timeline/agenda view of schedule items (title, time, location, lead person) |
| `RetreatParticipantList` | Table of persons: name, role (organizer/facilitator/participant), attendance status |
| `RetreatDescription` | Full description text |
**Data flow**:
- SSR: retreat detail, schedule items, persons (these may need dedicated list endpoints or be embedded in a "get retreat" response -- if not available, consider adding `getRetreat` endpoint)
---
### 4.15 Retreat Management (`/retreats/manage/:retreatId`)
**Purpose**: Full CRUD for a retreat.
**Components**:
| Component | Description |
|-----------|-------------|
| `RetreatForm` | Edit name, description, dates, capacity, check-in/out defaults |
| `ScheduleEditor` | Add/edit/remove schedule items. Each item: title, start, end, location, lead person |
| `PersonManager` | Add persons (by user search or email/name for external). Set role type. Track attendance status. |
| `RetreatActions` | Publish, Cancel buttons with confirmation modals |
**Data flow**:
- Client mutations: `updateRetreat`, `addRetreatPerson`, `addRetreatScheduleItem`, `publishRetreat`, `cancelRetreat`
---
### 4.16 Meal Schedule (`/kitchen`)
**Purpose**: View upcoming meals for all community members.
**Components**:
| Component | Description |
|-----------|-------------|
| `MealDayView` | Grouped by day, showing breakfast/lunch/dinner/snack cards. Each card: meal type icon, time, headcount, menu notes, status badge. |
| `MealDatePicker` | Navigate between days |
| `DietaryProfileLink` | Banner linking to `/kitchen/dietary` if profile is incomplete |
**Data flow**:
- SSR: `listMealServices(date=today)`
---
### 4.17 Kitchen Board (`/kitchen/manage`)
**Purpose**: Kitchen lead manages meal services.
**Components**:
| Component | Description |
|-----------|-------------|
| `MealServiceTable` | Table with date, type, headcount, status, actions. Filterable by date range and meal type. |
| `CreateMealServiceForm` | Modal: date, type (breakfast/lunch/dinner/snack), planned headcount, menu notes, optional linked retreat |
| `MealStatusUpdater` | Quick status transitions: planned > ready > served > completed |
| `DietarySummary` | Aggregated dietary needs for a given date (count of vegetarian, vegan, gluten-free, etc. across expected attendees) |
**Data flow**:
- SSR: `listMealServices`
- Client mutations: `createMealService`
---
### 4.18 Dietary Profile (`/kitchen/dietary` or `/profile`)
**Components**:
| Component | Description |
|-----------|-------------|
| `DietaryProfileForm` | Toggle switches: isVegetarian, isVegan, isGlutenFree, isDairyFree, hasNutAllergy. Text fields: allergyNotes, dislikes, otherRequirements. |
**Data flow**:
- Client mutation: `upsertDietaryProfile`
---
### 4.19 Inventory (`/inventory`)
**Components**:
| Component | Description |
|-----------|-------------|
| `InventoryTable` | Table: name, unit, current qty, minimum qty, status indicator (low stock = qty < minimum). Sortable. |
| `LowStockAlert` | Banner at top if any items are below minimum |
| `RequestSuppliesButton` | Links to `/inventory/request` |
**Data flow**:
- SSR: `listInventory`
---
### 4.20 Inventory Request (`/inventory/request`)
**Components**:
| Component | Description |
|-----------|-------------|
| `InventoryRequestForm` | Item selector (from inventory list or free-text name), quantity, unit, purpose |
**Data flow**:
- SSR: `listInventory` (for item selector)
- Client mutation: `requestInventory`
---
### 4.21 Inventory Request Queue (`/inventory/requests`)
**Components**:
| Component | Description |
|-----------|-------------|
| `InventoryRequestTable` | Table: item name, qty, requester, status, date. Actions: review (approve/reject), fulfill. |
| `ReviewDrawer` | Review notes, approve or reject |
| `FulfillButton` | Marks a request as fulfilled |
**Data flow**:
- Client mutations: `reviewInventoryRequest`, `fulfillInventoryRequest`
---
### 4.22 User Directory (`/admin/users`)
**Components**:
| Component | Description |
|-----------|-------------|
| `UserTable` | Paginated table: name, email, roles (pills), status, joined date |
| `UserSearchBar` | Search by name or email |
| `UserStatusFilter` | Active / Inactive / Archived |
**Data flow**:
- SSR: `listUsers`
---
### 4.23 User Detail (`/admin/users/:userId`)
**Components**:
| Component | Description |
|-----------|-------------|
| `UserProfileCard` | Name, email, phone, status, date of birth, nationality, emergency contact |
| `RoleManager` | Multi-select of roles with save button. Uses `setUserRoles`. |
| `UserProfileEditForm` | Edit profile fields. Uses `updateUserProfile`. |
| `UserActivityTimeline` | Recent stays, tasks, requests (read-only, assembled from multiple API calls) |
**Data flow**:
- SSR: `getUser(userId)`
- Client mutations: `setUserRoles`, `updateUserProfile`
---
### 4.24 Audit Log (`/admin/audit`)
**Components**:
| Component | Description |
|-----------|-------------|
| `AuditLogTable` | Paginated table: timestamp, user, action, table, record ID, changed fields (JSON expandable) |
| `AuditLogFilters` | Filter by table name, user, action, date range |
**Data flow**:
- SSR: `listAuditLog`
---
### 4.25 Notifications (`/notifications`)
**Components**:
| Component | Description |
|-----------|-------------|
| `NotificationList` | Grouped by date. Each item: icon by type, title, body preview, timestamp, read/unread indicator. Click marks as read and navigates to linked entity. |
| `MarkAllReadButton` | Bulk action (may need backend endpoint or iterative calls) |
| `NotificationBell` | In top bar, shows unread count badge. Dropdown with last 5 notifications. |
**Data flow**:
- SSR: `listNotifications`
- Client mutation: `markNotificationRead`
---
## 5. Shared Components
### Layout Components
| Component | Description |
|-----------|-------------|
| `AppShell` | Mantine `AppShell` with sidebar, header, main content area. Responsive breakpoints. |
| `RoleGate` | Wrapper that checks `session.roles` against required permissions. Renders children or a "not authorized" message. |
| `SidebarNav` | Role-filtered navigation links. Active state via `usePathname()`. |
| `MobileTabBar` | Fixed bottom bar for mobile. 5 tabs with icons and labels. |
| `PageHeader` | Title, breadcrumbs, optional action buttons (top-right). |
| `NotificationBell` | Header icon with unread count, dropdown preview. |
### Data Display Components
| Component | Description |
|-----------|-------------|
| `StatusBadge` | Mantine `Badge` with color mapped to status string. Reused across stays, boats, tasks, retreats, inventory. |
| `DateRangeDisplay` | Formatted date range (e.g., "Mar 15 - Mar 22, 2026") |
| `UserAvatar` | Avatar with name tooltip. Links to user detail for admin roles. |
| `PaginatedTable` | Wrapper around Mantine `Table` with limit/offset pagination controls. |
| `EmptyState` | Illustration + message + optional CTA button for empty lists. |
| `LoadingState` | Skeleton loaders matching the expected content shape. |
| `ConfirmModal` | Reusable confirmation dialog for destructive actions (cancel stay, reject request, etc.) |
### Form Components
| Component | Description |
|-----------|-------------|
| `DateRangePicker` | Mantine `DatePickerInput` configured for range selection |
| `UserSelect` | Async searchable select that queries `listUsers` |
| `RoomSelect` | Select populated from `listRooms` |
| `RetreatSelect` | Select populated from `listRetreats` |
| `StatusFilter` | Segmented control or select for status enum values |
---
## 6. Data Fetching Strategy
### Server-Side (SSR)
Next.js App Router server components call Pikku RPC functions directly (or via a server-side HTTP client to `localhost:6002/api/...`). This provides:
- Fast initial page loads
- SEO for any public pages (currently none, but future-proofed)
- Session cookie forwarded from the incoming request
```
// Example: app/stays/page.tsx (server component)
async function StaysPage() {
const stays = await pikkuClient.listStays({ limit: 50 })
return <StayTable data={stays} />
}
```
### Client-Side (Mutations + Polling)
- All write operations (create, approve, reject, claim, etc.) are client-side via Pikku RPC
- After mutation success: invalidate relevant server data using `router.refresh()` or React Query / SWR cache invalidation
- Polling: notifications badge polls every 60 seconds via `listNotifications(limit=1, unreadOnly=true)` to update the count
- Optimistic updates for instant feedback on claim/complete task actions
### Recommended Data Layer
Use `@tanstack/react-query` for client-side cache management:
- Query keys follow the pattern: `['domain', 'action', params]` (e.g., `['stays', 'list', { status: 'checked_in' }]`)
- Mutations invalidate related query keys on success
- Server components pass initial data as props; client components hydrate from those props
---
## 7. Mobile Considerations
The island context means many users will be on phones, often with intermittent connectivity.
### Design Principles
1. **Touch-first**: All interactive targets minimum 44x44px. Use Mantine's `size="lg"` for buttons and inputs on mobile.
2. **Single-column layouts**: All pages stack to single column below 768px. No horizontal scrolling.
3. **Bottom-anchored actions**: Primary actions (Request Seat, Claim Task, Check In) use sticky bottom action bars on mobile, not top-right buttons.
4. **Offline tolerance**: Show last-fetched data with a "stale data" indicator when offline. Queue mutations and retry when connectivity returns (stretch goal -- implement in v2).
5. **Minimal data transfer**: Use pagination aggressively. Default `limit=20` on mobile (vs. 50 on desktop). Avoid loading images where text suffices.
### Mobile-Specific Components
| Component | Description |
|-----------|-------------|
| `BottomActionBar` | Sticky bottom bar with primary action button(s). Replaces top-right action buttons on mobile. |
| `SwipeableCard` | Swipe-right to claim a task, swipe-left to dismiss. Provides fast interaction on task board. |
| `PullToRefresh` | Pull-down gesture triggers `router.refresh()` on list pages. |
| `CompactTable` | Table that collapses to a card-list layout on mobile, showing only key fields with expandable detail. |
---
## 8. Authentication Flow
### Login (`/login`)
- Credentials form (email + password) using Auth.js
- On success: redirect to `/` (dashboard)
- Session stored as HTTP-only cookie
- Auth.js endpoints at `/auth/*` (handled by `auth.wiring.ts`)
### Session Management
- Middleware (`middleware.ts` in Next.js root) checks for valid session on all routes except `/login` and `/auth/*`
- Session object shape: `{ userId: string, roles: string[] }`
- Roles array used by `RoleGate` and sidebar filtering
- Session refresh handled automatically by Auth.js JWT rotation
### Logout
- POST to `/auth/signout` (Auth.js endpoint)
- Clear local state and redirect to `/login`
---
## Appendix: Complete API Endpoint Reference
For reference, here is every backend endpoint grouped by domain, with its HTTP method, route, and required permission tag.
### System
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| GET | `/health-check` | none (public) | `healthCheck` |
### Users
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| GET | `/users` | `users.view` | `listUsers` |
| GET | `/users/:userId` | `users.view` | `getUser` |
| PUT | `/users/:userId/profile` | `users.manage` | `updateUserProfile` |
| GET | `/roles` | authenticated | `listRoles` |
| PUT | `/users/:userId/roles` | `roles.assign` | `setUserRoles` |
### Stays
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| POST | `/stays/requests` | `stays.request` | `requestStay` |
| POST | `/stays/requests/:requestId/approve` | `stays.review` | `approveStayRequest` |
| POST | `/stays/requests/:requestId/reject` | `stays.review` | `rejectStayRequest` |
| GET | `/stays/requests` | `stays.view_all` | `listStayRequests` |
| POST | `/stays` | `stays.manage` | `createStayFromRequest` |
| POST | `/stays/:stayId/check-in` | `stays.manage` | `checkInStay` |
| POST | `/stays/:stayId/check-out` | `stays.manage` | `checkOutStay` |
| POST | `/stays/:stayId/cancel` | `stays.manage` | `cancelStay` |
| GET | `/stays` | `stays.view_all` | `listStays` |
### Rooms
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| POST | `/rooms` | `rooms.manage` | `createRoom` |
| PUT | `/rooms/:roomId` | `rooms.manage` | `updateRoom` |
| POST | `/rooms/allocations` | `rooms.manage` | `allocateRoom` |
| POST | `/rooms/allocations/:allocationId/release` | `rooms.manage` | `releaseRoom` |
| GET | `/rooms` | `rooms.view_all` | `listRooms` |
| GET | `/rooms/allocations` | `rooms.view_all` | `listRoomAllocations` |
### Boats
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| POST | `/boats/routes` | `boats.manage` | `createBoatRoute` |
| POST | `/boats` | `boats.manage` | `createBoat` |
| POST | `/boats/trips` | `boats.manage` | `createBoatTrip` |
| POST | `/boats/trips/:tripId/open` | `boats.manage` | `openBoatTrip` |
| POST | `/boats/trips/:tripId/request-seat` | `boats.request` | `requestBoatSeat` |
| POST | `/boats/requests/:requestId/confirm` | `boats.manage` | `confirmBoatRequest` |
| POST | `/boats/requests/:requestId/decline` | `boats.manage` | `declineBoatRequest` |
| PUT | `/boats/trips/:tripId` | `boats.manage` | `updateBoatTrip` |
| POST | `/boats/trips/:tripId/cancel` | `boats.manage` | `cancelBoatTrip` |
| POST | `/boats/trips/:tripId/complete` | `boats.manage` | `completeBoatTrip` |
| GET | `/boats/trips` | `boats.view_all` | `listBoatTrips` |
| GET | `/boats/requests` | `boats.view_all` | `listBoatRequests` |
### Tasks
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| POST | `/tasks/templates` | `tasks.manage` | `createTaskTemplate` |
| POST | `/tasks` | `tasks.manage` | `createTaskInstance` |
| POST | `/tasks/:taskId/assign` | `tasks.assign` | `assignTask` |
| POST | `/tasks/:taskId/claim` | `tasks.claim` | `claimTask` |
| POST | `/tasks/:taskId/start` | `tasks.manage` | `startTask` |
| PUT | `/tasks/:taskId` | `tasks.manage` | `updateTask` |
| POST | `/tasks/:taskId/complete` | `tasks.manage` | `completeTask` |
| POST | `/tasks/:taskId/block` | `tasks.manage` | `markTaskBlocked` |
| GET | `/tasks` | `tasks.view_all` | `listTasks` |
### Retreats
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| POST | `/retreats` | `retreats.manage` | `createRetreat` |
| PUT | `/retreats/:retreatId` | `retreats.manage` | `updateRetreat` |
| POST | `/retreats/:retreatId/persons` | `retreats.manage` | `addRetreatPerson` |
| POST | `/retreats/:retreatId/publish` | `retreats.manage` | `publishRetreat` |
| POST | `/retreats/:retreatId/cancel` | `retreats.manage` | `cancelRetreat` |
| POST | `/retreats/:retreatId/schedule` | `retreats.manage` | `addRetreatScheduleItem` |
| GET | `/retreats` | `retreats.view` | `listRetreats` |
### Kitchen
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| PUT | `/kitchen/dietary-profile` | `kitchen.update_dietary` | `upsertDietaryProfile` |
| POST | `/kitchen/meal-services` | `kitchen.manage` | `createMealService` |
| GET | `/kitchen/meal-services` | `kitchen.view` | `listMealServices` |
### Inventory
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| POST | `/inventory/requests` | `inventory.request` | `requestInventory` |
| POST | `/inventory/requests/:requestId/review` | `inventory.manage` | `reviewInventoryRequest` |
| POST | `/inventory/requests/:requestId/fulfill` | `inventory.manage` | `fulfillInventoryRequest` |
| GET | `/inventory` | `inventory.manage` | `listInventory` |
### Notifications
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| GET | `/notifications` | authenticated | `listNotifications` |
| POST | `/notifications/:notificationId/read` | authenticated | `markNotificationRead` |
### Audit
| Method | Route | Permission | Function |
|--------|-------|------------|----------|
| GET | `/audit-log` | `audit_log.view` | `listAuditLog` |
---
## Appendix: Backend Gaps Identified
During architecture design, several backend additions would improve the frontend experience. These are not blockers for v1 but should be prioritized for the first iteration.
| Gap | Impact | Suggested Endpoint |
|-----|--------|--------------------|
| No "get single entity" endpoints for stay, retreat, trip, task | Detail pages must filter list endpoints or add dedicated `getStay`, `getRetreat`, `getBoatTrip`, `getTask` | `GET /stays/:stayId`, `GET /retreats/:retreatId`, `GET /boats/trips/:tripId`, `GET /tasks/:taskId` |
| No "my requests" filter on stay requests | Guest cannot efficiently see only their own requests | Add `userId` filter param to `listStayRequests` |
| No "my boat requests" filter | Guest cannot see their own boat reservation requests | Add `userId` filter param to `listBoatRequests` |
| No "my inventory requests" view | Users who requested supplies can't track them | Add `listInventoryRequests` with `userId` filter |
| No "get retreat" with schedule + persons | Retreat detail page needs 3 separate calls or a compound endpoint | `GET /retreats/:retreatId` returning nested schedule + persons |
| No list endpoints for boat routes or boats (fleet) | Boat management UI needs to populate dropdowns | `GET /boats/routes`, `GET /boats` |
| No "mark all notifications read" | Common UX pattern, currently requires N calls | `POST /notifications/mark-all-read` |
| No unread notification count | Notification bell badge requires fetching full list | `GET /notifications/unread-count` |
| `listStayRequests` filtered to own user for non-admin | Guests with `stays.request` but not `stays.view_all` need to see their own requests | Either a separate `GET /stays/my-requests` or backend auto-filters by session userId when `stays.view_all` is absent |