Mobile App — Feature Plan
Bối cảnh:
booking-mobile/là React Native + Expo app, chạy WatermelonDB offline-first, OpenAPI codegen cùng API contract với web. Phục vụ Owner + Staff trong 1 app duy nhất (role-based UI switch). Khách hàng dùng web customer portal (/b/:slug), KHÔNG nằm trong scope mobile.Đọc trước:
docs/mobile/admin-settings-inventory.md— inventory cài đặt admindocs/product/prd.md— user stories (US-2.4, US-4.4, US-4.5, US-5.2, US-7.3 là mobile-first)docs/flows/booking-flow.md— booking rulesdocs/progress/features.md— trạng thái backend hiện tại
0. Prerequisite — Role audit + harden (BLOCKER, làm TRƯỚC mobile)
⚠️ Mobile không build được trước khi fix điểm này. Hiện tại role-based permissions chưa được verify end-to-end trên web + API, nhiều endpoint chặn STAFF khiến flow mobile thiết yếu (walk-in, xem resources, đọc settings) sẽ gãy.
0.0.1 Hiện trạng (phát hiện khi viết plan này)
| Vấn đề | Chi tiết | Impact mobile |
|---|---|---|
| Web admin chặn STAFF | booking-web/src/app/admin/(dashboard)/layout.tsx: AuthGuard allowedRoles={["ADMIN","OWNER"]} — staff chưa login được vào /admin. Role-based UI chưa từng chạy E2E. |
Mobile không có web reference pattern để copy |
GET /services chặn STAFF |
service.controller.ts — chỉ OWNER, ADMIN |
Walk-in gãy — staff không chọn được service |
GET /resources chặn STAFF |
resource.controller.ts — chỉ OWNER, ADMIN |
Không list được staff khác để owner/staff gán lại booking |
GET /tenants/me chặn STAFF |
tenant.controller.ts — chỉ OWNER, ADMIN |
BLOCKER — staff không đọc được TenantSettings → không biết business hours, deposit, tz |
| Resource-scoping booking | booking.service.ts:710 có check isAdmin cho transition, KHÔNG thấy filter list theo resourceId = user.resource.id cho STAFF |
Staff có thể đang thấy mọi booking của salon — vi phạm privacy & UX |
| 0 test role boundary | roles.guard.spec.ts chỉ test decorator, không test từng endpoint 403 đúng chỗ. E2E không có scenario STAFF |
Không biết chỗ nào break |
0.0.2 Sprint "Role audit & harden" (1–1.5 tuần)
Deliverables:
- Ma trận quyền — file mới
docs/architecture/role-matrix.mdvới bảngendpoint × [ADMIN|OWNER|STAFF|CUSTOMER] × [read|create|update|delete]. Decide dứt khoát từng ô. - Mở quyền STAFF cho read-only endpoint cần thiết:
GET /services,GET /resources,GET /tenants/me,GET /loyalty/cards,GET /tenant-customers/:customerId. - Resource-scoping trong service layer:
BookingService.list()— nếurole=STAFF→ WHEREresourceId = user.resource.id OR resourceId IS NULL(cho self-pick).BookingService.update/updateStatus()— STAFF chỉ được sửa booking mình đang đảm nhận, trừ unassigned → self-pick.TenantCustomerService— staff sửa được notes/tags nhưng không sửa được metrics.
- Mở
/admincho STAFF trên web — đổiAuthGuard allowedRoles={["ADMIN","OWNER","STAFF"]}, sidebar filter theo role (staff không thấy Settings/Tax/Payment/Loyalty management), bookings page filter tự động theoresourceId. - E2E test role boundary (Playwright) — tối thiểu:
- STAFF không list được all resources/services khi endpoint chưa mở.
- STAFF đã mở quyền thì GET được nhưng không DELETE.
- STAFF không PATCH được booking của resource khác (403).
- STAFF self-pick unassigned booking khi có skill matching → OK.
- STAFF self-pick unassigned booking khi KHÔNG có skill → 403.
- OWNER override được conflict / double-booking.
- CUSTOMER token bị reject ở admin endpoints (
typeclaim check).
- Unit test role-scoping cho BookingService + TenantCustomerService (happy + 403 path).
Done criteria:
-
docs/architecture/role-matrix.mdmerged vào docs index. - Web
/adminlogin được bằng STAFF account, sidebar đúng, booking list đã filter. - 7+ E2E scenarios pass.
- 0 endpoint còn chặn STAFF nhầm (whitelist đã review toàn bộ controllers).
- OpenAPI spec regen,
booking-web+booking-mobiletypes sync.
Sau khi xong: mobile copy pattern role switch y hệt web (cùng permission logic, cùng sidebar filter), giảm rủi ro.
0. Nguyên tắc thiết kế
0.1 Role-based, single app
Đăng nhập → JWT trả về role (OWNER/STAFF/ADMIN). App swap navigation stack:
- OWNER → full access (dashboard, staff, services, settings, payments, reports).
- STAFF → schedule cá nhân, booking của mình, walk-in, self-pick, check-in, customer quick view. KHÔNG thấy settings, tax, payment config, không sửa được booking của staff khác (trừ khi owner cho).
- ADMIN (superadmin) → KHÔNG phục vụ trên mobile (memory đã note: admin cần "login as tenant" — dùng web).
0.2 Offline-first (critical cho salon thật)
Salon ở Na Uy nhiều chỗ wifi/signal kém, thao tác mid-appointment cần mượt. WatermelonDB sync cho entities dưới đây:
| Entity | Offline đọc | Offline ghi | Ghi chú |
|---|---|---|---|
Booking |
✅ | ✅ (queue) | Ghi offline → mark _status=created/updated, sync khi online. Conflict → server wins + toast. |
Resource |
✅ | ❌ | Owner sửa staff rất ít, cần online. |
Service |
✅ | ❌ | Như trên. |
Customer |
✅ | ✅ (create) | Walk-in tạo customer mới offline OK, merge khi sync. |
TenantCustomer (tags, notes, visitCount) |
✅ | ✅ (update notes/tags) | Conflict: merge last-write-wins cho notes, server-wins cho counters. |
TenantSettings snapshot |
✅ | ❌ | Read-only offline — chỉ thay qua desktop hoặc khi online. |
ResourceSchedule + ScheduleOverride |
✅ | ❌ | Tính availability offline nhưng cấm sửa. |
TimeOff |
✅ | ✅ (staff request) | Offline request, owner approve khi online. |
Payment |
❌ | ❌ | Tuyệt đối online (phải gọi provider). Hiển thị read-only từ cache last-synced, có badge "⚠ Ngoại tuyến — số liệu có thể cũ". |
LoyaltyStamp / LoyaltyPointTransaction |
✅ | ❌ | Đọc cached balance, ghi từ event booking COMPLETED online. |
0.3 Các ràng buộc bắt buộc
- KHÔNG fallback settings — nếu chưa sync TenantSettings, chặn tạo booking (screen "Đang sync dữ liệu salon, vui lòng đợi").
- Timezone — mọi so sánh giờ làm việc dùng salon timezone (từ
tenant.settings.timezone), KHÔNG device local. - Money — minor units (øre), format qua shared helper.
- Error codes — translate từ
error.code(domain prefix), KHÔNG showmessagethô. - Zero tolerance — lint + build + type sạch; React Compiler rules như web (memory có
feedback_react_compiler).
0.4 Tech stack xác nhận
| Layer | Lựa chọn | Lý do |
|---|---|---|
| Framework | Expo (React Native) | README định sẵn |
| State | React Query + Zustand | Đồng bộ với web |
| DB local | WatermelonDB | README định sẵn, offline-first |
| API client | OpenAPI codegen | Đã có ở web, port pipeline |
| Navigation | Expo Router (file-based) | Đồng nhất với Next.js pattern |
| Auth storage | expo-secure-store |
access + refresh token, never AsyncStorage |
| Push | expo-notifications |
PRD US-7.3 |
| i18n | expo-localization + i18next hoặc react-intl |
nb-NO primary, sync web translations |
| Map | react-native-maps |
Native + Apple/Google maps |
| Image | expo-image-picker + upload MinIO |
Branding + avatar + service |
| QR | expo-barcode-scanner |
Scan QR check-in |
| Date time | date-fns-tz hoặc luxon |
Phải dùng salon tz, tránh Date.getHours() |
1. Phân lớp MVP
Phase 1 — MVP (cho owner + staff xử lý vận hành hàng ngày)
Mục tiêu: Một salon có thể chạy full-day operation trên mobile mà không cần mở desktop (trừ setup ban đầu + payment config + tax).
Owner
- Đăng nhập, xem dashboard (metrics hôm nay + today timeline + upcoming bookings).
- Xem full calendar (day view) all staff, booking của tất cả.
- Tạo / sửa / cancel booking (multi-service, assign staff, walk-in, from-phone).
- Quản lý status (CONFIRMED → ARRIVED → IN_PROGRESS → COMPLETED / CANCELLED / NO_SHOW).
- Xem + sửa customer (profile, notes, tags, booking history, loyalty balance).
- Xem staff work schedule + approve time-off.
- Quick-add / quick-edit service (P0 — name + duration + price + category).
- Xem payment list (read-only) + capture/refund basic.
- Settings read-only + sửa được: booking policy (toggle walkIn/autoConfirm/bookingMode/cancellationHours), business hours, branding colors.
- Push notifications: new online booking, customer check-in, staff self-pick, payment success/failure.
Staff
- Đăng nhập → thấy schedule CÁ NHÂN ngày hôm nay.
- Xem booking chi tiết (customer, service, notes, giờ, status, payment status).
- Update status booking của chính mình (CONFIRMED → ARRIVED khi khách đến, → IN_PROGRESS khi bắt đầu, → COMPLETED khi xong).
- Scan QR của khách để mở booking (QR do customer portal gen ở
/b/:slug/bookings/:id). - Walk-in quick create (chọn service, auto-assign bản thân, start=now).
- Self-pick booking unassigned (nếu
bookingMode=allow_unassignedvà có skill phù hợp). - Customer quick view (profile, tags, notes, lịch sử booking, loyalty).
- Request time-off (owner approve qua notification).
- Profile cá nhân — đổi password, phone.
- Push notifications: booking mới assign cho mình, walk-in notify owner, schedule change.
Phase 2 — V1 (sau MVP chạy ổn định)
Owner
- Drag-drop reschedule trên calendar (long-press + drag).
- Week view calendar (scroll ngang).
- Staff management: CRUD resource, assign skills, upload avatar.
- Service management: full CRUD + category + accounting link + metadata + image.
- Branding upload logo / cover (camera + gallery).
- Multi-service booking (add item ngay trong drawer).
- Payment drawer: event log, capture/refund/void/collect remaining.
- Deposit flow (enable + type + value).
- Location edit (pin trên map + reverse geocode).
- TimeOff approve/reject với reason.
- Onboarding wizard đầu tiên qua mobile (cho user download app trước khi login web).
- Export booking/revenue report CSV (share qua email/drive).
Staff
- Self service: xin đổi lịch, xem lương/commission nếu metadata có.
- Multi-day schedule view (tuần tới).
- Chat với khách hàng (link về booking) — defer nếu chưa có API.
- Notifications settings — mute hours.
Phase 3 — V2+ (roadmap xa)
- Online payment via Vipps (Na Uy native — rất phổ biến).
- Tap-to-pay (smartphone-as-POS, Apple/Android Tap to Pay SDK).
- Waitlist (khách đăng ký khi full, staff gọi khi có slot).
- Review system post-booking.
- Marketing campaigns (SMS/push cho inactive customers).
- Calendar sync (iOS/Android native calendar, Google Calendar).
- Analytics dashboard (booking trends, staff utilization heatmap).
- Multi-location: swap location trong app (khi có Organization layer).
- Superadmin "login as tenant" — vẫn không nên đưa lên mobile.
2. Information architecture (IA)
2.1 Navigation — Owner
Tabs (bottom):
├── 🏠 Home (dashboard)
├── 📅 Calendar (day view, swap staff filter)
├── ➕ New Booking (FAB trung tâm, full-screen modal)
├── 👥 Customers
└── ⋯ More
├── Staff & Schedule
├── Services
├── Payments
├── Loyalty (view-only)
├── Settings
└── Profile / Logout
2.2 Navigation — Staff
Tabs (bottom):
├── 📅 My Schedule (today default, swipe cho ngày khác)
├── ➕ Walk-in (FAB trung tâm, quick-create)
├── 👥 Customers (read + quick edit notes/tags)
└── ⋯ More
├── Unassigned Queue (nếu bookingMode = allow_unassigned)
├── Scan QR
├── Time-off Request
└── Profile / Logout
2.3 Screen inventory (ước lượng)
| Group | Screens | Ghi chú |
|---|---|---|
| Auth | Signin, Forgot Password, Reset Password | Owner chỉ dùng email, staff có thể phone |
| Onboarding (mobile) | Welcome, Salon info, Business hours, Services, Staff, Policy, Review | V1 — MVP force web |
| Dashboard (owner) | Home | Metrics + Today timeline + Upcoming + Quick actions |
| Calendar | Day view, Week view (V1), Booking drawer, ServicePicker, Conflict modal | Owner view-all, staff view-self |
| Booking flow | Create booking (multi-step or single screen?), Edit, Status change, Cancel confirm, History audit | Single-screen stacked cho mobile |
| Customers | List, Detail (profile + history + loyalty + notes), Create, Edit | Tabs: Info / Bookings / Loyalty / Notes |
| Services | List, Create, Edit, Category manage | Quick-add inline |
| Staff (owner) | List, Detail (profile + skills + schedule), Create, Edit, Skill picker | Industry label dynamic |
| Work schedule (owner) | Grid per-staff, Weekly recurring editor, Override editor, TimeOff approval | Native time picker |
| TimeOff (staff) | Request create, My requests, Status view | Notif khi approve/reject |
| Settings | Overview (sections), Booking policy, Business hours, Branding, Location, General (read-only most) | Tab list → stack detail |
| Payments | List, Detail drawer, Capture/Refund/Void action sheets | Native action sheet |
| Payment config | Provider list, Provider detail (read + activate + health-check) | NO credentials editing |
| Loyalty | Customer balance (read-only) | Card creation → web |
| Profile | Email, phone, password | Common |
| Notifications | Feed + mark read + tap deep link | In-app feed + push |
| Scan QR | Full-screen scanner → open booking | Staff flow |
3. Đặc tả chi tiết một số flow mobile-first
3.1 Staff check-in khi khách đến (P0)
[Home / Calendar]
└─ tap booking → Booking Detail
├─ Status badge: CONFIRMED
├─ Button "Khách đến" (primary)
│ └─ Tap → PATCH /bookings/:id/status/ARRIVED → success toast
│ └─ Refetch booking, animate status badge
└─ Button "Bắt đầu" (sau ARRIVED)
└─ PATCH → IN_PROGRESS
Biến thể: Scan QR khách đưa điện thoại → mở trực tiếp Booking Detail → nút "Khách đến" ngay tay.
3.2 Walk-in (P0)
[Walk-in FAB]
├─ Chọn service (grid lớn — tap size ≥ 48×48)
├─ (nếu owner) chọn staff, (nếu staff) tự gán mình
├─ (optional) nhập tên khách + phone — hoặc skip
└─ Tap "Bắt đầu ngay"
└─ POST /bookings { source: WALK_IN, startTime: now, status: IN_PROGRESS }
└─ Toast + push notif owner
Yêu cầu offline: lưu vào WatermelonDB, queue sync, UI hiện ngay trên calendar với badge "⋯ đang đồng bộ".
3.3 Self-pick unassigned (P0 — theo US-4.5)
[Tab More → Unassigned Queue]
├─ List bookings resourceId=null, filter theo skill của staff
├─ Mỗi card: service + time + customer + duration
└─ Tap "Nhận" (swipe-right action)
└─ PATCH /bookings/:id { resourceId: myResourceId }
├─ Nếu 409 (ai đó vừa nhận) → toast "Người khác đã nhận rồi"
└─ Nếu 200 → remove khỏi list + add vào My Schedule
3.4 Owner approve TimeOff (P0)
Push notif "Staff A xin nghỉ 20–22/5" (tap) →
[TimeOff detail] →
├─ Hiển thị: staff, range, reason, impact (bookings bị ảnh hưởng)
└─ 2 button: Approve / Reject + note
└─ PATCH /time-offs/:id { isApproved: true/false }
└─ Trigger re-check bookings trong range → hiện danh sách nếu có conflict
3.5 Owner sửa business hours nhanh (P0)
[More → Settings → Business hours]
├─ 7 rows (thứ 2 → chủ nhật), mỗi row: toggle mở/đóng + slots list
├─ Tap slot → native time picker (start/end)
├─ "+ Thêm slot" cho break giữa ngày
└─ Save → PATCH /tenants/:id { settings: { businessHours } }
└─ Hỏi: "Áp dụng cho lịch staff?" (applyToStaff boolean)
3.6 Dashboard metrics (P0)
Widgets:
- Today bookings count (tap → filter calendar hôm nay).
- Revenue today (tổng
paidAmountcủa booking COMPLETED). - Customers new today.
- Pending bookings (cần confirm thủ công nếu autoConfirm=false).
- No-show rate 7 ngày.
4. Notifications (push)
4.1 Registration
- App khởi động → lấy Expo push token qua
expo-notifications. - POST
/api/auth/devicesvới{ token, platform, appVersion }. - Lưu localStorage + invalidate khi logout.
4.2 Events (source: domain events từ backend)
| Event | Audience | Deep link | Priority |
|---|---|---|---|
| Booking CREATED (source=ONLINE) | Owner + assigned staff | /bookings/:id |
P0 |
| Booking CREATED (unassigned) | All staff có skill | /unassigned |
P0 |
| Booking STATUS_CHANGE (ARRIVED) | Owner | /bookings/:id |
P0 |
| Booking RESCHEDULED (by customer) | Owner + staff | /bookings/:id |
P0 |
| Booking CANCELLED (by customer) | Owner + staff | /bookings/:id |
P0 |
| Walk-in created | Owner | /bookings/:id |
P0 |
| Staff self-pick | Owner | /bookings/:id |
P0 |
| TimeOff requested | Owner | /timeoff/:id |
P0 |
| TimeOff approved/rejected | Staff | /timeoff/:id |
P0 |
| Payment CAPTURED / REFUNDED / FAILED | Owner | /payments/:id |
P1 |
| Daily summary (end of day) | Owner | / |
P2 |
4.3 Backend changes cần thiết
DeviceTokenmodel (chưa có):{ userId, token, platform, lastSeenAt }.NotificationServicepublish Expo push qua BullMQ queue (infra đã có).- Wire vào domain events hiện có (BookingCreated, BookingConfirmed, BookingCancelled, PaymentCaptured, ...).
- Cron daily summary (end-of-day per tenant timezone).
5. Security & session
| Vấn đề | Giải pháp |
|---|---|
| Token storage | expo-secure-store (Keychain/Keystore) cho access + refresh |
| Biometric unlock | expo-local-authentication (FaceID/TouchID) — P1, optional |
| App background lock | Blur screen khi backgrounded (compliance salon có thể show khách hàng info) |
| Role switching | JWT trả role → guards navigation, KHÔNG trust client-side only, mọi API call vẫn được backend check |
| Tenant scope | Backend đã enforce tenantId từ JWT + header — mobile KHÔNG cần gửi tenant ID thủ công |
| Customer data privacy | Mask phone/email phần lớn ký tự trong list (chỉ full khi tap detail) |
| Logout | Revoke refresh (POST /auth/logout), clear SecureStore, clear WatermelonDB, force re-sync lần sau |
| Force logout (tokenVersion) | Mọi 401 với code TOKEN_VERSION_MISMATCH → clear + back to login |
| Offline auth | Access token hết hạn offline → chặn mutation, cho phép read cached; online lại → refresh flow |
6. Sync strategy (offline-first)
Nguyên tắc: dùng lại 100% REST API hiện có cho web admin. Mobile KHÔNG cần endpoints
/api/sync/*riêng. Chỉ bổ sung 2–3 thứ nhỏ (device token, idempotency key) để hỗ trợ retry & push.
6.1 Hướng tiếp cận — Hướng A (recommended cho MVP)
Read path — cache response REST hiện có:
- Login / mở app → gọi song song các list API (
GET /bookings?date=today,/resources,/services,/customers,/tenants/me) → lưu WatermelonDB. - Refetch định kỳ (30s foreground,
expo-background-fetch15 phút min khi background). - Offline: đọc trực tiếp từ WatermelonDB.
Write path — queue + idempotent retry:
- Mutation offline → lưu pending record trong WatermelonDB + gen
idempotencyKey(UUID v7 client-side). - Khi online → flush queue bằng chính REST endpoint gốc (
POST /bookings,PATCH /bookings/:id/status/:status, ...) kèm headerIdempotency-Key. - Retry với backoff (1s → 2s → 4s → 8s → 16s → drop + toast).
Tại sao đủ: 1 salon trung bình ~50–200 bookings/ngày → full list pull <200KB, mỗi request ~100ms. Overhead nhỏ, code backend phải viết cũng nhỏ. Incremental sync chỉ đáng đầu tư khi dataset >1000 records/ngày.
6.2 Bổ sung tối thiểu ở backend
| Item | Mô tả | Priority |
|---|---|---|
Idempotency-Key header |
Áp dụng cho POST /bookings, PATCH /bookings/:id, PATCH /bookings/:id/status/:status. Cache response 24h theo key. Payment context đã có pattern — port sang Booking. |
P0 |
POST /api/auth/devices + DELETE /api/auth/devices/:token |
Đăng ký / huỷ Expo push token, gắn với userId + role + tenantId. |
P0 |
(optional) ?updatedAfter=<iso> query param cho list endpoints |
Pull delta thay vì full list. Chỉ làm khi profiling cho thấy payload quá lớn. | P2 |
6.3 Conflict resolution (minimal)
Dùng updatedAt + If-Unmodified-Since hoặc ETag (API đã có updatedAt):
- Client gửi
If-Unmodified-Since: <cached_updatedAt>khi PATCH. - Server trả 409 nếu bản ghi đã bị sửa sau thời điểm đó.
- Client → toast "Dữ liệu đã thay đổi, refetch" → refetch + cho user thao tác lại.
Default rule: server always wins. Client không tự merge — refetch + show lại.
Ngoại lệ: Customer.notes, TenantCustomer.tags → client merge last-write-wins vì ít conflict thực tế.
6.4 Background sync
- Foreground: React Query
refetchInterval: 30_000cho danh sách chính. - Backgrounded:
expo-background-fetch15 phút (iOS min). - Kết nối lại sau offline:
NetInfolistener → flush pending queue + invalidate queries.
6.5 Sơ đồ dòng dữ liệu
[Mobile UI]
│ write
▼
[Mutation] ──online?── yes ─► [POST /api/bookings + Idempotency-Key]
│ │
│ no └─ 200 → update cache
▼ └─ 409 → refetch + retry
[WatermelonDB queue]
│
│ NetInfo → online
▼
[Flush loop] → same POST với cùng Idempotency-Key
7. Gate / chặn hành động
| Tình huống | Chặn | UX |
|---|---|---|
TenantSettings chưa sync |
❌ Không cho tạo booking | Full-screen loading "Đang đồng bộ dữ liệu salon" |
| Không có skill khi self-pick | ❌ Ẩn nút "Nhận" | Staff không thấy booking đó trong queue |
allowDoubleBooking=false + conflict detect |
⚠️ Cảnh báo, chỉ owner override | Modal confirm "Booking này trùng với X — vẫn tiếp tục?" |
autoConfirm=false + booking mới |
ℹ️ Status = PENDING | Badge "Chờ xác nhận" + CTA confirm |
depositEnabled=true nhưng không có payment config |
❌ Không cho enable trong settings | Warning icon + link sang payment config |
| Offline + action yêu cầu online (capture, refund, change password) | ❌ Disable button | Tooltip "Cần kết nối mạng" |
8. Test strategy
8.1 Test pyramid
- Unit: utility functions (format, tz conversion, sync conflict logic). Vitest hoặc Jest + React Native Testing Library.
- Integration: navigation flows + API mocks. MSW (mock service worker).
- E2E: Detox hoặc Maestro — smoke test critical flows (login → new booking → status change).
8.2 Offline tests
- Simulated airplane mode khi walk-in → sync → verify.
- Conflict simulation: 2 devices self-pick cùng lúc.
8.3 Coverage target
80%+ như web (memory: feedback_commit_checklist).
9. Milestone / timeline ước lượng
| Phase | Nội dung | Ước tính |
|---|---|---|
| M-1 Role audit (xem §0) | Ma trận quyền, mở quyền STAFF các read endpoint, resource-scoping service layer, mở /admin cho STAFF, E2E role boundary |
1–1.5 tuần (blocker) |
| M0 Setup | Expo app skeleton, codegen pipeline, auth login, SecureStore, i18n, theme sync branding | 3–5 ngày |
| M1 Infra | WatermelonDB models, sync endpoints (API side), push token register | 5–7 ngày |
| M2 Owner MVP | Dashboard, Calendar day view, Booking CRUD + status, Customers read, Services quick-edit, Settings P0 | 10–14 ngày |
| M3 Staff MVP | My Schedule, Walk-in, Status change, Self-pick, Scan QR, TimeOff request, Customer quick view | 7–10 ngày |
| M4 Notifications | Expo push wire-up, event bus → push listener, in-app feed | 3–5 ngày |
| M5 Polish + E2E | Offline flows, conflict test, error boundaries, accessibility, Detox/Maestro smoke, store build | 5–7 ngày |
Tổng MVP: ~7–9.5 tuần solo developer (role audit 1–1.5 tuần + mobile 6–8 tuần), song song với backend enhancements (device token, push worker, idempotency key).
10. Backend dependencies (cần chuẩn bị trước khi mobile build)
Tin tốt: 90% REST API hiện tại của web admin reuse được nguyên cho mobile (cùng OpenAPI, cùng auth, cùng tenant scope). Chỉ cần bổ sung những mục dưới đây.
| Task | Ai làm | Status | Priority |
|---|---|---|---|
Role audit & harden (xem §0) — ma trận quyền, mở quyền STAFF cho read endpoints, resource-scoping service layer, mở /admin cho STAFF, E2E role boundary |
Backend + Web | ❌ | P0 — BLOCKER |
DeviceToken model + POST/DELETE /api/auth/devices |
Backend | ❌ | P0 |
| Expo push notification service (BullMQ worker) | Backend | ❌ | P0 |
| Event bus → push dispatcher (listen 5–6 event types) | Backend | ❌ | P0 |
Idempotency-Key header cho POST /bookings + PATCH /bookings/:id + PATCH /bookings/:id/status/:status |
Backend | ❌ (payment đã có) | P0 |
| OpenAPI spec stability (no breaking change trong MVP sprint) | Backend | — | P0 |
| REST API hiện tại (bookings, resources, services, customers, tenant, settings, payments) | Backend | ✅ | — |
/api/auth/login trả role + tenantId |
Backend | ✅ | — |
updatedAfter=<iso> query param cho list endpoints (nice-to-have) |
Backend | ❌ | P2 |
| SMS notifications (US-7.1, 7.2) | Backend | ❌ | P1 (mobile không phụ thuộc) |
| Multi-location (Organization layer) | Backend | ❌ | P2 (V2) |
11. Tham khảo pattern có sẵn trên web để port
| Web helper/hook | Mobile tương đương |
|---|---|
useTenant() |
Giống, dùng React Query + cache từ Watermelon |
useResourceLabel() (industry-aware) |
Port y hệt |
useCurrency(), useFormatMoney() |
Port y hệt |
lib/booking-status.constants.ts (transitions, colors) |
Share qua codegen hoặc copy |
lib/timezone.ts (salon tz utilities) |
Port dùng luxon hoặc date-fns-tz |
components/form/* (FormField, PhoneField, MoneyField) |
Tạo bản native tương đương |
components/ui/ConfirmDialog |
react-native-modal + cùng API |
useFormMutation |
Giống (React Query mutation wrapper) |
useToast() |
react-native-toast-message hoặc custom |
💡 Một số helper có thể extract thành package shared (
@booking/shared) nếu monorepo. Hiện là multi-repo nên copy + cùng DTO types qua OpenAPI codegen là đủ.
12. Câu hỏi mở — cần decision trước khi bắt đầu
- Auth login: chỉ email/password, hay cho phép staff login bằng phone + OTP? PRD không nói rõ.
- Walk-in customer: bắt buộc nhập phone hay optional? Hiện public flow là optional.
- Self-pick race: backend đã có optimistic lock? Nếu chưa → cần thêm
If-Match/ version check trên PATCH booking. - Notification sound/vibration: có cần per-event preference không? P1 là đủ.
- Dark mode: salon hay sáng sủa, nhưng đêm staff xử lý booking có thể tối. Theme system nên follow branding primaryColor.
- Min OS: iOS 14+ / Android 8+? Tuỳ Expo SDK version (SDK 51+ yêu cầu iOS 13.4+ / Android 7+).
- Payment in mobile: có ý định cho owner nhập credentials KHÔNG BAO GIỜ trên mobile (confirmed). Nhưng capture / refund / void từ mobile nên cho, vì thường xử lý ngay khi khách trả tiền.
- Admin impersonate (login as tenant): defer, chỉ desktop.
- Waitlist / prepaid packages: roadmap (docs/progress) nói "Long-term". Mobile nhận lịch scope V2.
- Commission tracking:
Resource.metadata.commissionđã có. Có show cho staff xem tiền hoa hồng không? → đề xuất P1 kèm toggle owner bật/tắt per-tenant.
13. Tóm tắt — bắt đầu từ đâu?
Đề xuất sprint 1 (2 tuần) để chứng minh vòng khép kín:
- Backend:
DeviceToken+POST /api/auth/devices; 1 BullMQ worker gửi Expo push; listen 1 event (BookingCreated); thêmIdempotency-KeychoPOST /bookings. - Mobile: login + SecureStore + codegen + WatermelonDB 2 model (Booking, Resource).
- Mobile: gọi
GET /api/bookings?date=today→ cache WatermelonDB → My Schedule đọc offline được. - Mobile: offline walk-in → queue POST với idempotency key → flush khi online.
- Mobile: nhận push khi booking mới → deep link vào Booking Detail.
Xong sprint 1 → có framework để mở rộng nhanh. Các phase sau chủ yếu là thêm UI + lặp lại pattern read/write này cho entities khác.
14. Keep in sync
Doc này update song song khi:
- Thêm/sửa TenantSettings field mới (link
admin-settings-inventory.md). - Backend thêm domain event cần push.
- Thay đổi sync protocol.
- Release mobile version (ghi trong
docs/progress/changelog.md).