Basic Design (UI Specification)
| Project | PTX Channel Manager (ptx-cm) |
| Version | 2.2.0 |
| Date | 2026-02-20 |
| 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 | S-02, S-08, S-09, S-10, S-18 |
| Retention | Efficiency gains: bulk rate updates, analytics, role management, workflow config | S-11, S-12, S-14, S-19, S-20 |
| 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 │ │
│ ───────── │ │
│ 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
- Header:
--color-bg-pagebackground,--text-h4font - Rows: White background, hover
--color-bg-page - Borders: Bottom border
--color-border - Pagination: Bottom right, page numbers + per-page selector
- Sorting: Clickable headers with arrow indicators
- Filtering: Dropdown filters above table
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 log file (default: 100 entries, user can increase via limit selector 1-500)
- 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
- Layout: Data table with action buttons
- CJX Stage: Onboarding / Usage
- Elements:
- "Add Property" button (secondary, top right) — manual creation fallback
- Property table columns: Name, Country (pill badge), Timezone, Currency, OTA Connections (dots), Sync Status, Actions (Edit/View)
- Filter: Country dropdown (auto-set for staff), Status dropdown
- Search: Property name search
- Transitions:
- Click property name → S-04 Property Detail
- Click "Add Property" → S-04 (create mode)
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
- Layout: Filterable data table
- CJX Stage: Usage
- Elements:
- Filters row: Property (dropdown), OTA (multi-select badges), Status (confirmed/cancelled), 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), Detected At
- Sort by any column
- Pagination (25/50/100 per page)
- Export CSV button
- 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
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)
- 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
useApiGet('booking-status/workflow') → { statuses, transitions } (SWR cached)
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].colorGraceful 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 [✎] │ │ 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: No route, no component, no page exists.
- Layout: Grid — rows: room types, columns: dates (14-day view default)
- CJX Stage: Usage
- Elements:
- Property selector (dropdown, top)
- Date range navigation: < [02/01-02/14] >
- Grid cells show:
available / total(e.g., "3/10") - Cell colors: Green (>50% available), Yellow (20-50%), Red (<20%), Black (0 available)
- Click cell → popover to block/unblock rooms
- Block action → triggers availability push to all OTAs
- Legend bar at bottom
S-11: Rate Manager ❌ NOT IMPLEMENTED
- Phase: P2
- Implementation Status: No route, no component, no backend API. Rate/RateRule DB models exist but unused.
- Layout: Form + preview table
- CJX Stage: Retention
- Elements:
- Property selector
- Room type selector
- Date range picker
- Base rate input
- Per-OTA adjustments: checkboxes + markup/discount percentage
- Preview table: Date | Base Rate | Booking.com | Agoda | Traveloka | Expedia (calculated rates)
- "Push Rates" button → confirmation modal → queues rate push jobs
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
- Layout: Data table with filters
- CJX Stage: Usage
- Related FR: FR-31, FR-33
- Permission: Requires Suppliers VIEW
Elements:
- Paginated table: Supplier Code, Name, Country (CountryPill), Phone, Email, Total Rooms (SUM of active allocations)
- Filters: search (debounced), status (active/inactive), country scope
- Actions: Export CSV, Import CSV (batch 50, add/clear modes), Add Supplier modal
- Row click →
/suppliers/{id}(S-22)
Layout:
┌─────────────────────────────────────────────────────────┐
│ Suppliers [Import] [Export] [+] │
├─────────────────────────────────────────────────────────┤
│ [Search...] [Status ▼] [Country: auto-scoped] │
├──────┬──────────┬─────┬───────┬─────────┬───────────────┤
│ Code │ Name │ CC │ Phone │ Email │ Total Rooms │
├──────┼──────────┼─────┼───────┼─────────┼───────────────┤
│ ID10 │ Narendra │ [ID]│ ... │ ... │ 14 │
│ VN05 │ Minh │ [VN]│ ... │ ... │ 8 │
└──────┴──────────┴─────┴───────┴─────────┴───────────────┘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
- Phase: P2
- 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
- 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
Layout:
┌─────────────────────────────────────────────────────────┐
│ Master Data │
├─────────────────────────────────────────────────────────┤
│ [Users] [Roles] [Countries] [Booking Statuses] │
├─────────────────────────────────────────────────────────┤
│ (Tab content below) │
└─────────────────────────────────────────────────────────┘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-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-24 Master Data] (/master-data) │
│ │ (Users, Roles, Countries, Booking Statuses) │
│ │ │
│ └──→ [S-19 Role Management] (Roles tab) │
│ │
├──→ [S-17 Sync Job Log] │
│ │ │
│ └── 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 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.5 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.6 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.7 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-17 Sync Jobs | sync-jobs | 10s | Yes |
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)
Next: /ipa:design to generate HTML prototypes from this spec