Basic Design (UI Specification)
Project: PTX Channel Manager (ptx-cm)
Version: 2.7.0
Date: 2026-03-12
Style: Professional Blue (Cloudbeds/SiteMinder-inspired)
1. Design System
1.1 Reference Source
- Style: Professional Blue — hotel operations dashboard (Cloudbeds/SiteMinder inspired)
- Rationale: Staff use 8+ hours/day; blue palette reduces eye strain, conveys trust/reliability, suits data-dense operational dashboards
1.2 Color Palette
| Token | Value | Usage |
|---|---|---|
--color-primary | #1E3A5F | Sidebar, nav headers, primary actions |
--color-primary-light | #2B5A8F | Hover states, active nav items |
--color-primary-dark | #0F1F33 | Sidebar active state, deep headers |
--color-accent | #3B82F6 | Links, interactive elements, focus rings |
--color-accent-light | #60A5FA | Hover on links, secondary buttons |
--color-success | #22C55E | Synced status, available rooms, confirmed bookings |
--color-warning | #F59E0B | Session expiring, low availability, sync delayed |
--color-danger | #EF4444 | Overbooking alert, sync failed, session expired |
--color-info | #06B6D4 | Informational badges, tooltips |
--color-bg-page | #F1F5F9 | Page background (slate-100) |
--color-bg-card | #FFFFFF | Cards, panels, modals |
--color-bg-sidebar | #1E3A5F | Sidebar background |
--color-text-primary | #0F172A | Headings, body text (slate-900) |
--color-text-secondary | #64748B | Descriptions, labels (slate-500) |
--color-text-muted | #94A3B8 | Placeholders, disabled text (slate-400) |
--color-text-inverse | #FFFFFF | Text on dark backgrounds |
--color-border | #E2E8F0 | Card borders, dividers (slate-200) |
--color-border-focus | #3B82F6 | Input focus ring |
1.3 OTA Brand Colors (Status Indicators)
| OTA | Color | Usage |
|---|---|---|
| Booking.com | #003580 | OTA badge, channel label |
| Agoda | #5F2688 | OTA badge, channel label |
| Traveloka | #0194F3 | OTA badge, channel label |
| Expedia | #FBCE00 | OTA badge, channel label (dark text) |
1.4 Country Display
Countries displayed as rounded pill badges matching OTA badge style (Section 3.2). 2-letter country code on colored background. White text for contrast. No flag emojis — text-only for cross-platform consistency.
| Code | Pill BG Color | Text Color | Example |
|---|---|---|---|
| VN | #DA251D | #FFFFFF | [VN] |
| ID | #CE1126 | #FFFFFF | [ID] |
| MY | #010066 | #FFFFFF | [MY] |
Style: Same border-radius: 9999px (pill), padding: 3px 10px, font-size: 12px/600 as OTA badges. Ensures visual consistency between OTA and country indicators.
Contrast: All combinations meet WCAG AA contrast ratio (white text on dark backgrounds). Indonesia uses #CE1126 (darker crimson) instead of #FF0000 for better white text readability.
Usage: Property tables, booking rows, user lists, dashboard dropdown.
1.5 Format Presets (Per-User)
Each user selects a display format preset via Settings. Frontend uses Intl API.
| Preset | Locale | Date | Number | Currency Example |
|---|---|---|---|---|
| Vietnamese | vi | dd/MM/yyyy | 1.000.000 | 1.000.000 VND |
| Indonesian | id | dd/MM/yyyy | 1.000.000 | Rp 1.000.000 |
| Malay | ms | dd/MM/yyyy | 1,000,000 | RM 1,000,000 |
| English | en | yyyy-MM-dd | 1,000,000 | VND 1,000,000 |
Default: en. Stored in User.locale field. Sync logs (S-17) always use ISO format regardless of user locale.
1.6 Typography
| Token | Value | Usage |
|---|---|---|
--font-sans | 'Inter', system-ui, -apple-system, sans-serif | All UI text |
--font-mono | 'JetBrains Mono', 'Fira Code', monospace | IDs, timestamps, logs |
--text-h1 | 24px / 700 | Page titles |
--text-h2 | 20px / 600 | Section headers |
--text-h3 | 16px / 600 | Card titles, subsections |
--text-h4 | 14px / 600 | Labels, small headings |
--text-body | 14px / 400 | Body text, table cells |
--text-small | 13px / 400 | Helper text, timestamps |
--text-caption | 12px / 400 | Badges, meta info |
1.7 Spacing Scale
| Token | Value |
|---|---|
--space-xs | 4px |
--space-sm | 8px |
--space-md | 16px |
--space-lg | 24px |
--space-xl | 32px |
--space-2xl | 48px |
--space-3xl | 64px |
1.8 Border Radius
| Token | Value | Usage |
|---|---|---|
--radius-sm | 4px | Inputs, small badges |
--radius-md | 8px | Cards, buttons, dropdowns |
--radius-lg | 12px | Modals, large panels |
--radius-full | 9999px | Avatars, pill badges, OTA dots |
1.9 Shadows
| Token | Value | Usage |
|---|---|---|
--shadow-sm | 0 1px 2px rgba(0,0,0,0.05) | Cards at rest |
--shadow-md | 0 4px 6px rgba(0,0,0,0.07) | Cards on hover, dropdowns |
--shadow-lg | 0 10px 15px rgba(0,0,0,0.1) | Modals, floating panels |
1.10 CJX Stage Variables
| Stage | Description | Screens |
|---|---|---|
| Onboarding | First-time setup: connect OTA accounts, import properties, map rooms | S-05, S-06, S-07, S-04 |
| Usage | Daily operations: monitor sync, review bookings, manage availability, manage own profile, manage customers, view process instances | S-02, S-08, S-09, S-10, S-18, S-26, S-27, S-29 |
| Retention | Efficiency gains: bulk rate updates, analytics, role management, workflow config, process management | S-11, S-12, S-14, S-19, S-20, S-28 |
| Discovery | Advanced features: rate rules, parity checker | S-13, S-15 |
2. Layout Structure
2.1 Shell Layout
┌──────────────────────────────────────────────────────┐
│ [Sidebar 240px] │ [Main Content Area] │
│ │ │
│ Logo │ ┌─ Top Bar ───────────────────┐ │
│ ───────── │ │ [🇻🇳 VN] ▾ 🔔 👤 │ │
│ Dashboard │ └────────────────────────────┘ │
│ Properties │ │
│ Bookings │ ┌─ Content ──────────────────┐ │
│ Calendar │ │ │ │
│ Rates │ │ [Page-specific content] │ │
│ Reports │ │ │ │
│ ───────── │ │ │ │
│ OTA Accounts │ └──────────────────────────────┘ │
│ Sync Logs │ │
│ Settings │ │
│ ───────── │ │
│ Processes │ (admin, S-28) │
│ Process Log │ (settings view, S-29) │
│ OTA Status │ │
│ ● Booking.com │ │
│ ● Agoda │ │
│ ○ Traveloka │ │
│ ✕ Expedia │ │
└──────────────────────────────────────────────────────┘- Sidebar: Fixed, 240px wide,
--color-bg-sidebarbackground - Sidebar items: Visibility controlled by user's role permissions. Items where user has no View permission are hidden. Uses
hasPermission(module, VIEW)check. - Sidebar bottom: Real-time OTA account status summary (dots: green=active, yellow=expiring, red=expired/error)
- Top bar: Country dropdown selector (
[VN] ▾pill badge style), notification bell (alert count badge), user avatar/menu. Top bar haspadding-top: 8pxfor visual breathing room. - Content area: Scrollable,
--color-bg-pagebackground, max-width 1440px - Country selector: Persists last selection in localStorage; staff users locked to their assigned country
2.2 Responsive Breakpoints
| Breakpoint | Layout |
|---|---|
| >= 1280px | Full sidebar + content |
| 768-1279px | Collapsible sidebar (icons only), content expands |
| < 768px | Hidden sidebar (hamburger toggle), full-width content |
3. Component Patterns
3.1 Status Badge
[● Active] — green bg, green text, rounded pill
[⚠ Expiring] — yellow bg, yellow text
[✕ Expired] — red bg, red text
[○ Inactive] — gray bg, gray text
[? 2FA] — blue bg, blue text (requires manual login)3.2 OTA Channel Badge
[Booking.com] — #003580 bg, white text, rounded pill
[Agoda] — #5F2688 bg, white text
[Traveloka] — #0194F3 bg, white text
[Expedia] — #FBCE00 bg, dark text3.3 Country Badge
Same visual style as OTA badges (Section 3.2) — rounded pill with 2-letter code, no flag emojis.
[VN] — #DA251D bg, white text, rounded pill
[ID] — #CE1126 bg, white text, rounded pill
[MY] — #010066 bg, white text, rounded pillMatches OTA badge dimensions and border-radius. Tooltip shows full country name on hover.
3.4 KPI Card
┌─────────────────────┐
│ Total Bookings Today │ ← label (text-secondary)
│ 47 │ ← value (text-h1, text-primary)
│ ↑ 12% vs yesterday │ ← trend (text-small, success/danger)
└─────────────────────┘3.5 Data Table (Standard List Page Layout)
All list pages (S-03, S-08, S-21, S-26) use a unified full-height flex layout with 4 fixed zones:
┌─────────────────────────────────────────────────────┐
│ HEADER BAR h-14 fixed, bg-card, border-b │
│ Title (text-lg font-bold) │ [Search...] │ [Actions] │
├─────────────────────────────────────────────────────┤
│ FILTER BAR bg-card, border-b, px-6 py-2.5 │
│ Active Filters: [chip ✕] [chip ✕] [Dropdown ▾] [Clear All] │
├─────────────────────────────────────────────────────┤
│ TABLE AREA flex-1, overflow-auto │
│ thead: sticky top-0 z-10 │
│ th: text-[10px] uppercase tracking-wider semibold │
│ bg-gray-50 dark:bg-slate-800/60 │
│ td: px-3 py-1.5 whitespace-nowrap │
│ row hover: bg-gray-50 dark:bg-slate-800/50 │
├─────────────────────────────────────────────────────┤
│ PAGINATION FOOTER bg-card, border-t, px-6 py-3 │
│ Rows [20▾] │ Showing 1-20 of 156 │ [<][1][2][3][>] │
└─────────────────────────────────────────────────────┘Zone 1 — Header Bar:
- Classes:
bg-card border-b border-border flex-shrink-0 flex items-center justify-between px-6 h-14 z-10 - Left: Title (
text-lg font-bold text-text-primary tracking-tight) + Search input with icon - Right: Action buttons (Export, Add New)
Zone 2 — Filter Bar:
- Classes:
bg-card border-b border-border px-6 py-2.5 flex items-center gap-3 flex-wrap flex-shrink-0 - Shows "Active Filters:" label + removable FilterChip tags when filters active
- Dropdown selectors for each filter dimension
- "Clear All" link when any filter active
Zone 3 — Table Area:
- Outer:
flex-1 overflow-auto bg-card relative - Table:
min-w-full border-collapse - thead:
sticky top-0 z-10 - th:
px-3 py-2 text-left text-[10px] font-semibold text-text-secondary uppercase tracking-wider border-b border-border bg-gray-50 dark:bg-slate-800/60 whitespace-nowrap - tbody:
divide-y divide-border - td:
px-3 py-1.5 whitespace-nowrap - Row hover:
hover:bg-gray-50 dark:hover:bg-slate-800/50 transition-colors cursor-pointer
Zone 4 — Pagination Footer:
- Classes:
bg-card border-t border-border px-6 py-3 flex items-center justify-between flex-shrink-0 - Left: Rows per page selector + "Showing X to Y of Z" text
- Right: Previous/Next buttons with page number buttons between
Shared Components:
FilterChip(components/ui/filter-chip.tsx): removable filter tag with X button.inline-flex items-center px-2.5 py-1 rounded border border-blue-200 bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 text-xs font-mediumResizableHeader(components/ui/resizable-header.tsx): drag-to-resize column header. Used by Properties (S-03) and Suppliers (S-21)
Page-Specific Variations:
- S-08 Bookings: adds checkbox column for bulk select
- S-03 Properties, S-21 Suppliers: use ResizableHeader for column widths
- S-26 Customers: no checkboxes, no resizable headers
3.6 Alert Banner
┌─ 🔴 ─────────────────────────────────────────────┐
│ OVERBOOKING: Property "Siam Lodge" - Deluxe Room │
│ Date: 2026/02/15 | Booked: 12/10 rooms │
│ Sources: Booking.com (2), Agoda (1) [Resolve →] │
└───────────────────────────────────────────────────┘- Critical: Red left border + light red background
- Warning: Yellow left border + light yellow background
- Info: Blue left border + light blue background
3.7 OTA Account Card
┌──────────────────────────────────────┐
│ [Booking.com] ● Active │
│ Main Account │
│ 12 properties | Last sync: 2 min ago │
│ [Manage] [Refresh] [Import] │
└──────────────────────────────────────┘3.8 Property Import Row
┌──────────────────────────────────────────────────┐
│ ☐ Grand Palace Hotel, Ho Chi Minh City │
│ 5 room types | 42 rooms total │
│ Match: "Grand Palace HCMC" (Agoda) [✓ Linked] │ ← cross-OTA match
└──────────────────────────────────────────────────┘4. Screen Specifications
S-01: Login Screen
- Phase: P1
- Layout: Centered card on
--color-primary-darkfull-page background - Elements:
- Logo + "PTX Channel Manager" heading
- Email input (required, type=email)
- Password input (masked, toggle visibility)
- "Sign In" button (primary, full-width)
- Error toast on failed login
- Transitions:
- Success → S-02 Dashboard
- Error → Inline error message
S-02: Dashboard
- Phase: P1
- Layout: Grid layout with country tabs + KPI row + 2-column content + activity log panel (super admin only)
- CJX Stage: Usage
Activity Log Panel (Super Admin Only):
- Visibility: Only visible when user has
super_adminrole - Position: Right column, above OTA Account Status (if admin) or alone (if non-admin)
- Style: Dark terminal-style panel with monospace font (
--font-mono) - Refresh: SWR polling every 5 seconds, pausable by user
- Display: Last N entries from
activity_logsPostgreSQL table viaGET /activity-logs?limit=N(default: 100, user can increase via limit selector 1-500). Previously flat-file based; now DB-backed (FR-35) - Log Format: Each row shows:
TIMESTAMP | EMAIL | METHOD | PATH | STATUS | SCREEN - Method Badges: Color-coded HTTP method pills
- GET: Gray (
#6B7280) - POST: Green (
#22C55E) - PATCH: Blue (
#3B82F6) - PUT: Yellow (
#F59E0B) - DELETE: Red (
#EF4444)
- GET: Gray (
- Status Colors:
- 2xx/3xx: White text on dark background (default)
- 4xx: Amber text
- 5xx: Red text
- Controls: Pause/Resume toggle button, Limit selector (1-500 entries), Auto-scroll toggle
Country Selector (top bar):
- Dropdown select styled like OTA badges: selected value shows as pill badge
[VN] ▾ - Dropdown options:
[VN] Vietnam,[ID] Indonesia,[MY] Malaysia,[All] All Countries - Each option shows country pill badge + full name for clarity
- Staff: locked to assigned country (tabs disabled)
- Manager: free selection, "All" default
- Persisted in localStorage
KPI Row (4 cards):
| Card | Data | Source |
|---|---|---|
| Properties Online | Count of properties (in selected country) with all OTAs synced | E-05 via E-04 status |
| Today's Bookings | New bookings detected today (in selected country) | E-08 created_at |
| Sync Health | % of OTA accounts with status=active | E-04 status |
| Active Alerts | Count of unresolved alerts (in selected country) | E-11 is_resolved |
Left Column (60%):
- Alert Panel: Unresolved alerts sorted by severity. Each: property name, alert type, message, timestamp, [Resolve] button
- Recent Bookings: Last 10 bookings. Columns: Property, Guest, OTA (badge), Check-in, Check-out, Amount, Status
Right Column (40%):
- OTA Account Status: Per-account accordion. Each shows: OTA badge, status dot, properties count, last sync time, pending jobs
- Quick Actions: "Connect OTA Account" button, "Refresh All Sessions" button, "Force Sync" button
S-03: Properties List
- Phase: P1 (base), P2 (layout standardization — FR-43)
- Layout: Standard list page layout (§3.5)
- CJX Stage: Onboarding / Usage
- Related FR: FR-05, FR-43
- Elements:
- Header bar: "Properties List" title + search input + "Add Property" button
- Filter bar: Status filter (All/Active/Inactive) with FilterChip + Clear All
- Table (ResizableHeader columns): Name, Country (CountryPill), Connected OTAs (OtaBadge), Sync Status (StatusBadge), Room Types (count)
- Sticky uppercase table headers, compact row padding
- Full pagination footer with rows selector + page numbers
- Country scope auto-applied from top bar selector
- Transitions:
- Click row → S-04 Property Detail
- Click "Add Property" → modal (PropertyForm)
S-04: Property Detail
- Phase: P1
- Layout: Tabbed interface within property context
- CJX Stage: Onboarding
Info Tab:
- Property name, country, timezone, currency, address (edit form)
- Save/Cancel buttons
Room Types Tab:
- Room type table: Name, Base Rate, Total Rooms, Max Occupancy, Suppliers (allocation badges), Actions
- "Suppliers" column: compact badges showing
SupplierName: Nper allocation. Warning badge (⚠) if SUM != totalRooms. Pencil icon → S-23 Supplier Allocation Manager modal. - "Add Room Type" button
- Inline edit or modal form
OTA Connections Tab:
- List of OTA connections linked via OtaAccount
- Each row shows: OTA badge, OTA account label, OTA property ID, status (from parent OtaAccount), is_active toggle
- Room mapping sub-table per connection: internal room → OTA room/rate plan
- "Edit Mapping" action per connection
S-05: OTA Accounts List
- Phase: P1
- Layout: Card grid or data table
- CJX Stage: Onboarding
- Elements:
- "Connect OTA Account" button (primary, top right)
- OTA account cards (see 3.7 pattern): OTA badge, label, status, property count, last sync, actions
- Filter: OTA type, Status
- Actions per card: [Manage] → edit credentials, [Refresh] → refresh session, [Import] → S-07, [Delete]
- Transitions:
- Click "Connect OTA Account" → S-06
- Click [Import] → S-07
S-06: Connect OTA Account
- Phase: P1
- Layout: Step wizard (2 steps)
- CJX Stage: Onboarding
Step 1: OTA & Credentials
- OTA selector: 4 OTA cards with brand colors (click to select)
- Login form (email/username + password)
- 2FA method selector: None / TOTP (enter secret) / Manual Login
- If Manual Login: "Open Extranet" button launches Playwright browser for manual authentication. System captures session after login.
- "Test Connection" button
Step 2: Confirmation
Connection test result (success/fail)
If success: OTA account label input, "Save & Import Properties" or "Save & Skip Import"
If fail: Error message, retry option
Transitions:
- "Save & Import Properties" → S-07
- "Save & Skip Import" → S-05
S-07: Import Properties ❌ NOT IMPLEMENTED
- Phase: P1
- Implementation Status: OTA adapter
fetchProperties()is a stub. No import UI exists. - Layout: Property selection list with matching UI
- CJX Stage: Onboarding
- Elements:
Property Discovery:
- Auto-fetches properties from OTA account on load
- Loading state: skeleton list
- Property list with checkboxes (see 3.8 pattern): name, city, country, room type count, room count
Cross-OTA Matching (when 2nd+ OTA connected):
- For each discovered property, show best match from existing properties
- Match confidence: High (auto-linked, editable) / Low (suggest, ask) / None (create new)
- User can override: select different existing property or "Create New"
Actions:
"Import Selected" button → creates properties, room types, connections, mappings
Progress indicator during import
Success summary: N properties imported, N room types created, N mappings set
Transitions:
- Import complete → S-05 (with success toast)
S-08: Bookings List
- Phase: P1 (base), P2 (OTA status column — FR-41, layout standard — FR-43)
- Layout: Standard list page layout (§3.5) — reference implementation
- CJX Stage: Usage
- Related FR: FR-03, FR-41, FR-43
- Elements:
- Filters row: Property (dropdown), OTA (multi-select badges), Status (confirmed/cancelled), OTA Status (dropdown, populated from
GET /ota-status, FR-41), Date range (check-in), Country (auto-set for staff) - Table columns: Property, Guest Name, OTA (badge), Room Type, Check-in, Check-out, Rooms, Amount, Status (badge), OTA Status (badge, FR-41), Detected At
- Sort by any column
- Pagination (25/50/100 per page)
- Export CSV button
- Filters row: Property (dropdown), OTA (multi-select badges), Status (confirmed/cancelled), OTA Status (dropdown, populated from
OTA Status Column (FR-41):
- Displays colored badge using
OtaStatusDefnormalization when mapping exists - Fallback: raw OTA status string in neutral gray badge when no mapping configured
- Null/empty: shows "—" dash when booking has no OTA status (pre-sync or manual bookings)
- Uses
useOtaStatus()hook to fetch and cacheOtaStatusDefmappings (SWR, 60s revalidate)
OTA Status Filter (FR-41):
Dropdown populated from
GET /ota-statusgrouped by OTA typeFilter sends
otaStatusquery param toGET /bookingsIndependent from internal Status filter — both can be active simultaneously
Transitions:
- Click row → S-09 Booking Detail
S-09: Booking Detail
- Phase: P1 (base), P2 (workflow enhancement)
- Layout: Header + card grid (2-col) with conditional visibility
- CJX Stage: Usage
- Related FR: FR-03, FR-26, FR-28, FR-41
Header (BookingHeader):
- Back button + "Booking Detail" title
- StatusBadge with dynamic color from BookingStatusDef (fallback to hardcoded STATUS_COLORS)
- Status transition buttons row (P2): one button per available transition for current status + user role
- Each transition button shows target status label + color dot
- Buttons hidden if user lacks Bookings EDIT permission or no transitions available
Card Grid (2-column on lg, 1-column on mobile):
Guest Info Card (conditional visibility via uiConfig.sections):
- Guest name (inline editable if in uiConfig.editableFields)
- Guest email (inline editable if in uiConfig.editableFields)
- Number of guests (inline editable if in uiConfig.editableFields)
- Number of rooms (inline editable if in uiConfig.editableFields)
Booking Info Card (conditional visibility):
- OTA source (badge with brand color)
- OTA booking ID (monospace, copyable)
- Booking status (StatusBadge with dynamic color)
- OTA status (badge with OtaStatusDef color/label, raw string fallback; shows "—" if null) (FR-41)
- OTA status last updated (timestamp, user's dateFormat; hidden if null) (FR-41)
- Total amount + currency
Stay Details Card (conditional visibility):
- Check-in / Check-out dates
- Number of nights (calculated)
Property & Room Card (conditional visibility):
- Property name
- Room type
- Created at timestamp
Booking History Section (below card grid):
- Vertical timeline of audit log entries fetched from
GET /bookings/:id/history - Each entry shows: dynamic status icon (from BookingStatusDef.icon), action description, actor name, timestamp (user's dateFormat)
- Entries connected by vertical line for visual continuity
- OTA sync events (action=create from system) show with OTA brand icon
- "Revert" action available on latest history entry if user has Bookings EDIT permission
- Revert opens confirmation dialog → PATCH /bookings/:id/revert
- Sync history: When booking was detected, when availability was pushed to other OTAs (from
syncHistoryin booking detail response) - Raw OTA data (collapsible JSON view, visible per uiConfig)
Status Transition Dialog (P2):
- Modal opens on transition button click
- Shows: "Change status from {current} to {target}?"
- Optional note textarea
- Confirm/Cancel buttons
- Loading spinner during API call
- On confirm: PATCH
/bookings/:id/status→ refresh booking data via SWR mutate
Data Flow (P2):
useApiGet('bookings/{id}') → booking data (includes otaStatus, otaStatusUpdatedAt)
useApiGet('booking-status/workflow') → { statuses, transitions } (SWR cached)
useOtaStatus() → OtaStatusDef[] (SWR cached 60s) (FR-41)
useAuth() → user.roleName
Resolve transitions: filter by fromKey=booking.status + role in allowedRoles
Resolve uiConfig: statuses[booking.status].uiConfig[roleName] || uiConfig['*']
Resolve statusColor: statuses[booking.status].color
Resolve otaStatusDisplay: otaStatusDefs.find(d => d.rawStatus === booking.otaStatus && d.otaType === booking.otaType)?.normalizedLabel || booking.otaStatus || '—'
Resolve otaStatusColor: otaStatusDefs.find(...)?.color || '#94A3B8' (muted gray fallback)Graceful Degradation:
- If workflow not loaded: show all sections, hide transition buttons
- If no transitions for current status: show no buttons
- If user lacks BOOKINGS.EDIT: hide all mutation UI
Layout (P2 enhanced):
┌─────────────────────────────────────────────────────┐
│ ← Back Booking Detail │
│ │
│ [● Confirmed] [→ Check-in] [→ Cancel] [→ No Show]│
│ │
│ ┌─ Guest Info ────────┐ ┌─ Booking Info ─────────┐│
│ │ Name: John Doe [✎] │ │ OTA: [Booking.com] ││
│ │ Email: j@mail [✎] │ │ Ref: BC-123456 ││
│ │ Guests: 2 [✎] │ │ Status: [● Confirmed] ││
│ │ Rooms: 1 [✎] │ │ OTA Status: [● conf.] ││
│ │ │ │ Amount: 2,500 THB ││
│ └──────────────────────┘ └────────────────────────┘│
│ │
│ ┌─ Stay Details ──────┐ ┌─ Property & Room ──────┐│
│ │ Check-in: 02/15 │ │ Property: Siam Lodge ││
│ │ Check-out: 02/18 │ │ Room: Deluxe Double ││
│ │ Nights: 3 │ │ Created: 02/14 09:30 ││
│ └──────────────────────┘ └────────────────────────┘│
└─────────────────────────────────────────────────────┘S-10: Availability Calendar ❌ NOT IMPLEMENTED
- Phase: P2
- Implementation Status: Specced and brainstormed. No route, no component, no page exists yet.
- Route:
/availability(sidebar Operations group, between Rates and Suppliers) - Layout: Property sidebar (left, same pattern as Rate Manager S-11) + date grid (right)
- CJX Stage: Usage
- Actors: U-03 (Manager), U-04 (OTA Operator)
- Backend:
GET /api/v1/availability?propertyId=X&startDate=YYYY-MM-DD&endDate=YYYY-MM-DD→ nested room types with daily availabilityPUT /api/v1/availability/block→{ roomTypeId, date, blockedRooms }→ queues PUSH_AVAILABILITY sync job- New
AvailabilityController+AvailabilityServiceinapps/api/src/modules/availability/ - Existing:
AvailabilityCalcService,AvailabilitySyncProcessor,BookingHooksService
- Components:
apps/web/components/availability/availability-grid.tsx— date grid table (rooms × dates)apps/web/components/availability/availability-cell.tsx— single cell with color + countapps/web/components/availability/block-room-popover.tsx— click-to-block/unblock popover
- Elements:
- Property sidebar: scrollable list, click to select, room types expand underneath (reuse rates pattern)
- Date range navigation:
◀ [03/01 – 03/14] ▶(14-day default, shift ±7 days) - Grid header row: dates (day-of-week + day number)
- Grid rows: one per room type, cells show
available / total(e.g., "3/10") - Cell color thresholds: Green (>50%), Yellow (20–50%), Red (<20%), Black (0%)
- Click cell →
BlockRoomPopover: current stats + number input for blockedRooms + "Block" / "Unblock" button - Block action → immediate API call → sync job queued → SWR revalidation
- Legend bar at bottom: color key
- Country scope: filtered by user's country (or all for managers)
- SWR polling: 30s refresh interval (availability changes on booking pull)
- Sticky first column: room type names visible on horizontal scroll
- Brainstorm ref:
plans/reports/brainstorm-260310-1130-availability-calendar.md
S-11: Rate Manager 🔄 REPLACING (v2.7.0 Formula Engine)
- Phase: P2
- Implementation Status: Replacing simple adjustments with formula-based rate calculator. Route
/rates, components inapps/web/components/rates/. - Layout: Property sidebar (left) + Net price header + OTA config cards (right, vertical stack)
- CJX Stage: Retention
- Components:
formula-rate-manager.tsx— main container (replacespricing-config-matrix.tsx)ota-rate-config-card.tsx— collapsible card per OTA connectionrate-line-row.tsx— single rate line within cardadd-rate-line-dialog.tsx— dialog for adding rate linesrate-overview-matrix.tsx— portfolio overview (kept, no changes)format-currency.ts— utility (kept)ota-sync-status-cards.tsx— sync status (kept)
- Backend:
apps/api/src/modules/ota-rate-configs/(controller, service, DTOs),formula-engine.service.ts(mathjs),apps/api/src/modules/rate-rules/(CRUD, retained) - Elements:
- Header area:
- Property sidebar selector (existing pattern)
- Room type selector (existing)
- Net price input: toggle Manual / Airbnb-sourced, number input with currency formatting
- OTA Config Cards (one per active OTA connection):
- Card header: OTA name + brand color icon + BAR rate badge
- Parameter inputs (rendered dynamically from E-23 template
parametersJSONB):- Each param: label + number input with min/max bounds
- E.g., Commission: [15%], Promote: [5%], Surcharge (Traffic Boost): [2%]
- BAR display: auto-calculated read-only amount (server-side via
/ota-rate-configs/calculate) - Rate lines table:
- Columns: Name | Type badge (base/member/campaign) | Discount % | Calculated rate
- Inline edit: click → inputs appear → save/cancel
- Delete: confirm dialog
- "+ Add Rate Line" button →
add-rate-line-dialog.tsx
- Footer actions:
- "Sync All OTAs" button →
POST /ota-rate-configs/sync→ createspush_ratesSyncJob per connection - "Sync Selected" button (multi-select OTAs)
- "Sync All OTAs" button →
- Live recalculation: On param change or Net price change, debounced 500ms call to
/ota-rate-configs/calculateupdates all BAR + line rates on screen
- Header area:
- API Endpoints:
GET /ota-formula-templates— list all templatesGET /ota-rate-configs?propertyId=— get configs for propertyPOST /ota-rate-configs— create configPUT /ota-rate-configs/:id— update paramsPOST /ota-rate-configs/:id/rate-lines— add rate linePUT /ota-rate-lines/:id— update rate lineDELETE /ota-rate-lines/:id— remove rate linePOST /ota-rate-configs/calculate— preview all rates (read-only)POST /ota-rate-configs/sync— push to OTAs
- Wireframe:
┌──────────────────────────────────────────────────────┐ │ Property: Villa Sunrise │ Room: Deluxe King │ │ Net Price: ₫1,200,000 │ [Manual ▼] │ ├──────────────────────────────────────────────────────┤ │ ┌─ Trip.com ──────── Commission: [15%] Promote: [5%] │ │ │ Surcharge: [2%] BAR: ₫1,359,649 │ │ │ ├─ Basic: ₫1,359,649 (0% off) │ │ │ ├─ Trip Plus: ₫1,223,684 (-10%) │ │ │ ├─ Early Bird: ₫1,155,702 (-15%) │ │ │ └─ Lastmin: ₫1,087,719 (-20%) │ │ │ [+ Add Rate Line] │ │ └─────────────────────────────────────────────────────│ │ ┌─ Agoda ─────────── Commission: [20%] Promote: [0%] │ │ │ BAR: ₫1,500,000 │ │ │ ├─ Member: ₫1,350,000 (-10%) │ │ │ [+ Add Rate Line] │ │ └─────────────────────────────────────────────────────│ │ ┌─ Expedia ────────── Commission: [18%] Promote: [0%]│ │ │ BAR: ₫1,463,415 │ │ │ ├─ Member: ₫1,317,073 (-10%) │ │ │ [+ Add Rate Line] │ │ └─────────────────────────────────────────────────────│ │ [Sync All OTAs] [Sync Selected] │ └──────────────────────────────────────────────────────┘ - Deprecated components (to remove after migration):
pricing-config-matrix.tsx,add-rate-plan-dialog.tsx,ota-rate-cell.tsx,rate-preview-badges.tsx
S-12: Booking Timeline ❌ NOT IMPLEMENTED
- Phase: P2
- Implementation Status: No route, no component, no page exists.
- Layout: Horizontal Gantt chart
- CJX Stage: Usage
- Elements:
- Y-axis: Room types (grouped by property)
- X-axis: Dates (scrollable, 30-day default view)
- Bars: Booking blocks, color-coded by OTA
- Hover: Guest name, dates, OTA, amount
- Click: Opens S-09 Booking Detail
- Filters: Property, OTA, date range
S-13: Rate Parity Report ❌ NOT IMPLEMENTED
- Phase: P3
- Implementation Status: Not built.
- Layout: Comparison table
- CJX Stage: Discovery
- Elements:
- Property selector
- Date range selector
- Table: Room Type | Date | Booking.com Rate | Agoda Rate | Traveloka Rate | Expedia Rate | Parity Status
- Parity status: "Match" (green) or "Mismatch +X%" (red)
- Filter: Show mismatches only toggle
S-14: Analytics Dashboard ❌ NOT IMPLEMENTED
- Phase: P3
- Implementation Status: Not built.
- Layout: Chart grid (2x2)
- CJX Stage: Retention
- Elements:
- Date range selector (7d / 30d / 90d / custom)
- Property filter (all or specific)
- Country filter
- Chart 1: Revenue by OTA (stacked bar chart, monthly)
- Chart 2: Occupancy rate trend (line chart, daily)
- Chart 3: Booking source distribution (donut chart)
- Chart 4: ADR trend (line chart, daily)
- Summary KPI cards below charts: Total Revenue, Avg Occupancy, Avg ADR, Total Bookings
S-15: Rate Rules Config ❌ NOT IMPLEMENTED
- Phase: P3
- Implementation Status: Not built.
- Layout: Rule list + add form
- CJX Stage: Discovery
- Elements:
- Active rules table: Property, Room Type, OTA, Rule Type, Value, Date Range, Status, Actions
- "Add Rule" button → modal form:
- Property selector
- Room type selector (or "All")
- OTA selector (or "All")
- Rule type: Markup % | Discount % | Fixed override
- Value input
- Date range (optional, for seasonal)
- Enable/disable toggle
S-16: Settings
- Phase: P1
- Layout: Tabbed settings page (4 tabs)
- CJX Stage: Onboarding
- Note: Users, Roles, Countries, Booking Statuses moved to S-24 Master Data (
/master-data)
Workflow Tab (default):
- Booking workflow configuration (same content as S-20, now embedded here)
- Status list + Mermaid diagram + transition management + UI visibility config
Preferences Tab (per-user):
- Display format: select dropdown (Vietnamese / Indonesian / Malay / English)
- Live preview showing sample formatted values:
- Date:
10/02/2026 - Number:
1.000.000 - Currency:
1.000.000 VND
- Date:
- Save button
Notifications Tab:
- LINE Notify: Token input, test button
- Email: SMTP config (host, port, user, password), test button
- Alert preferences: Which alert types to notify on (checkboxes)
System Tab:
- Sync intervals: Booking pull frequency (default 3 min), Verification frequency (default 10 min)
- Buffer rooms: Default buffer per room type (0-3)
- Session refresh interval
- Audit log retention: Number of days to keep audit logs (default 90)
S-17: Sync Job Log
- Phase: P1
- Layout: Filterable log table
- CJX Stage: Usage
- Elements:
- Filters: Property, OTA Account, Job Type, Status, Date range
- Table: Timestamp, Property, OTA (badge), Job Type, Status (badge), Duration, Error (truncated)
- Click row → expand to show full error message + payload
- Auto-refresh toggle (5s interval)
- Clear completed jobs button
S-18: User Profile
- Phase: P2
- Layout: Single-column form card, max-width 640px, centered in content area
- CJX Stage: Usage
- Related FR: FR-16, FR-17, FR-19
- Access: Top bar user avatar → "Profile" link, or direct navigation to
/profile
Profile Info Section:
- Card with heading "Profile"
- Initials-based avatar (circle,
--color-primarybg,--text-h1white text, first letter of name) - Name input (editable, required)
- Email input (editable, required, validates uniqueness on blur)
- Country display (read-only text with pill badge for staff; read-only for all users — country assigned by manager)
- Role display (read-only badge: "Manager" or "Staff")
- Locale selector dropdown (Vietnamese / Indonesian / Malay / English) with live format preview:
- Date:
10/02/2026 - Number:
1.000.000 - Currency:
1.000.000 VND
- Date:
- "Save Changes" button (primary, disabled until form dirty)
- Success toast on save
Password Section:
- Separate card with heading "Change Password"
- Current password input (masked, required)
- New password input (masked, required, min 8 chars)
- Confirm new password input (masked, must match new)
- Inline validation: password strength indicator, match check
- "Update Password" button (primary)
- Error toast on wrong current password (401)
- Success toast + clear fields on success
Preferences Section:
- Separate card with heading "Preferences"
- Theme toggle: Light / Dark switch
- Persisted in localStorage, applied immediately via CSS class on
<html> - No backend call
Layout:
┌─────────────────────────────────────────┐
│ ← Back to Dashboard │
│ │
│ ┌─ Profile ──────────────────────────┐ │
│ │ [NV] Nguyen Van │ │
│ │ │ │
│ │ Name: [Nguyen Van ] │ │
│ │ Email: [nguyen@ptx.com ] │ │
│ │ Country: [VN] Vietnam │ │
│ │ Role: [Staff] │ │
│ │ Locale: [Vietnamese ▾] │ │
│ │ Preview: 10/02/2026 │ │
│ │ │ │
│ │ [Save Changes] │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─ Change Password ─────────────────┐ │
│ │ Current: [•••••••• ] │ │
│ │ New: [•••••••• ] │ │
│ │ Confirm: [•••••••• ] │ │
│ │ │ │
│ │ [Update Password] │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─ Preferences ─────────────────────┐ │
│ │ Theme: [Light ○ ● Dark] │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘- Transitions:
- Back link → S-02 Dashboard
- Save success → toast, stay on page
- Password update success → toast, clear password fields
S-19: Role Management
- Phase: P2
- Layout: Full-width form with permission matrix grid
- CJX Stage: Retention
- Related FR: FR-20, FR-21, FR-22, FR-23
- Access: Settings > Roles tab → "Create Role" or click existing role
Role Info Section:
- Name input (slug format, e.g.
warehouse_staff, used as key — disabled for system roles) - Label input (display name, e.g. "Warehouse Staff")
- Description textarea (optional)
Permission Matrix Section:
- Grid table: rows = modules, columns = permission actions (View, Create, Edit, Delete)
- Each cell = checkbox (checked = bit set, unchecked = bit unset)
- Module rows: Dashboard, Properties, Room Types, OTA Accounts, OTA Connections, Bookings, Availability, Sync Jobs, Rates, Rate Rules, Alerts, Settings, Users
- Row header shows module name with icon
- Column header shows action name with bit value (V=1, C=2, E=4, D=8)
- "Select All" checkbox per row (sets all 4 bits = 15)
- "Select All" checkbox per column (e.g., check all View permissions)
- For system preset roles: checkboxes are editable (admin can adjust preset permissions)
- Bitmask preview: shows computed integer per module (e.g.,
properties: 15)
Actions:
- "Save Role" button (primary)
- "Delete Role" button (danger, hidden for system roles)
- "Duplicate Role" button (secondary — creates copy with "_copy" suffix)
Layout:
┌─────────────────────────────────────────────────────────┐
│ ← Back to Settings │
│ │
│ ┌─ Role Info ────────────────────────────────────────┐ │
│ │ Name: [warehouse_staff ] │ │
│ │ Label: [Warehouse Staff ] │ │
│ │ Description: [Manages warehouse... ] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Permissions ──────────────────────────────────────┐ │
│ │ View Create Edit Delete All │ │
│ │ ───────────────────────────────────────────── │ │
│ │ Dashboard [✓] [ ] [ ] [ ] [ ] │ │
│ │ Properties [✓] [✓] [✓] [✓] [✓] │ │
│ │ Room Types [✓] [✓] [✓] [✓] [✓] │ │
│ │ OTA Accounts [✓] [ ] [ ] [ ] [ ] │ │
│ │ OTA Connections [✓] [ ] [ ] [ ] [ ] │ │
│ │ Bookings [✓] [ ] [✓] [ ] [ ] │ │
│ │ Availability [✓] [ ] [✓] [ ] [ ] │ │
│ │ Sync Jobs [✓] [ ] [ ] [ ] [ ] │ │
│ │ Rates [✓] [ ] [ ] [ ] [ ] │ │
│ │ Rate Rules [ ] [ ] [ ] [ ] [ ] │ │
│ │ Alerts [✓] [ ] [✓] [ ] [ ] │ │
│ │ Settings [ ] [ ] [ ] [ ] [ ] │ │
│ │ Users [ ] [ ] [ ] [ ] [ ] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ [Delete Role] [Duplicate] [Save Role] │
└─────────────────────────────────────────────────────────┘- Validation:
- Name: required, unique, slug format (lowercase + underscores)
- Label: required
- At least one permission must be set
- Transitions:
- Save → back to S-16 Roles tab with success toast
- Delete → confirmation modal → back to S-16 Roles tab
S-20: Workflow Config
- Phase: P2
- Layout: 2-panel top + detail panel bottom
- CJX Stage: Retention
- Related FR: FR-25, FR-28, FR-29, FR-30
- Access: Settings sidebar → "Booking Workflow" or direct
/settings/booking-workflow - Permission: Requires Settings EDIT permission
Top Left Panel — Status List:
- Clickable rows listing all BookingStatusDef entries (non-deleted)
- Each row: color dot + label + key (muted) + transition count badge
- Selected status highlighted with accent border
- Sorted by sortOrder
Top Right Panel — Mermaid Diagram:
- Read-only Mermaid stateDiagram-v2 preview
- Rendered with mermaid.js client-side
- Shows all active transitions with role annotations on arrows
- Auto-refreshes when transitions are modified
- Example:
stateDiagram-v2 confirmed --> checked_in : [SA, Admin, OTA] confirmed --> cancelled : [SA, Admin, CS] confirmed --> no_show : [SA, Admin] checked_in --> checked_out : [SA, Admin] cancelled --> confirmed : [SA, Admin]
Bottom Panel — Selected Status Detail (appears on status click):
Transitions Tab:
- Table: Target Status | Allowed Roles | Hooks | Sort Order | Active | Actions
- Each row shows one outgoing transition from selected status
- "Add Transition" button → inline form or modal:
- Target status dropdown (excludes self and existing targets)
- Role multi-select (empty = all roles allowed)
- Hooks checkboxes: ☑ Audit Log, ☑ Update Availability (action: restore/reduce), ☑ Send Notification (template, channels)
- Sort order input
- Edit/Delete actions per row
- Changes saved via POST/PATCH/DELETE
/booking-status-transitions
UI Visibility Tab:
- Role selector dropdown (shows all role names + "*" wildcard)
- Per selected role, checkboxes for:
- Sections: ☑ Guest Info, ☑ Booking Info, ☑ Stay Details, ☑ Property & Room
- Buttons: ☑ Edit Guest, ☑ Change Room (future)
- Editable Fields: ☑ guestName, ☑ guestEmail, ☑ numGuests, ☑ numRooms
- Save updates PATCH
/booking-status/:key/ui-config
Layout:
┌─────────────────────────────────────────────────────────┐
│ Booking Workflow Configuration │
├─────────────────────┬───────────────────────────────────┤
│ │ │
│ Status List │ Mermaid Flow Diagram │
│ (clickable) │ (read-only preview) │
│ │ │
│ ● confirmed ▸ │ confirmed ──→ checked_in │
│ ● checked_in ▸ │ checked_in ──→ checked_out │
│ ● cancelled ▸ │ confirmed ──→ cancelled │
│ ● no_show │ confirmed ──→ no_show │
│ ● checked_out │ cancelled ──→ confirmed │
│ │ │
├─────────────────────┴───────────────────────────────────┤
│ │
│ Selected: "confirmed" │
│ [Transitions] [UI Visibility] │
│ │
│ ┌─ Transitions ──────────────────────────────────────┐ │
│ │ → checked_in [SA, Admin, OTA] [audit, avail] │ │
│ │ → cancelled [SA, Admin, CS] [audit, avail] │ │
│ │ → no_show [SA, Admin] [audit] │ │
│ │ [+ Add Transition] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘- Transitions:
- Back → Settings page or sidebar navigation
- Add/edit transition → modal with form → save → refresh diagram
- Switch role in UI Visibility → update checkboxes → save
S-21: Suppliers List
- Phase: P2 (base), P2 (layout standardization — FR-43)
- Layout: Standard list page layout (§3.5)
- CJX Stage: Usage
- Related FR: FR-31, FR-33, FR-43
- Permission: Requires Suppliers VIEW
Elements:
- Header bar: "Suppliers List" title + search input + [Export CSV] [Import CSV] [+ Add Supplier] buttons
- Filter bar: Status filter (All/Active/Inactive) with FilterChip + Clear All
- Table (ResizableHeader columns): Code, Name, Country (CountryPill), Phone, Email, Total Rooms
- Sticky uppercase table headers, compact row padding
- Full pagination footer with rows selector + page numbers
- Country scope auto-applied from top bar selector
- Row click →
/suppliers/{id}(S-22)
Layout:
┌─────────────────────────────────────────────────────────┐
│ Suppliers List │ [Search...] │ [Export][Import][+ Add] │ ← h-14 header
├─────────────────────────────────────────────────────────┤
│ Active Filters: [Status: Active ✕] [Status ▾] [Clear]│ ← filter bar
├──────┬──────────┬─────┬───────┬─────────┬───────────────┤
│ CODE │ NAME │ CC │ PHONE │ EMAIL │ TOTAL ROOMS │ ← sticky thead
├──────┼──────────┼─────┼───────┼─────────┼───────────────┤
│ ID10 │ Narendra │ [ID]│ ... │ ... │ 14 │
│ VN05 │ Minh │ [VN]│ ... │ ... │ 8 │
├──────┴──────────┴─────┴───────┴─────────┴───────────────┤
│ Rows [20▾] │ Showing 1-20 of 45 │ [<][1][2][3][>] │ ← full pagination
└─────────────────────────────────────────────────────────┘S-22: Supplier Detail
- Phase: P2
- Layout: Two-column info + allocations table
- CJX Stage: Usage
- Related FR: FR-31, FR-33
- Permission: Requires Suppliers VIEW; EDIT for modifications
Info Section:
- Two cards: Basic Info (code, name, country, phone, email, Airbnb URL, notes) + Bank Details (bankName, accountNo, accountName)
- Edit via modal (SupplierForm)
Room Allocations Section (replaces "Linked Properties"):
- Table grouped by property: Property Name | Room Type | Rooms Allocated
- Grouped rows: property name spans multiple room type rows
- Click property name →
/properties/{id} - Shows total rooms across all properties at bottom
Layout:
┌─────────────────────────┬─────────────────────────────┐
│ Basic Info │ Bank Details │
│ Code: ID10 │ Bank: BCA │
│ Name: Narendra │ Account: 874xxxxx │
│ Country: [ID] │ Name: Patricia V. │
│ Phone: +62... │ │
│ Airbnb: [link] │ │
│ [Edit]│ [Edit]│
├─────────────────────────┴─────────────────────────────┤
│ Room Allocations │
├──────────────┬────────────┬────────────────────────────┤
│ Property │ Room Type │ Rooms Allocated │
├──────────────┼────────────┼────────────────────────────┤
│ Beach Villa │ Deluxe │ 4 │
│ │ Standard │ 6 │
│ City Hotel │ Suite │ 2 │
├──────────────┴────────────┼────────────────────────────┤
│ Total │ 12 │
└───────────────────────────┴────────────────────────────┘S-23: Supplier Allocation Manager ✅ IMPLEMENTED
- Phase: P2
- Implementation Status: Complete. Component:
components/properties/supplier-allocation-manager.tsx. Triggered from room type table Users icon. - Layout: Modal dialog on room type table row
- CJX Stage: Usage
- Related FR: FR-31, FR-32
- Permission: Requires Suppliers CREATE/EDIT/DELETE
Trigger: Click pencil icon in "Suppliers" column of room type table (S-04 Room Types Tab)
Elements:
- Modal title: "Manage Suppliers — {RoomTypeName}"
- Header: "Total Rooms: {totalRooms} | Allocated: {sumAllocated}" with warning badge if mismatch
- Allocation list: Supplier Name | Room Count | Notes | Actions (edit/deactivate)
- Add form: Supplier dropdown (active suppliers) + Room Count input + Notes (optional)
- Submit: POST
/room-types/{id}/supplier-allocations - Edit inline: PUT
/supplier-allocations/{id} - Deactivate: soft delete → isActive=false
Layout:
┌─────────────────────────────────────────────────────────┐
│ Manage Suppliers — Deluxe Room [✕] │
├─────────────────────────────────────────────────────────┤
│ Total Rooms: 10 │ Allocated: 8 │ ⚠ 2 unallocated │
├─────────────────────────────────────────────────────────┤
│ Supplier │ Rooms │ Notes │ Actions │
│ Narendra │ 4 │ contract Q1 │ [✏] [🗑] │
│ Minh │ 4 │ │ [✏] [🗑] │
├─────────────────────────────────────────────────────────┤
│ [Supplier ▼] │ [___] │ [notes...] │ [+ Add] │
└─────────────────────────────────────────────────────────┘Transitions:
- Close → return to room type table, SWR revalidate
- On add/edit/delete → revalidate room-types and allocations SWR keys
S-24: Master Data
- Phase: P2
- Route:
/master-data - Layout: Tabbed data management page
- CJX Stage: Retention
- Permission: Requires Settings VIEW; individual tabs gated by Users VIEW / Users EDIT
- Related FR: FR-20, FR-23, FR-42
- Access: Sidebar navigation item
Users Tab (requires Users VIEW):
- User list: Name, Email, Role (from roles table), Country (pill badge), Status (active/inactive), Actions
- "Invite User" / "Add User" button
- Role selector: dropdown populated from active roles
- Assign country, deactivate user
- Country field: dropdown (VN/ID/MY) for country-scoped roles, empty/disabled for full-access roles
- Admin actions: Reset Password (temp), Send Reset Link
Roles Tab (requires Users EDIT):
- Role list table: Label, Key, Description, System badge, Actions
- System preset roles show "System" badge, Delete disabled
- "Create Role" button → S-19 Role Management form
- Click role row → S-19 Role Management (edit mode)
- Actions: Edit → S-19, Delete (custom roles only)
Countries Tab (all Settings viewers):
- Countries table: Code, Name, Timezone, Currency, Pill Preview, Sort Order, Active, Actions
- Edit color/sort via inline form or modal
- Inactive countries hidden from country selectors
Booking Statuses Tab (all Settings viewers):
- Booking status list: Color dot, Label, Key, Sort Order, Default/Terminal flags, Actions
- "Add Status" button → create form
- Edit: modify label, color, icon, sort order
- Soft delete: sets isDeleted=true (cannot delete statuses with active bookings)
- Restore: re-activates deleted status
OTA Statuses Tab (all Settings viewers, FR-42):
- OTA status mapping list: Color dot, OTA Type (badge), Raw Status, Normalized Label, Sort Order, Actions
- "Add Mapping" button → create form
- OTA Type filter dropdown at top of tab (All / Booking.com / Agoda / Traveloka / Expedia)
- Create form fields: OTA Type (dropdown), Raw Status (text), Normalized Label (text), Color (picker)
- Edit: modify normalized label, color, sort order (raw status + OTA type immutable after creation)
- Soft delete / Restore (same pattern as Booking Statuses)
- Empty state: "No OTA status mappings yet. Unmapped statuses will display as raw strings."
OTA Statuses Tab Layout:
┌─────────────────────────────────────────────────────────┐
│ OTA Type: [All ▾] [+ Add Mapping] │
├──────────┬──────────────────┬──────────────┬──────┬─────┤
│ OTA │ Raw Status │ Display Label│ Color│ Act │
├──────────┼──────────────────┼──────────────┼──────┼─────┤
│[BDC] │ confirmed │ Confirmed │ 🟢 │[✏][🗑]│
│[BDC] │ cancelled_by_gst │ Cancelled │ 🔴 │[✏][🗑]│
│[AGD] │ no_show │ No Show │ ⚫ │[✏][🗑]│
└──────────┴──────────────────┴──────────────┴──────┴─────┘Layout:
┌─────────────────────────────────────────────────────────┐
│ Master Data │
├─────────────────────────────────────────────────────────┤
│ [Users] [Roles] [Countries] [Booking Statuses] [OTA St]│
├─────────────────────────────────────────────────────────┤
│ (Tab content below) │
└─────────────────────────────────────────────────────────┘S-25: Logs
- Phase: P2
- Route:
/logs - Layout: Tabbed page, two tabs
- CJX Stage: Retention
- Permission: Super Admin only (role check on page, no permission bitmask)
- Related FR: FR-35, FR-27
Tab 1 — Activity Log:
- Same dark terminal-style panel as S-02 Activity Log Panel (see S-02 spec)
- Full-page variant: wider table, more visible columns, limit selector prominently placed
- Source:
GET /activity-logs?limit=N→activity_logsPostgreSQL table (FR-35) - Columns: Time, User (email), Method (badge), Path (truncated), Status, Screen
- Controls: Pause/Resume toggle, Limit selector (100/200/500), Auto-scroll toggle
Tab 2 — Audit Log:
- Source:
GET /activity-logs/audit?page=N&limit=50&entityType=X→audit_logstable (E-12) - Filters: Entity Type dropdown (Property, RoomType, OtaConnection, Booking, User, Role, Settings, etc.), Action dropdown (create/update/delete)
- Table: Timestamp, Action (badge colored by type), Entity Type, Entity ID (truncated), Performer
- Expandable rows → Diff Viewer showing old → new value changes in syntax-highlighted key-value table
- Pagination: 50 entries/page with prev/next
Action badge colors:
create→ green (--color-success)update→ blue (--color-accent)delete→ red (--color-danger)
Layout:
┌─────────────────────────────────────────────────────────┐
│ Logs │
├─────────────────────────────────────────────────────────┤
│ [Activity Log] [Audit Log] │
├─────────────────────────────────────────────────────────┤
│ Activity Log Tab: │
│ ● LIVE [Pause] Limit: [100 ▾] │
│ ┌────────────┬─────────────┬────────┬──────┬────┬──────┐│
│ │ Time │ User │ Method │ Path │ St │ Scrn ││
│ ├────────────┼─────────────┼────────┼──────┼────┼──────┤│
│ │ 15:32:01 │ admin@ptx │ [GET] │ /bkn │ 200│ Book ││
│ │ 15:31:58 │ staff@ptx │ [POST] │ /bkn │ 201│ Book ││
│ └────────────┴─────────────┴────────┴──────┴────┴──────┘│
│ │
│ Audit Log Tab: │
│ [Entity Type ▾] [Action ▾] │
│ ┌────────────┬────────┬──────────┬───────┬─────────────┐│
│ │ Time │ Action │ Entity │ ID │ Performer ││
│ ├────────────┼────────┼──────────┼───────┼─────────────┤│
│ │ 15:30:00 │[update]│ Booking │ a1b2… │ admin@ptx ││
│ │ ↳ [expanded] old: status=confirmed → new: status=checked_in ││
│ └────────────┴────────┴──────────┴───────┴─────────────┘│
│ [< Prev] Page 1 of 12 [Next >] │
└─────────────────────────────────────────────────────────┘- Transitions:
- Sidebar "Logs" link → this page
- No outbound navigation
S-26: Customers List
- Phase: P2 (base), P2 (layout standardization — FR-43)
- Layout: Standard list page layout (§3.5)
- CJX Stage: Usage
- Related FR: FR-36, FR-37, FR-43
- Permission: Requires Customers VIEW; CREATE for add button
Elements:
- Header bar: "Customers List" title + search input (name, email, phone, debounced 300ms) + [+ New Customer] button
- Filter bar: Status filter (All/Active/Inactive) with FilterChip + Clear All
- Table: Name, Email, Phone, Nationality (CountryPill), Bookings (count), Last Stay (date), Actions (delete)
- Sticky uppercase table headers, compact row padding (px-3 py-1.5)
- Full pagination footer with rows selector + page numbers
- Row click →
/customers/{id}(S-27)
Layout:
┌─────────────────────────────────────────────────────────┐
│ Customers List │ [Search...] │ [+ New Customer] │ ← h-14 header
├─────────────────────────────────────────────────────────┤
│ Active Filters: [Status: Active ✕] [Status ▾] [Clear] │ ← filter bar
├──────────┬─────────────┬──────────┬────┬─────┬──────────┤
│ NAME │ EMAIL │ PHONE │ CC │ BKGS│ LAST STAY│ ← sticky thead
├──────────┼─────────────┼──────────┼────┼─────┼──────────┤
│ John Doe │ john@x.com │ +84... │[VN]│ 12 │ 25 Feb │
│ Jane Smi │ — │ +62... │[ID]│ 3 │ 18 Jan │
├──────────┴─────────────┴──────────┴────┴─────┴──────────┤
│ Rows [20▾] │ Showing 1-20 of 45 │ [<][1][2][3][>] │ ← full pagination
└─────────────────────────────────────────────────────────┘- Transitions:
- Row click → S-27 Customer Detail
- "New Customer" → Modal with CustomerForm
- Sidebar "Customers" → this page
S-27: Customer Detail
- Phase: P2
- Layout: Header + two-column info cards + booking history table
- CJX Stage: Usage
- Related FR: FR-36, FR-37, FR-38
- Permission: Requires Customers VIEW; EDIT for modifications; DELETE for merge
Header:
- Back button →
/customers - Customer name (h1) + active/inactive status badge
- Actions: [Edit] [Merge with...] (permission-gated)
Info Section (two-column grid):
- Contact Info card: Name, Email, Phone, Nationality (CountryPill)
- Notes card: Free-text notes (editable)
- Timestamps card: Created, Updated
Booking History Section:
- Table: Guest Name (OTA source), Property, Room Type, OTA (badge), Check-in, Check-out, Status
- Row click →
/bookings/{id}(S-09) - "Link Booking" button opens search modal
- Each row has [Unlink] action (permission-gated)
Link Booking Modal:
- Search by guest name or OTA booking ID
- Auto-suggestions: unlinked bookings matching customer name/email (from
GET /customers/suggestions) - Checkbox multi-select + "Link Selected" button
- On success: mutate SWR key, close modal
Merge Modal (via "Merge with..." button):
- Search other customer by name/email
- Results: Name, Email, Booking Count
- Select → Confirmation dialog: "Move all bookings from [source] to [target]. Source profile will be deleted. Continue?"
- On confirm:
POST /customers/:id/merge, redirect to merged profile
Layout:
┌─────────────────────────────────────────────────────────┐
│ [← Back] John Doe [● Active] [Edit] [Merge with…]│
├─────────────────────────┬───────────────────────────────┤
│ Contact Information │ Notes │
│ Name: John Doe │ VIP guest, prefers high │
│ Email: john@example.com │ floor. Always books Deluxe. │
│ Phone: +84 123 456 789 │ │
│ Nationality: [VN] │ │
├─────────────────────────┴───────────────────────────────┤
│ Booking History (12) [+ Link Booking] │
├──────────┬──────────┬─────┬────────┬────────┬───────────┤
│ Guest │ Property │ OTA │ Check-in│ Check-out│ Status │
├──────────┼──────────┼─────┼────────┼────────┼───────────┤
│ John Doe │ Beach V. │[BDC]│ Feb 20 │ Feb 23 │[confirmed]│
│ J. Doe │ City H. │[AGD]│ Jan 5 │ Jan 8 │[checked_out]│
│ │ │ │ │ │ [Unlink] │
└──────────┴──────────┴─────┴────────┴────────┴───────────┘- Transitions:
- Row click → S-09 Booking Detail
- "Edit" → Modal with CustomerForm (pre-filled)
- "Merge with..." → Merge modal
- "Link Booking" → Link booking modal
- Back → S-26 Customers List
S-28: Process Management ✅ IMPLEMENTED
- Phase: P2
- Route:
/settings/processes - Layout: Tabbed — process type selector + workflow config (extends S-20 pattern)
- CJX Stage: Retention
- Related FR: FR-44, FR-45, FR-47
- Permission: Requires Settings EDIT (admin)
Elements:
- Process Type selector tabs: Booking | OTA Sync | Alert Resolution | Customer Mgmt | Onboarding | Setup
- Status list + Mermaid diagram filtered per process type (same as S-20, now with processTypeKey support)
- Full transition CRUD + hooks + uiConfig per process type
- Process type metadata card: key, label, description, entity_class (read-only for seeded types)
- API endpoints:
/api/v1/process-types,/api/v1/process-status,/api/v1/process-transitions
Layout:
┌─────────────────────────────────────────────────────────┐
│ Process Management │
├─────────────────────────────────────────────────────────┤
│ [Booking] [OTA Sync] [Alert] [Customer] [Onboard] [Setup] │
├─────────────────────┬───────────────────────────────────┤
│ Status List │ Mermaid Diagram │
│ (editable for │ (stateDiagram for state machines, │
│ state machines) │ flowchart for pipelines) │
├─────────────────────┴───────────────────────────────────┤
│ Transitions / Process Steps Detail Panel │
└─────────────────────────────────────────────────────────┘- Transitions:
- Sidebar "Processes" → this page (admin only)
- Tab switch → load process type config
- Same CRUD patterns as S-20 for state machine processes
S-29: Process Instances ✅ IMPLEMENTED
- Phase: P2
- Route:
/process-instances - Layout: Standard list page layout (§3.5)
- CJX Stage: Usage
- Related FR: FR-46
- Permission: Requires Settings VIEW
Elements:
- Header bar: "Process Instances" title + search input
- Filter bar: Process Type (?processType=), Status (Active/Completed via ?isActive=), Date range
- Table: Process Type (badge), Entity (link to detail), Current Status (badge), Started By, Started At, Duration (computed from startedAt/completedAt)
- Row click → entity detail page (S-09 for bookings, S-04 for properties, S-27 for customers, etc.)
- Pagination: page, limit query params
- No create button — instances created automatically by system via API
- API endpoint: GET
/api/v1/process-instanceswith filters
Layout:
┌─────────────────────────────────────────────────────────┐
│ Process Instances │ [Search...] │
├─────────────────────────────────────────────────────────┤
│ Type: [All ▾] Status: [Active ▾] Date: [range] │
├──────────┬──────────┬────────┬─────────┬────────┬──────┤
│ TYPE │ ENTITY │ STATUS │ STARTED │ BY │ DUR │
├──────────┼──────────┼────────┼─────────┼────────┼──────┤
│[Booking] │ BC-12345 │[conf.] │ 10:30 │ system │ 2d │
│[Onboard] │ Agoda-VN │[step3] │ 09:00 │ admin │ 1h │
├──────────┴──────────┴────────┴─────────┴────────┴──────┤
│ Rows [20▾] │ Showing 1-20 of 85 │ [<][1][2][3][>] │
└─────────────────────────────────────────────────────────┘- Transitions:
- Sidebar "Process Log" → this page
- Row click → entity detail page
5. Screen Flow
[S-01 Login]
│
▼
[S-02 Dashboard] ◄────────────────────────────────────────┐
│ │
├──→ [S-03 Properties List] │
│ │ │
│ └──→ [S-04 Property Detail] │
│ │
├──→ [S-05 OTA Accounts List] │
│ │ │
│ ├──→ [S-06 Connect OTA Account] │
│ │ │ │
│ │ └──→ [S-07 Import Properties] │
│ │ │ │
│ │ └── (success) ────────────►│
│ │ │
│ └──→ [S-07 Import Properties] (from [Import]) │
│ │
├──→ [S-08 Bookings List] │
│ │ │
│ └──→ [S-09 Booking Detail] │
│ │ │
│ └── (status change) → PATCH API │
│ │
├──→ [S-10 Availability Calendar] │
│ │ │
│ └── (block/unblock) → sync job triggered │
│ │
├──→ [S-11 Rate Manager] │
│ │ │
│ └── (push rates) → sync job triggered │
│ │
├──→ [S-12 Booking Timeline] │
│ │ │
│ └──→ [S-09 Booking Detail] │
│ │
├──→ [S-13 Rate Parity Report] │
│ │
├──→ [S-14 Analytics Dashboard] │
│ │
├──→ [S-15 Rate Rules Config] │
│ │
├──→ [S-21 Suppliers List] │
│ │ │
│ └──→ [S-22 Supplier Detail] │
│ │
├──→ [S-26 Customers List] │
│ │ │
│ └──→ [S-27 Customer Detail] │
│ │ │
│ └──→ [S-09 Booking Detail] │
│ │
├──→ [S-04 Property Detail] │
│ └──→ Room Types Tab │
│ └──→ [S-23 Supplier Allocation Manager] │
│ │
├──→ [S-16 Settings] │
│ │ (Workflow, Preferences, Notifications, System)│
│ │ │
│ └──→ [S-19 Role Management] (from S-24 Roles tab)│
│ │
├──→ [S-28 Process Management] (/settings/processes) │
│ │ (Process type selector + workflow config) │
│ └── extends S-20 pattern per process type │
│ │
├──→ [S-29 Process Instances] (/process-instances) │
│ │ │
│ └──→ entity detail pages (S-09, S-04, etc.) │
│ │
├──→ [S-24 Master Data] (/master-data) │
│ │ (Users, Roles, Countries, Booking Statuses) │
│ │ │
│ └──→ [S-19 Role Management] (Roles tab) │
│ │
├──→ [S-17 Sync Job Log] │
│ │ │
│ └── breadcrumb back ──────────────────────────►│
│ │
├──→ [S-25 Logs] (/logs, super_admin only) │
│ │ (Activity Log tab + Audit Log tab) │
│ └── breadcrumb back ──────────────────────────►│
│ │
└──→ [S-18 User Profile] (via TopBar avatar menu) │
│ │
└── back ────────────────────────────────────┘Primary Onboarding Flow:
[S-01 Login] → [S-02 Dashboard] → [S-05 OTA Accounts]
→ [S-06 Connect OTA Account] → [S-07 Import Properties]
→ [S-05 OTA Accounts] (repeat for each OTA)
→ [S-02 Dashboard] (ready for daily use)6. Design Rationale
6.1 Why Professional Blue
- Trust & reliability: Blue is the industry standard for operations tools. Staff needs to trust sync status at a glance
- Eye comfort: Staff use this 8+ hours daily. Blue palette with light content area reduces fatigue
- OTA brand contrast: OTA badges (Booking navy, Agoda purple, Traveloka blue, Expedia yellow) pop against neutral content area
- Status visibility: Green/yellow/red status indicators are immediately visible against white cards and blue sidebar
6.2 Why Fixed Sidebar
- Navigation is always visible — staff switches between screens frequently
- Bottom OTA account status panel gives constant awareness without navigating
- Sidebar collapses on smaller screens to preserve content space
6.3 Why Data Tables Over Cards
- Staff manages 100+ properties x 4 OTAs = 400+ data points. Cards waste vertical space
- Tables allow sorting, filtering, batch actions — essential for operational efficiency
- Card layout only used for KPI metrics, OTA account cards, and property import selection
6.4 Why Unified List Page Layout (FR-43)
- All list pages (S-03, S-08, S-21, S-26) follow §3.5 standard layout — reduces cognitive load when switching modules
- Fixed header bar keeps title + search + actions always visible without scrolling
- Filter chips provide clear visibility of active filters with one-click removal
- Sticky table headers maintain column context when scrolling long lists (100+ rows)
- Full pagination with page numbers allows direct page jumps for large datasets
- Bookings page is the reference implementation; other pages match its structure
6.5 Why Country Tabs in Top Bar
- The #1 organizational dimension for 100+ properties is country
- Persistent in top bar = always visible, instant switching
- Staff users auto-locked to their country = no clutter
- Manager can view "All" or filter = flexibility without complexity
6.6 Why OTA-First Onboarding
- Matches real-world workflow: OTA accounts already exist → discover properties from them
- Reduces manual data entry: property names, room types, rates auto-imported
- Cross-OTA matching prevents duplicate properties when connecting 2nd OTA
- Step wizard (S-06 → S-07) guides user through the flow naturally
6.7 Why OTA Account Status in Sidebar (Not Per-Connection)
- At 100+ properties, showing per-connection status is too noisy
- OTA accounts are the real health indicator: if the account session is dead, all its properties are affected
- Green/yellow/red dots per OTA account are scannable in <1 second
6.8 Why Separate Profile Page from Settings
- Settings (S-16) is manager-gated and contains system-wide configuration
- Profile (S-18) is self-service for all users — staff should not need manager permissions to update their own name or password
- Profile accessed via TopBar avatar menu — natural, discoverable location
- Single-column narrow form reduces cognitive load for personal info editing
7. Interaction Patterns
7.1 Sync Job Feedback
When user triggers a manual sync or availability update:
- Button changes to loading spinner + "Syncing..."
- Toast notification: "Pushing availability to Booking.com, Agoda..."
- On completion: Toast updates to "Availability synced to 3/4 OTAs" (green) or "Sync failed for Expedia" (red with retry link)
7.2 Session Expiry Flow
- Sidebar OTA account dot turns yellow → tooltip "Session expiring in 30 min"
- Dot turns red → tooltip "Session expired"
- Alert created (E-11) → notification sent (LINE/email)
- Staff clicks dot or alert → navigates to S-05 → [Refresh] or [Manage] to re-authenticate
- Session restored → dot turns green
7.3 Overbooking Alert Flow
- Booking pull detects
booked_rooms > total_rooms - Alert banner appears at top of S-02 Dashboard (red, persistent)
- LINE Notify + email sent immediately
- Alert shows: property, room type, date, which OTAs contributed
- Staff clicks [Resolve] → opens property detail with options
- Staff marks alert as resolved with notes
7.4 OTA-First Onboarding Flow
- User navigates to S-05 OTA Accounts → clicks "Connect OTA Account"
- S-06: Selects OTA, enters credentials, chooses 2FA method
- System tests connection via Playwright → success/fail feedback
- On success → S-07: System auto-discovers properties from OTA
- User selects properties to import, reviews cross-OTA matches (if 2nd+ OTA)
- System creates: Property + RoomTypes + OtaConnection + OtaRoomMappings
- Redirect to S-05 with success summary
- Repeat for each OTA account
7.5 Country Switching
- Manager clicks country dropdown in top bar, selects country (VN/ID/MY/All)
- All dashboard data refreshes for selected country
- Selection persisted in localStorage
- Staff user: dropdown locked to their assigned country (other options disabled)
7.6 TopBar User Menu
- User clicks avatar/initials circle in top bar right area
- Dropdown shows: user name, email, role badge, divider, "Profile" link, "Logout" button
- "Profile" → navigates to S-18
- "Logout" → clears JWT, redirects to S-01
7.7 Profile Edit Flow
- User navigates to S-18 via TopBar user menu
- Profile form pre-populated with current user data from auth context
- User edits name, email, or locale → "Save Changes" button becomes enabled
- On save →
PATCH /users/me→ success toast → auth context updated in-place - Email change validates uniqueness on blur (debounced API call)
7.8 Password Change Flow
- User fills current password, new password, confirm password
- Client-side validation: min 8 chars, passwords match
- On submit →
POST /users/me/password - If current password wrong → 401 → error toast "Current password incorrect"
- If success → success toast, all password fields cleared
7.9 Auth Hydration on Refresh
- App mounts → checks for JWT in cookie/localStorage
- If JWT exists →
GET /users/me→ populate auth context (includes role permissions) - If 401 → clear JWT → redirect to S-01 Login
- If no JWT → redirect to S-01 Login
- All routes except S-01 are protected by auth context check
- Sidebar items filtered by
hasPermission(module, VIEW)from auth context
7.10 Role Management Flow
- SA/Admin navigates to S-16 Settings → Roles tab
- Views list of all roles (7 system presets + any custom roles)
- Click "Create Role" → S-19 with empty form
- Fill role info (name, label, description)
- Check permission matrix checkboxes for each module × action
- Save → role created with computed JSONB permissions
- Assign role to users in Users tab via role dropdown
7.11 Permission-Based UI Rendering
- JWT includes
permissions: { module: bitmask }from user's role - Auth context provides
hasPermission(module: string, action: number): boolean - Sidebar items: hidden when
!hasPermission(module, VIEW) - Action buttons (Create/Edit/Delete): hidden when user lacks corresponding permission
- API endpoints:
PermissionsGuardchecks@RequirePermission(module, action)decorator - Unauthorized API call returns 403 with
{ message: "Insufficient permissions" } - Frontend shows toast on 403: "You don't have permission to perform this action"
7.12 Permission Bit Calculation
Permission bits:
VIEW = 0b0001 = 1
CREATE = 0b0010 = 2
EDIT = 0b0100 = 4
DELETE = 0b1000 = 8
ALL = 0b1111 = 15
Check: (permissions[module] & action) !== 0
Set: permissions[module] |= action
Unset: permissions[module] &= ~action
Example: Role with View + Edit on bookings
permissions.bookings = VIEW | EDIT = 1 | 4 = 5 = 0b0101
hasPermission('bookings', VIEW) → (5 & 1) = 1 → true
hasPermission('bookings', CREATE) → (5 & 2) = 0 → false
hasPermission('bookings', EDIT) → (5 & 4) = 4 → true
hasPermission('bookings', DELETE) → (5 & 8) = 0 → false7.13 Booking Status Change Flow
- User opens S-09 Booking Detail
useBookingWorkflowhook fetches workflow config via SWR (cached)- Hook resolves available transitions for current status + user's roleName
- Transition buttons rendered in header (one per available transition)
- User clicks transition button → StatusTransitionDialog opens
- Dialog shows: "Change status from {current} to {target}?" + optional note field
- User clicks Confirm → PATCH
/bookings/:id/statuswith{ status: targetKey } - Server validates transition, checks role, updates status, executes hooks
- On success → SWR mutate refreshes booking data → StatusBadge updates
- On 400 (invalid transition) or 403 (role denied) → error toast
- Hook failures logged server-side, don't affect UI response
7.14 Workflow Config Flow
- SA/Admin navigates to S-20 Workflow Config (requires Settings EDIT)
- Left panel shows all booking statuses. Right panel shows Mermaid diagram
- Click status → bottom panel shows outgoing transitions + uiConfig
- Add transition: click "Add Transition" → select target status, roles, hooks → POST
/booking-status-transitions - Edit transition: click edit icon → update roles/hooks/active → PATCH
/booking-status-transitions/:id - Delete transition: click delete → confirm → DELETE
/booking-status-transitions/:id - Mermaid diagram auto-refreshes on any transition change
- UI Visibility: switch to UI Visibility tab → select role → toggle section/button/field checkboxes → PATCH
/booking-status/:key/ui-config - Changes take effect immediately for users viewing booking detail pages
7.15 Conditional Section Visibility
- Booking detail page loads workflow config (SWR cached)
- Resolves uiConfig for current booking status + user role:
uiConfig[roleName] || uiConfig['*'] - If uiConfig specifies
sectionsarray → only show listed sections - If uiConfig is null/missing → show all sections (graceful degradation)
- If uiConfig specifies
editableFields→ show edit icons on those fields - Inline editing: click edit icon → field becomes input → save on blur/enter → PATCH
/bookings/:id
8. Data Fetching Patterns (FR-24)
8.1 SWR Configuration
All GET requests use SWR with a global config wrapping the app:
// lib/swr-config.tsx
<SWRConfig value={{
fetcher: apiGet,
revalidateOnFocus: false, // no refetch on tab focus
dedupingInterval: 5000, // dedupe same-key requests within 5s
}}>8.2 Data Loading Pattern
Replace all useEffect + apiGet + useState patterns with useSWR:
// Before
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
apiGet('roles').then(setData).finally(() => setLoading(false));
}, []);
// After
const { data = [], isLoading } = useSWR('roles', apiGet);8.3 Cache Invalidation on Mutations
After any create/update/delete, call mutate(key) to revalidate:
await apiPost('roles', payload);
mutate('roles'); // triggers refetch for all components using 'roles' key8.4 Polling Intervals
| Screen | Endpoint | Interval | Pause on Hidden |
|---|---|---|---|
| S-02 Dashboard | dashboard/summary | 30s | Yes |
| S-02 Activity Log Panel | activity-logs?limit=100 | 5s | Yes (pausable by user) |
| S-17 Sync Jobs | sync-jobs | 10s | Yes |
| S-25 Activity Log tab | activity-logs?limit=N | 5s | Yes (pausable by user) |
SWR's refreshInterval with refreshWhenHidden: false handles both polling and tab-visibility pausing automatically.
8.5 Filter/Search Debounce
Search and filter inputs use 300ms debounce before updating the SWR key:
const [debouncedSearch] = useDebounce(search, 300);
const { data } = useSWR(`properties?search=${debouncedSearch}`, apiGet);8.6 Shared Data Deduplication
SWR automatically deduplicates requests with the same key. Components on the same page sharing a key (e.g., roles used by both UsersTab and RolesTab in S-16) trigger only one network request.
GATE 2: Requirements Validation
Before proceeding to /ipa:design:
[ ] Stakeholders reviewed SRD.md
[ ] Feature priorities (P1/P2/P3) confirmed
[ ] Screen list and flows approved
[ ] Design System (Professional Blue) accepted
[ ] OTA-first onboarding flow validated (OtaAccount entity, S-05/S-06/S-07)
[ ] Country scoping design approved (pill badges matching OTA style + dropdown selector)
[ ] Per-user format presets accepted (vi/id/ms/en locale)
[ ] Entity model accepted (E-04 OtaAccount replaces old credential model)
[ ] Scale target acknowledged (100+ properties, 400+ connections)
[ ] Out-of-scope items acknowledged (PMS deferred, TH removed)
[ ] User Profile (S-18) scope confirmed: profile edit, password change, theme toggle
[ ] Auth hydration approach accepted (GET /users/me on mount)
[ ] No avatar upload in v1 confirmed (initials-based)
[ ] Bitwise permission model accepted (JSONB
{module: bitmask}, 4 bits per module: V/C/E/D)[ ] 7 preset roles confirmed (SA, Admin, Manager, OTA, CS, FIN, PO)
[ ] Custom role creation by SA/Admin accepted
[ ] Permission matrix UI (S-19) design approved (checkbox grid per module × action)
[ ] JWT includes permissions (larger token, no DB lookup per request)
[ ] Migration plan accepted (existing
manager→managerrole,staff→otarole)[ ] SWR caching strategy accepted (FR-24: load once, mutate on change, pause polling when hidden)
[ ] Hybrid workflow model accepted (D-17: transition table + JSONB uiConfig)
[ ] Hooks on transitions accepted (D-18: audit_log, update_availability, send_notification)
[ ] Workflow Config page (S-20) design approved (status list + Mermaid + transitions + uiConfig)
[ ] Enhanced Booking Detail (S-09) approved (status buttons, conditional sections, inline editing)
[ ] BookingStatusTransition entity (E-15) accepted (UNIQUE(from,to), allowedRoles, hooks)
[ ] Guest-only editable fields confirmed (guestName, guestEmail, numGuests, numRooms)
[ ] Activity log DB migration accepted (D-22: flat-file →
activity_logsPostgres table, FR-35)[ ] Logs page (S-25) design approved (super-admin only, Activity Log + Audit Log tabs, DB-backed)
[ ] OTA status tracking accepted (D-26: view-only, raw + optional normalization, zero coupling with internal workflow)
[ ] OTA Status column in S-08 approved (FR-41: colored badge, raw fallback, independent filter)
[ ] OTA Status in S-09 Booking Info card approved (FR-41: OTA status + last updated timestamp)
[ ] OTA Statuses tab in S-24 Master Data approved (FR-42: CRUD for OtaStatusDef mappings)
[ ] OtaStatusDef entity (E-20) accepted (UNIQUE(rawStatus, otaType), soft delete)
[ ] BPM_SPEC.md process models reviewed (6 processes: P-01 to P-06 with Mermaid diagrams)
[ ] ISO 19510 concept mapping accepted (practical modeling, not strict XML compliance)
[ ] Generalized workflow engine approach accepted (D-27: extend current, not Camunda/bpmn-engine)
[x] Process Management screen (S-28) design approved (extends S-20 pattern with process type selector) — IMPLEMENTED Phase B
[x] Process Instances screen (S-29) approved (standard list page layout §3.5) — IMPLEMENTED Phase B
[x] ProcessType entity (E-21) accepted (6 seeded types, admin CRUD) — IMPLEMENTED Phase B
[x] ProcessInstance entity (E-22) accepted (polymorphic entity tracking) — IMPLEMENTED Phase B
Next: /ipa:design to generate HTML prototypes from this spec