Appearance
UI Spec — Thu thập & Chuẩn hoá thông tin cá nhân khách hàng
Version: 1.0 Ngày: 25/03/2026 Tham chiếu:
docs/features/thu-thap-thong-tin-khach-hang/prd.mdPlatform: Flutter Customer App (SCR-01, SCR-02) + Vue 3 Admin Web (SCR-03)
B1) Screen Map
[Flutter Customer App]
├── Auth Flow (hiện có — thêm logic check)
│ ├── Splash → Login/Signup → Auth Success
│ │ ├── SCR-01: ConsentScreen # MỚI — fullscreen, bắt buộc
│ │ │ └── Ref: FR-001, DEC-001, DEC-002, DEC-008
│ │ ├── SCR-02: ProfileUpdatePopup # MỚI — dialog, có thể bỏ qua
│ │ │ └── Ref: FR-002, FR-003, DEC-001, DEC-003, DEC-004, DEC-005
│ │ └── Dashboard (hiện có)
│ │ └── Event Popup (hiện có, không đổi)
│
[Vue 3 Admin Web]
└── /s/app-settings/customer-staff-app/
└── Tab "Ứng dụng khách hàng" (hiện có)
└── SCR-03: Consent Tab # MỚI — tab mới trong trang có sẵn
├── Section: Thống kê # FR-007, DEC-006
├── Section: Cấu hình Consent # FR-006, DEC-006, DEC-008
└── Section: Đề xuất cập nhật # FR-006, DEC-008Danh sách màn hình
| SCR | Tên | Platform | Loại | FR refs | DEC refs |
|---|---|---|---|---|---|
| SCR-01 | Consent Screen | Flutter mobile | MỚI — fullscreen | FR-001 | DEC-001, DEC-002, DEC-008 |
| SCR-02 | Profile Update Popup | Flutter mobile | MỚI — dialog | FR-002, FR-003 | DEC-001, DEC-003, DEC-004, DEC-005 |
| SCR-03 | Admin Consent Tab | Vue 3 Admin Web | MỚI tab trong trang có sẵn | FR-006, FR-007 | DEC-006, DEC-008 |
B2) SCR-01: Consent Screen (Flutter Mobile)
Ref: FR-001 | DEC-001 (data-collection-first), DEC-002 (single-page), DEC-008 (server-driven) Pattern: Fullscreen, không có nút back/close. BLoC pattern: modules/consent/bloc/, modules/consent/views/.
Layout ASCII — Portrait
┌─────────────────────────────────────┐
│ (StatusBar — iOS/Android) │
├─────────────────────────────────────┤
│ │
│ [ Logo Diva — 80px ] │
│ │
│ ┌─────────────────────────────┐ │
│ │ │ │
│ │ Chào mừng bạn đến │ │
│ │ Viện Thẩm Mỹ Diva! │ │
│ │ │ │
│ │ Để phục vụ bạn, chúng │ │
│ │ tôi sử dụng thông tin │ │
│ │ cơ bản (tên, SĐT) và │ │
│ │ gửi thông báo liên quan │ │
│ │ đến dịch vụ. │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ [v] Nhận thông tin khuyến │ │
│ │ mãi │ │
│ │ SMS, push, Zalo │ │
│ ├─────────────────────────────┤ │
│ │ [v] Hiển thị ảnh điều trị │ │
│ │ trên app của bạn │ │
│ └─────────────────────────────┘ │
│ │
│ Bằng việc tiếp tục, bạn đồng ý │
│ với Chính sách bảo mật và │
│ Điều khoản sử dụng của chúng tôi │
│ (link underline) │
│ │
│ ┌─────────────────────────────┐ │
│ │ Đồng ý & Tiếp tục │ │
│ │ (ThemeButton.primary) │ │
│ └─────────────────────────────┘ │
│ │
│ (SafeArea bottom) │
└─────────────────────────────────────┘Chi tiết thành phần
| # | Thành phần | Widget/Component | Mô tả |
|---|---|---|---|
| 1 | Logo Diva | Image.asset hoặc CachedNetworkImage | Logo thương hiệu, căn giữa, height 80px |
| 2 | Title | Text bold, size 20 | Nội dung từ consent_config.title |
| 3 | Body | Text regular, size 14 | Nội dung từ consent_config.body, multi-line |
| 4 | Checkbox items | ListView + custom CheckboxListTile | Render động từ consent_config.items[]. Mỗi item: checkbox + label (bold) + description (subtitle) |
| 5 | Link CSBM | GestureDetector → WebView | URL từ app_setting.privacy_policy |
| 6 | Link DKSD | GestureDetector → WebView | URL từ app_setting.term_condition |
| 7 | Nút CTA | ThemeButton.primary full-width | Text: "Đồng ý & Tiếp tục" |
Hành vi
| Hành vi | Chi tiết |
|---|---|
| Checkbox mặc định | Tất cả checked (default: true từ config). Khách có thể bỏ tick |
| Bấm CTA | Upsert customer_consent → consent_data (JSON trạng thái checkbox) + consent_version + accepted_at. Navigate tiếp (FR-004) |
| consent_config không tồn tại | Skip screen, vào Dashboard bình thường (DEC-008 fallback) |
| Kill app giữa màn hình | Login sau hiện lại vì chưa có record customer_consent |
| Swipe back / System back | Không cho phép — WillPopScope return false |
| Link CSBM/DKSD | Mở InAppWebView, có nút Close để quay lại consent screen |
State Matrix — SCR-01
| State | Điều kiện | Hiển thị |
|---|---|---|
| Normal | consent_config tồn tại, chưa consent hoặc version cũ | Layout đầy đủ như wireframe |
| Loading | Đang load consent_config từ app_setting | Spinner tâm màn hình (splash-like) |
| Error | API lỗi load consent_config | Toast "Không tải được cấu hình. Vui lòng thử lại" + nút "Thử lại" |
| Skip | consent_config không tồn tại trong app_setting | Không hiện screen, navigate thẳng tiếp (DEC-008) |
| Already Consented | customer_consent.consent_version = config.version | Không hiện screen, navigate tiếp |
Permission Matrix — SCR-01
| Role | Truy cập | Ghi chú |
|---|---|---|
| user (customer) | Xem + interact | Scope: self only. account_id = X-Hasura-User-Id |
| guest (chưa login) | Không thấy | Screen chỉ hiện sau auth success |
B3) SCR-02: Profile Update Popup (Flutter Mobile)
Ref: FR-002 (popup), FR-003 (retry logic) | DEC-001 (data-collection-first), DEC-003 (3 fields), DEC-004 (dropdown province), DEC-005 (retry) Pattern: Dialog/Popup. Tham khảo: PopupEventScreen pattern (showGeneralDialog + ScaleTransition). BLoC: modules/profile_update/bloc/.
Layout ASCII — Portrait
┌─────────────────────────────────────┐
│ (Overlay nền tối 60%) │
│ │
│ ┌─────────────────────────────┐ │
│ │ [X] │ │
│ │ │ │
│ │ Chúng em muốn hiểu thêm │ │
│ │ về bạn │ │
│ │ │ │
│ │ Thông tin dưới đây giúp │ │
│ │ chúng em cá nhân hoá trải │ │
│ │ nghiệm và phục vụ bạn │ │
│ │ tốt hơn │ │
│ │ │ │
│ │ Ngày sinh │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ dd-mm-yyyy [v] │ │ │
│ │ └───────────────────────┘ │ │
│ │ Cập nhật để nhận voucher │ │
│ │ sinh nhật từ Diva │ │
│ │ │ │
│ │ Nghề nghiệp │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ Chọn nghề nghiệp [v] │ │ │
│ │ └───────────────────────┘ │ │
│ │ Giúp đề xuất dịch vụ │ │
│ │ phù hợp │ │
│ │ │ │
│ │ Tỉnh/Thành phố │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ Chọn tỉnh/thành [v] │ │ │
│ │ └───────────────────────┘ │ │
│ │ Giúp gửi ưu đãi đúng │ │
│ │ khu vực │ │
│ │ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ Cập nhật │ │ │
│ │ │ (ThemeButton.primary) │ │ │
│ │ └───────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ Bỏ qua │ │ │
│ │ │ (TextButton, subtle) │ │ │
│ │ └───────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────┘Layout ASCII — Chỉ còn 1 field (các field khác đã có data)
┌─────────────────────────────────────┐
│ (Overlay nền tối 60%) │
│ │
│ ┌─────────────────────────────┐ │
│ │ [X] │ │
│ │ │ │
│ │ Chúng em muốn hiểu thêm │ │
│ │ về bạn │ │
│ │ │ │
│ │ Thông tin dưới đây giúp │ │
│ │ chúng em cá nhân hoá trải │ │
│ │ nghiệm và phục vụ bạn │ │
│ │ tốt hơn │ │
│ │ │ │
│ │ Nghề nghiệp │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ Chọn nghề nghiệp [v] │ │ │
│ │ └───────────────────────┘ │ │
│ │ Giúp đề xuất dịch vụ │ │
│ │ phù hợp │ │
│ │ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ Cập nhật │ │ │
│ │ └───────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ Bỏ qua │ │ │
│ │ └───────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────┘Chi tiết thành phần
| # | Thành phần | Widget/Component | Mô tả |
|---|---|---|---|
| 1 | Nút X (close) | IconButton top-right | Đóng popup = Bỏ qua |
| 2 | Title | Text bold, size 18 | Nội dung từ profile_update_info.title |
| 3 | Body | Text regular, size 14 | Nội dung từ profile_update_info.body |
| 4 | Field Ngày sinh | InputContainer + DatePicker | Format dd-mm-yyyy. Chỉ hiện nếu account.birthday IS NULL |
| 5 | Field Nghề nghiệp | InputContainer + DropdownButton | Options từ default_master_data (type='occupation', disabled=false). Chỉ hiện nếu account.occupation IS NULL. Ref: DEC-009 |
| 6 | Field Tỉnh/Thành phố | InputContainer + DropdownButton với search | Dùng danh sách province từ getProvincesData() (đã load sẵn tại splash). Chỉ hiện nếu account_address.province_code IS NULL |
| 7 | Hint text | Text regular, size 12, muted color | Hiển thị dưới mỗi field, nội dung từ fields[].hint |
| 8 | Nút Cập nhật | ThemeButton.primary full-width | Mutation update account + upsert account_address |
| 9 | Nút Bỏ qua | TextButton full-width, muted style | Text: "Bỏ qua" |
Dropdown Nghề nghiệp — Options
Options lấy từ
default_master_data(type='occupation', disabled=false). Danh sách hiện có 11 items (nail_worker, hair_worker, dentist, freelancer, civil_servant, spa_worker, retailer, makeup_artist, farmer, marketer, accountant). Admin quản lý tại Cài đặt > Dữ liệu tham chiếu > Nghề nghiệp (Ref: DEC-009).Flutter app query
default_master_data(where: { type: { _eq: "occupation" }, disabled: { _eq: false } })để populate dropdown.
Dropdown Tỉnh/Thành phố
- Dùng danh sách province từ
getProvincesData()(63 tỉnh/thành) - Có search input bên trong dropdown (client-side filter)
- Hiển thị:
province_name(VD: "TP. Hồ Chí Minh", "Hà Nội", "Đà Nẵng") - Lưu:
province_code+province_namevàoaccount_address
Hành vi
| Hành vi | Chi tiết |
|---|---|
| Bấm "Cập nhật" | Mutation update account (birthday, occupation) + upsert account_address (province_code, province_name) + set update_info_completed = true. Đóng popup → navigate tiếp (DEC-001) |
| Cập nhật 1-2 fields | Lưu fields đã chọn. Lần sau chỉ hiện fields còn thiếu (FR-002 AC) |
| Bấm "Bỏ qua" / nút X | Tăng update_info_skip_count +1 trong customer_consent. Đóng popup → Dashboard (FR-003) |
| Tất cả 3 fields đã có | Không hiện popup. Navigate thẳng Dashboard |
| Bấm overlay (ngoài popup) | Không đóng — barrierDismissible: false (tránh misclick) |
| Birthday validation | Ngày sinh phải <= ngày hiện tại. Tuổi >= 12 (born <= 2014). Nếu không hợp lệ → hint đỏ "Ngày sinh không hợp lệ" |
| Nút "Cập nhật" disabled | Khi chưa chọn/nhập field nào → nút disabled (mờ, không tap được) |
Logic retry popup (FR-003, DEC-005)
Điều kiện hiện popup:
1. update_info_completed = false
2. update_info_skip_count < max_skip (mặc định 3)
3. Còn field thiếu (birthday/occupation/province_code có NULL)
4. app_open_count >= update_info_skip_count * reshow_after_opens (mặc định 4)
VD timeline:
Lần 1: Mở app lần 1 → hiện popup → Bỏ qua (skip_count=1)
Lần 2-4: Mở app → KHÔNG hiện (app_open < 1*4)
Lần 5: app_open_count=5 >= 1*4 → hiện popup → Bỏ qua (skip_count=2)
Lần 6-9: KHÔNG hiện (app_open < 2*4)
Lần 10: app_open_count=10 >= 2*4 → hiện popup → Bỏ qua (skip_count=3)
Lần 11+: skip_count=3 >= max_skip → KHÔNG hiện nữaState Matrix — SCR-02
| State | Điều kiện | Hiển thị |
|---|---|---|
| Normal (3 fields) | Cả 3 fields thiếu data | Popup đầy đủ 3 fields |
| Normal (1-2 fields) | Một số fields đã có data | Popup chỉ hiện fields thiếu |
| Loading | Đang submit mutation | Nút "Cập nhật" hiện spinner, disable tap |
| Error | API lỗi khi submit | Toast "Lỗi cập nhật. Vui lòng thử lại" + popup không đóng |
| Skip — đã đủ data | Cả 3 fields NOT NULL | Không hiện popup |
| Skip — đã hoàn thành | update_info_completed = true | Không hiện popup |
| Skip — hết lượt | update_info_skip_count >= max_skip | Không hiện popup |
| Skip — chưa đủ lần mở | app_open_count < skip_count * reshow_after_opens | Không hiện popup |
| Skip — popup tắt | profile_update_info.enabled = false | Không hiện popup |
B4) SCR-03: Admin Consent Tab (Vue 3 Admin Web)
Ref: FR-006 (config), FR-007 (thống kê) | DEC-006 (tab trong Settings), DEC-008 (server-driven) Trang hiện có: /s/app-settings/customer-staff-app/customer — CustomerAndStaffAppEditor.tsxFramework: Vue 3 + Quasar + TypeScript + URQL. Mutation: UpsertAppSettings (có sẵn).
Vị trí tab mới
CustomerAndStaffAppEditor tabs:
├── Tab "Ứng dụng khách hàng" ← hiện có
├── Tab "Ứng dụng nhân viên" ← hiện có
└── Tab "Consent" ← MỚI (thêm vào)Layout ASCII — Desktop (1280px+)
┌──────────────────────────────────────────────────────────────────────┐
│ Cài đặt > Ứng dụng khách hàng & nhân viên │
├──────────────────────────────────────────────────────────────────────┤
│ [Ứng dụng khách hàng] [Ứng dụng nhân viên] [Consent] │
│ ───────────────────────────────────────────────────────── │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ === THỐNG KÊ TỔNG QUAN (FR-007) ================================ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Tổng KH │ │ Đã consent │ │ Có ngày sinh │ │ Có nghề │ │
│ │ │ │ │ │ │ │ nghiệp │ │
│ │ 12,450 │ │ 8,715 │ │ 5,180 │ │ 4,980 │ │
│ │ │ │ 70.0% │ │ 41.6% │ │ 40.0% │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ Có tỉnh/TP │ │
│ │ │ │
│ │ 5,355 │ │
│ │ 43.0% │ │
│ └──────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ === CẤU HÌNH CONSENT (FR-006 — Section 1) ====================== │
│ │
│ Tiêu đề │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Chào mừng bạn đến Viện Thẩm Mỹ Diva! │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Nội dung │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Để phục vụ bạn, chúng tôi sử dụng thông tin cơ bản │ │
│ │ (tên, SĐT) và gửi thông báo liên quan đến dịch vụ. │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Danh sách mục đồng ý │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ # │ Key │ Label │ Mô tả │ MĐ │ │
│ │ 1 │ marketing │ Nhận TT khuyến mãi │ SMS,push,.. │ [v] │ │
│ │ 2 │ treatment_... │ Hiển thị ảnh điều trị │ trên app.. │ [v] │ │
│ │ │ │ │ │ │ │
│ │ [+ Thêm mục] │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Version hiện tại: 1 │
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Lưu │ │ Lưu & Tăng version │ │
│ │ (secondary) │ │ (primary, warning) │ │
│ └──────────────┘ └──────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ === ĐỀ XUẤT CẬP NHẬT (FR-006 — Section 2) ===================== │
│ │
│ Bật/tắt popup đề xuất │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ [TOGGLE ON] Hiển thị popup đề xuất cập nhật thông tin │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Tiêu đề popup │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Chúng em muốn hiểu thêm về bạn │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Nội dung popup │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Thông tin dưới đây giúp chúng em cá nhân hoá trải │ │
│ │ nghiệm và phục vụ bạn tốt hơn │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Số lần bỏ qua tối đa (max_skip) │
│ ┌──────────┐ │
│ │ 3 │ (min: 1, max: 10) │
│ └──────────┘ │
│ │
│ Hiện lại sau mỗi X lần mở app (reshow_after_opens) │
│ ┌──────────┐ │
│ │ 4 │ (min: 1, max: 20) │
│ └──────────┘ │
│ │
│ Danh sách nghề nghiệp │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ℹ️ Quản lý tại: Cài đặt > Dữ liệu tham chiếu > │ │
│ │ Nghề nghiệp (/s/master-data/occupations) │ │
│ │ Hiện có 11 nghề nghiệp. [Đi đến cấu hình →] │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Lưu cài đặt │ │
│ │ (primary) │ │
│ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘Dialog xác nhận "Lưu & Tăng version"
┌────────────────────────────────────────────────┐
│ Cảnh báo tăng version consent │
│ │
│ Khi tăng version, TẤT CẢ khách hàng sẽ phải │
│ đồng ý lại consent khi mở app lần tiếp theo. │
│ │
│ Version hiện tại: 1 → Version mới: 2 │
│ │
│ Bạn có chắc chắn muốn tiếp tục? │
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Huỷ │ │ Xác nhận tăng │ │
│ │ (outlined) │ │ (negative/red) │ │
│ └──────────────┘ └──────────────────────┘ │
└────────────────────────────────────────────────┘Chi tiết thành phần
| # | Thành phần | Component | Mô tả |
|---|---|---|---|
| 1 | Stat cards | QCard grid (QRow + QCol) | 5 cards: Tổng KH, Đã consent (%), Có ngày sinh (%), Có nghề nghiệp (%), Có tỉnh/TP (%) |
| 2 | Input tiêu đề consent | QInput | Bind consent_config.title |
| 3 | Textarea nội dung | QInput type="textarea" rows=3 | Bind consent_config.body |
| 4 | Bảng checkbox items | QTable editable inline | Columns: #, Key, Label, Mô tả, Mặc định (checkbox). Row actions: xoá. Footer: + Thêm mục |
| 5 | Nút Lưu | QBtn color="secondary" | Update JSON, giữ version cũ |
| 6 | Nút Lưu & Tăng version | QBtn color="primary" | Mở dialog xác nhận → update JSON + version++ |
| 7 | Toggle bật/tắt popup | QToggle | Bind profile_update_info.enabled |
| 8 | Input tiêu đề popup | QInput | Bind profile_update_info.title |
| 9 | Textarea nội dung popup | QInput type="textarea" rows=2 | Bind profile_update_info.body |
| 10 | Input max_skip | QInput type="number" | Range 1-10, bind profile_update_info.max_skip |
| 11 | Input reshow_after_opens | QInput type="number" | Range 1-20, bind profile_update_info.reshow_after_opens |
| 12 | Link cấu hình nghề nghiệp | QBanner info + RouterLink | Hiển thị note "Quản lý tại Cài đặt > Dữ liệu tham chiếu > Nghề nghiệp" + link đến /s/master-data/occupations. Ref: DEC-009 |
| 13 | Nút Lưu cài đặt | QBtn color="primary" | Update profile_update_info trong app_setting |
Hành vi — Section Thống kê (FR-007)
| Hành vi | Chi tiết |
|---|---|
| Data load | Query aggregate khi mount tab. Không cache — realtime mỗi lần mở |
| % tính | count / tổng_KH * 100, hiển thị 1 decimal (VD: 41.6%) |
| Tổng KH = 0 | Hiển thị "0" cho count, "--" cho % |
Hành vi — Section Consent Config (FR-006)
| Hành vi | Chi tiết |
|---|---|
| Nút Lưu | Gọi UpsertAppSettings mutation với consent_config JSON mới, giữ version cũ. Toast "Đã lưu cấu hình consent" |
| Nút Lưu & Tăng version | Mở dialog xác nhận. Nếu confirm → version++ + save. Toast "Đã lưu + tăng version. Tất cả khách sẽ phải đồng ý lại" |
| Thêm checkbox item | Thêm row mới vào bảng: key (required, snake_case), label (required), description, default (checkbox, mặc định true) |
| Xoá checkbox item | Xoá row. Không cần confirm nếu chưa save |
| Key validation | Key phải unique, chỉ chứa a-z0-9_. Duplicate → highlight đỏ |
Hành vi — Section Đề xuất cập nhật (FR-006)
| Hành vi | Chi tiết |
|---|---|
| Toggle tắt | Popup sẽ không hiện trên app khách. Tất cả form fields vẫn editable |
| Nút Lưu cài đặt | Gọi UpsertAppSettings mutation với profile_update_info JSON. Toast "Đã lưu cài đặt đề xuất cập nhật" |
| Click "Đi đến cấu hình" | Navigate đến /s/master-data/occupations (trang quản lý nghề nghiệp trong Master Data). Ref: DEC-009 |
| max_skip validation | Số nguyên, range 1-10. Ngoài range → border đỏ + hint "Giá trị từ 1 đến 10" |
| reshow_after_opens validation | Số nguyên, range 1-20. Ngoài range → border đỏ + hint "Giá trị từ 1 đến 20" |
State Matrix — SCR-03
| State | Điều kiện | Hiển thị |
|---|---|---|
| Normal | Data load thành công | Layout đầy đủ như wireframe |
| Loading | Đang load app_setting + aggregate stats | Skeleton loader trên stat cards + spinner trên form sections |
| Empty | app_setting chưa có consent_config/profile_update_info | Hiện form trống với giá trị mặc định (seed data) |
| Error | API lỗi load hoặc save | Toast "Lỗi tải/lưu cấu hình. Vui lòng thử lại" + nút "Thử lại" |
| No Permission | User không phải admin | Tab "Consent" ẩn trong tab list (Diva RBAC: ẩn, không disable) |
| Saving | Đang gọi mutation save | Nút Lưu hiện spinner, disable tất cả input |
Permission Matrix — SCR-03
| Role | Xem tab | Sửa config | Xem thống kê | Ghi chú |
|---|---|---|---|---|
| admin | Thấy tab | Sửa được | Xem được | Full access |
| Không phải admin | Tab ẩn | -- | -- | Diva RBAC: ẩn menu/button |
B5) User Flows
Flow 1: Khách mới — Đăng ký + Consent + Cập nhật thông tin
Ref: FR-001, FR-002, FR-004 | DEC-001, DEC-002
Khách mở app lần đầu
→ Splash (load config + consent_config + provinces)
→ Onboarding slides
→ Đăng nhập SĐT → OTP → Signup form (tên, giới tính, ngày sinh, email, chi nhánh)
→ Auth Success
│
├── Bước 1: Check consent
│ → Query customer_consent WHERE account_id = user_id
│ → Không có record → Hiện SCR-01 (ConsentScreen)
│ → Khách check/uncheck checkbox
│ → Bấm "Đồng ý & Tiếp tục"
│ → Upsert customer_consent (consent_data, consent_version, accepted_at)
│
├── Bước 2: Check profile update
│ → Check account fields (birthday, occupation, province_code)
│ → birthday đã nhập khi signup → ẩn field birthday
│ → occupation + province_code NULL → hiện SCR-02 với 2 fields
│ → Khách chọn nghề + tỉnh/TP
│ → Bấm "Cập nhật"
│ → Mutation update account + upsert account_address
│ → update_info_completed = true
│
└── Navigate → Dashboard → Event Popup (nếu có) → Push prompt (OS)Flow 2: Khách cũ — Bỏ qua popup + Retry logic
Ref: FR-003, FR-005 | DEC-005
Khách mở app (đã consent, chưa cập nhật đủ)
→ Auth Success → increment app_open_count (+1)
│
├── Check: update_info_skip_count < max_skip?
│ ├── skip_count = 1, app_open_count = 3, reshow = 4
│ │ → 3 < 1*4 → KHÔNG hiện popup → Dashboard
│ │
│ ├── skip_count = 1, app_open_count = 5, reshow = 4
│ │ → 5 >= 1*4 → HIỆN popup (SCR-02)
│ │ → Khách bấm "Bỏ qua"
│ │ → skip_count = 2, navigate → Dashboard
│ │
│ └── skip_count = 3 (>= max_skip)
│ → KHÔNG hiện popup → Dashboard
│
└── DashboardFlow 3: Admin — Cấu hình consent + thống kê
Ref: FR-006, FR-007 | DEC-006, DEC-008
Admin mở Cài đặt > Ứng dụng KH & NV
→ Click tab "Consent" (SCR-03)
│
├── Section Thống kê (tự load khi mount)
│ → Xem 5 stat cards (tổng KH, consent %, birthday %, occupation %, province %)
│
├── Section Consent Config
│ → Sửa title/body/checkbox items
│ → Bấm "Lưu" → update JSON, giữ version
│ → HOẶC bấm "Lưu & Tăng version"
│ → Dialog cảnh báo "Tất cả KH sẽ phải đồng ý lại"
│ → Bấm "Xác nhận tăng" → version++ + save
│
└── Section Đề xuất cập nhật
→ Toggle bật/tắt popup
→ Sửa title, body, max_skip, reshow_after_opens
→ Xem link "Đi đến cấu hình" nghề nghiệp (→ /s/master-data/occupations)
→ Bấm "Lưu cài đặt" → update app_settingFlow 4: Consent version update — Khách cũ bị hỏi lại
Ref: FR-001, FR-006 | DEC-002, DEC-008
Admin tăng consent version (1 → 2) trên SCR-03
↓
Khách cũ (đã consent version 1) mở app
→ Auth Success
→ Query customer_consent → consent_version = 1
→ Load consent_config → version = 2
→ 1 < 2 → Hiện SCR-01 (ConsentScreen) với nội dung mới
→ Khách bấm "Đồng ý & Tiếp tục"
→ Upsert customer_consent: consent_version = 2, consent_data mới
→ Tiếp tục flow bình thường (check SCR-02 nếu cần)B6) Notification Spec
Không có notification mới cho feature này.
Consent screen và profile update popup là UI-only interactions. Không trigger push notification, in-app notification, SMS, hay Zalo message nào.
B7) Copy Text Dictionary
Diva chỉ hỗ trợ tiếng Việt, không cần i18n EN.
SCR-01: Consent Screen (Flutter)
| Key | Text | Context |
|---|---|---|
consent.title | Chào mừng bạn đến Viện Thẩm Mỹ Diva! | Title — dynamic từ config |
consent.body | Để phục vụ bạn, chúng tôi sử dụng thông tin cơ bản (tên, SĐT) và gửi thông báo liên quan đến dịch vụ. | Body — dynamic từ config |
consent.item.marketing.label | Nhận thông tin khuyến mãi | Checkbox label |
consent.item.marketing.desc | SMS, push, Zalo | Checkbox subtitle |
consent.item.treatment_photo.label | Hiển thị ảnh điều trị | Checkbox label |
consent.item.treatment_photo.desc | trên app của bạn | Checkbox subtitle |
consent.legal_text | Bằng việc tiếp tục, bạn đồng ý với {link_privacy} và {link_terms} của chúng tôi | Legal disclaimer |
consent.link.privacy | Chính sách bảo mật | Link text |
consent.link.terms | Điều khoản sử dụng | Link text |
consent.btn.accept | Đồng ý & Tiếp tục | CTA button |
consent.error.load_failed | Không tải được cấu hình. Vui lòng thử lại | Error toast |
consent.btn.retry | Thử lại | Retry button |
SCR-02: Profile Update Popup (Flutter)
| Key | Text | Context |
|---|---|---|
profile_update.title | Chúng em muốn hiểu thêm về bạn | Title — dynamic từ config |
profile_update.body | Thông tin dưới đây giúp chúng em cá nhân hoá trải nghiệm và phục vụ bạn tốt hơn | Body — dynamic từ config |
profile_update.field.birthday.label | Ngày sinh | Field label |
profile_update.field.birthday.hint | Cập nhật để nhận voucher sinh nhật từ Diva | Hint text |
profile_update.field.birthday.placeholder | dd-mm-yyyy | Placeholder |
profile_update.field.birthday.error | Ngày sinh không hợp lệ | Validation error |
profile_update.field.occupation.label | Nghề nghiệp | Field label |
profile_update.field.occupation.hint | Giúp đề xuất dịch vụ phù hợp | Hint text |
profile_update.field.occupation.placeholder | Chọn nghề nghiệp | Placeholder |
profile_update.field.province.label | Tỉnh/Thành phố | Field label |
profile_update.field.province.hint | Giúp gửi ưu đãi đúng khu vực | Hint text |
profile_update.field.province.placeholder | Chọn tỉnh/thành | Placeholder |
profile_update.field.province.search | Tìm tỉnh/thành... | Search input trong dropdown |
profile_update.btn.submit | Cập nhật | CTA button |
profile_update.btn.skip | Bỏ qua | Skip button |
profile_update.error.submit_failed | Lỗi cập nhật. Vui lòng thử lại | Error toast |
profile_update.success | Cảm ơn bạn đã cập nhật thông tin! | Success toast |
| (occupation options) | (lấy động từ default_master_data type='occupation') | Dropdown options render từ DB, không hardcode. Ref: DEC-009 |
SCR-03: Admin Consent Tab (Vue 3)
| Key | Text | Context |
|---|---|---|
admin.consent.tab_label | Consent | Tab label |
admin.consent.stats.title | Thống kê tổng quan | Section title |
admin.consent.stats.total_customers | Tổng khách dùng app | Stat card label |
admin.consent.stats.consented | Đã consent | Stat card label |
admin.consent.stats.has_birthday | Có ngày sinh | Stat card label |
admin.consent.stats.has_occupation | Có nghề nghiệp | Stat card label |
admin.consent.stats.has_province | Có tỉnh/TP | Stat card label |
admin.consent.config.title | Cấu hình Consent | Section title |
admin.consent.config.input_title | Tiêu đề | Input label |
admin.consent.config.input_body | Nội dung | Textarea label |
admin.consent.config.items_title | Danh sách mục đồng ý | Table title |
admin.consent.config.col.key | Key | Column header |
admin.consent.config.col.label | Label | Column header |
admin.consent.config.col.desc | Mô tả | Column header |
admin.consent.config.col.default | Mặc định | Column header |
admin.consent.config.add_item | + Thêm mục | Button |
admin.consent.config.version_label | Version hiện tại: | Version display |
admin.consent.config.btn_save | Lưu | Button |
admin.consent.config.btn_save_version | Lưu & Tăng version | Button |
admin.consent.config.save_success | Đã lưu cấu hình consent | Toast success |
admin.consent.config.save_version_success | Đã lưu + tăng version. Tất cả khách sẽ phải đồng ý lại | Toast success |
admin.consent.config.version_dialog.title | Cảnh báo tăng version consent | Dialog title |
admin.consent.config.version_dialog.body | Khi tăng version, TẤT CẢ khách hàng sẽ phải đồng ý lại consent khi mở app lần tiếp theo. | Dialog body |
admin.consent.config.version_dialog.version_info | Version hiện tại: {current} -> Version mới: | Dialog version info |
admin.consent.config.version_dialog.confirm_text | Bạn có chắc chắn muốn tiếp tục? | Dialog confirm text |
admin.consent.config.version_dialog.btn_cancel | Huỷ | Dialog cancel button |
admin.consent.config.version_dialog.btn_confirm | Xác nhận tăng | Dialog confirm button |
admin.update_info.title | Đề xuất cập nhật thông tin | Section title |
admin.update_info.toggle_label | Hiển thị popup đề xuất cập nhật thông tin | Toggle label |
admin.update_info.input_title | Tiêu đề popup | Input label |
admin.update_info.input_body | Nội dung popup | Textarea label |
admin.update_info.input_max_skip | Số lần bỏ qua tối đa (max_skip) | Input label |
admin.update_info.input_reshow | Hiện lại sau mỗi X lần mở app (reshow_after_opens) | Input label |
admin.update_info.occupation_note | Quản lý tại Cài đặt > Dữ liệu tham chiếu > Nghề nghiệp | Info banner text |
admin.update_info.occupation_link | Đi đến cấu hình | Link text |
admin.update_info.btn_save | Lưu cài đặt | Button |
admin.update_info.save_success | Đã lưu cài đặt đề xuất cập nhật | Toast success |
admin.consent.error.load_failed | Lỗi tải cấu hình. Vui lòng thử lại | Error toast |
admin.consent.error.save_failed | Lỗi lưu cấu hình. Vui lòng thử lại | Error toast |
admin.consent.validation.key_duplicate | Key đã tồn tại | Validation error |
admin.consent.validation.key_format | Key chỉ chứa a-z, 0-9 và dấu gạch dưới | Validation error |
admin.consent.validation.range_max_skip | Giá trị từ 1 đến 10 | Validation error |
admin.consent.validation.range_reshow | Giá trị từ 1 đến 20 | Validation error |
B8) Analytics Events
SCR-01: Consent Screen
| Event | Trigger | Properties | Ghi chú |
|---|---|---|---|
consent_screen_viewed | ConsentScreen mở thành công | { consent_version, is_new_user, source: "login" | "version_update" } | Track impression |
consent_accepted | Khách bấm "Đồng ý & Tiếp tục" | { consent_version, items: { marketing: true, treatment_photo: false }, duration_seconds } | Track thời gian xem + lựa chọn |
consent_link_tapped | Khách tap link CSBM hoặc DKSD | { link_type: "privacy_policy" | "term_condition" } | Track engagement |
SCR-02: Profile Update Popup
| Event | Trigger | Properties | Ghi chú |
|---|---|---|---|
profile_update_shown | Popup hiện thành công | { show_count, fields_shown: ["birthday", "occupation", "province"], skip_count } | Track impression |
profile_update_submitted | Khách bấm "Cập nhật" | { fields_updated: ["occupation", "province"], fields_skipped: ["birthday"], duration_seconds } | Track conversion |
profile_update_skipped | Khách bấm "Bỏ qua" hoặc X | { skip_count, fields_shown, duration_seconds } | Track drop-off |
profile_update_field_interaction | Khách tap vào 1 field | { field_key: "birthday" | "occupation" | "province" } | Track field-level engagement |
SCR-03: Admin Consent Tab
| Event | Trigger | Properties | Ghi chú |
|---|---|---|---|
admin_consent_tab_viewed | Tab Consent được mở | { admin_id } | Track admin usage |
admin_consent_config_saved | Admin bấm Lưu (consent config) | { version_changed: false, items_count } | Track config changes |
admin_consent_version_bumped | Admin xác nhận tăng version | { old_version, new_version } | Track version bumps |
admin_update_info_saved | Admin bấm Lưu cài đặt (popup config) | { enabled, max_skip, reshow_after_opens } | Track config changes |
B9) Tooltip Dictionary
| Màn hình | Field/Icon | Tooltip Text | Điều kiện hiện |
|---|---|---|---|
| SCR-03 — Thống kê | Đã consent (%) | Số khách đã đồng ý consent / tổng khách dùng app x 100% | Hover icon (i) cạnh stat card |
| SCR-03 — Thống kê | Có ngày sinh (%) | Số khách có ngày sinh / tổng khách dùng app x 100% | Hover icon (i) cạnh stat card |
| SCR-03 — Thống kê | Có nghề nghiệp (%) | Số khách có nghề nghiệp / tổng khách dùng app x 100% | Hover icon (i) cạnh stat card |
| SCR-03 — Thống kê | Có tỉnh/TP (%) | Số khách có tỉnh/thành phố (address chính) / tổng khách dùng app x 100% | Hover icon (i) cạnh stat card |
| SCR-03 — Consent | Lưu & Tăng version | Tăng version sẽ yêu cầu TẤT CẢ khách hàng đồng ý lại consent | Hover nút |
| SCR-03 — Đề xuất | max_skip | Số lần tối đa khách được bỏ qua popup. Sau đó không hỏi nữa | Hover icon (i) cạnh label |
| SCR-03 — Đề xuất | reshow_after_opens | Số lần mở app giữa 2 lần hiện popup. VD: 4 = sau 4 lần mở sẽ hiện lại | Hover icon (i) cạnh label |
| SCR-03 — Đề xuất | Nghề nghiệp | Danh sách nghề nghiệp quản lý tại Cài đặt > Dữ liệu tham chiếu > Nghề nghiệp | Hover icon (i) cạnh banner |
| SCR-02 — Birthday | icon (i) | Cập nhật để nhận voucher sinh nhật từ Diva | Luôn hiện dưới field (hint text, không phải tooltip) |
| SCR-02 — Occupation | icon (i) | Giúp đề xuất dịch vụ phù hợp | Luôn hiện dưới field (hint text) |
| SCR-02 — Province | icon (i) | Giúp gửi ưu đãi đúng khu vực | Luôn hiện dưới field (hint text) |
B-Edge Cases
SCR-01: Consent Screen
| # | Edge Case | Điều kiện | Hành vi mong đợi |
|---|---|---|---|
| EC-01-01 | Consent config chưa được seed | app_setting không có key consent_config | Skip consent screen hoàn toàn, vào flow tiếp theo (DEC-008 fallback) |
| EC-01-02 | Khách kill app giữa consent screen | Bấm Home hoặc kill app khi đang ở SCR-01 | Lần mở app sau, login → hiện lại SCR-01 vì chưa có record customer_consent |
| EC-01-03 | Mất mạng khi submit consent | Bấm "Đồng ý & Tiếp tục" nhưng offline | Toast "Không có kết nối mạng. Vui lòng thử lại". Nút CTA trở lại trạng thái bình thường (không spinner). Không navigate |
| EC-01-04 | Khách bỏ hết checkbox rồi bấm CTA | Tất cả checkbox items unchecked | Vẫn cho vào app. Lưu consent_data với tất cả values = false. Consent đã ghi nhận |
| EC-01-05 | Admin thay đổi consent content (giữ version) | consent_config thay đổi title/body nhưng version không đổi | Khách đã consent không bị hỏi lại. Chỉ khách mới thấy nội dung mới |
| EC-01-06 | consent_config.items rỗng (mảng trống) | Admin xoá hết checkbox items | Hiện screen với title + body + link + CTA. Không hiện section checkbox. Khách bấm CTA → lưu consent_data = {} |
| EC-01-07 | Khách đã consent version 1, admin tăng lên version 3 | customer_consent.consent_version = 1, config.version = 3 | Hiện consent screen (1 < 3). Sau khi accept → lưu consent_version = 3 |
SCR-02: Profile Update Popup
| # | Edge Case | Điều kiện | Hành vi mong đợi |
|---|---|---|---|
| EC-02-01 | Khách nhập birthday > ngày hiện tại | Chọn ngày trong tương lai | Validation: "Ngày sinh không hợp lệ". Nút Cập nhật disabled |
| EC-02-02 | Khách nhập birthday tuổi < 12 | VD: sinh năm 2020 (6 tuổi) | Validation: "Ngày sinh không hợp lệ". Nút Cập nhật disabled |
| EC-02-03 | Province data chưa load xong | getProvincesData() chưa hoàn thành từ splash | Dropdown tỉnh/TP hiện spinner bên trong. Khi data load xong → hiện list bình thường |
| EC-02-04 | Khách cập nhật 1 field rồi kill app | Chọn occupation → kill app trước khi bấm Cập nhật | Data chưa được lưu. Lần mở sau hiện popup với cả 3 fields (hoặc fields còn thiếu) |
| EC-02-05 | Khách cập nhật birthday từ profile screen (ngoài popup) | Birthday đã được cập nhật bằng cách khác (edit profile) | Lần mở app sau, popup chỉ hiện occupation + province (birthday đã có). Nếu cả 3 đã đủ → không hiện popup |
| EC-02-06 | profile_update_info config không tồn tại | app_setting không có key profile_update_info | Không hiện popup. Navigate thẳng Dashboard |
| EC-02-07 | profile_update_info.enabled = false | Admin tắt popup | Không hiện popup cho bất kỳ khách nào. Navigate thẳng Dashboard |
| EC-02-08 | Khách chưa consent nhưng đã có đủ 3 fields | Signup form đã nhập birthday, profile đã có occupation + province | Hiện consent screen (SCR-01), sau đó skip popup (đủ data) → Dashboard |
| EC-02-09 | Race condition: 2 thiết bị cùng account | Khách login trên 2 điện thoại, cả 2 hiện popup | Cả 2 đều có thể submit. Mutation cuối cùng wins (last-write-wins). Không conflict vì là upsert |
SCR-03: Admin Consent Tab
| # | Edge Case | Điều kiện | Hành vi mong đợi |
|---|---|---|---|
| EC-03-01 | Admin nhập key trùng trong bảng checkbox items | Gõ key "marketing" khi đã tồn tại | Border đỏ trên row trùng + hint "Key đã tồn tại". Nút Lưu disabled |
| EC-03-02 | Admin xoá hết checkbox items rồi bấm Lưu | items[] = [] | Cho phép lưu. Consent screen trên app sẽ hiện không có checkbox (EC-01-06) |
| EC-03-03 | Admin nhập max_skip = 0 | Input 0 vào max_skip | Validation: "Giá trị từ 1 đến 10". Border đỏ. Nút Lưu disabled |
| EC-03-04 | 2 admin sửa consent config đồng thời | Admin A và Admin B mở tab cùng lúc, cùng sửa | Last-write-wins. Admin save sau sẽ ghi đè. Không có lock mechanism (acceptable cho M-size feature) |
| EC-03-05 | Tổng KH = 0 | Hệ thống mới chưa có khách nào | Stat cards: count = 0, % hiển thị "--" (không hiện 0% hay NaN) |
| EC-03-06 | Admin muốn thêm/sửa nghề nghiệp | Click "Đi đến cấu hình" trên tab Consent | Navigate đến /s/master-data/occupations. CRUD tại đó theo pattern Master Data (Ref: DEC-009) |
| EC-03-07 | Network timeout khi save config | Mutation timeout sau 30s | Toast "Lỗi lưu cấu hình. Vui lòng thử lại". Form giữ nguyên data đã nhập, không reset |