# 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 `` 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 } ``` ### 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 |