Skip to content

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-008

Danh sách màn hình

SCRTênPlatformLoạiFR refsDEC refs
SCR-01Consent ScreenFlutter mobileMỚI — fullscreenFR-001DEC-001, DEC-002, DEC-008
SCR-02Profile Update PopupFlutter mobileMỚI — dialogFR-002, FR-003DEC-001, DEC-003, DEC-004, DEC-005
SCR-03Admin Consent TabVue 3 Admin WebMỚI tab trong trang có sẵnFR-006, FR-007DEC-006, DEC-008

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ầnWidget/ComponentMô tả
1Logo DivaImage.asset hoặc CachedNetworkImageLogo thương hiệu, căn giữa, height 80px
2TitleText bold, size 20Nội dung từ consent_config.title
3BodyText regular, size 14Nội dung từ consent_config.body, multi-line
4Checkbox itemsListView + custom CheckboxListTileRender động từ consent_config.items[]. Mỗi item: checkbox + label (bold) + description (subtitle)
5Link CSBMGestureDetector → WebViewURL từ app_setting.privacy_policy
6Link DKSDGestureDetector → WebViewURL từ app_setting.term_condition
7Nút CTAThemeButton.primary full-widthText: "Đồng ý & Tiếp tục"

Hành vi

Hành viChi tiết
Checkbox mặc địnhTất cả checked (default: true từ config). Khách có thể bỏ tick
Bấm CTAUpsert 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ạiSkip screen, vào Dashboard bình thường (DEC-008 fallback)
Kill app giữa màn hìnhLogin sau hiện lại vì chưa có record customer_consent
Swipe back / System backKhông cho phép — WillPopScope return false
Link CSBM/DKSDMở InAppWebView, có nút Close để quay lại consent screen

State Matrix — SCR-01

StateĐiều kiệnHiển thị
Normalconsent_config tồn tại, chưa consent hoặc version cũLayout đầy đủ như wireframe
LoadingĐang load consent_config từ app_settingSpinner tâm màn hình (splash-like)
ErrorAPI lỗi load consent_configToast "Không tải được cấu hình. Vui lòng thử lại" + nút "Thử lại"
Skipconsent_config không tồn tại trong app_settingKhông hiện screen, navigate thẳng tiếp (DEC-008)
Already Consentedcustomer_consent.consent_version = config.versionKhông hiện screen, navigate tiếp

Permission Matrix — SCR-01

RoleTruy cậpGhi chú
user (customer)Xem + interactScope: self only. account_id = X-Hasura-User-Id
guest (chưa login)Không thấyScreen 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ầnWidget/ComponentMô tả
1Nút X (close)IconButton top-rightĐóng popup = Bỏ qua
2TitleText bold, size 18Nội dung từ profile_update_info.title
3BodyText regular, size 14Nội dung từ profile_update_info.body
4Field Ngày sinhInputContainer + DatePickerFormat dd-mm-yyyy. Chỉ hiện nếu account.birthday IS NULL
5Field Nghề nghiệpInputContainer + DropdownButtonOptions từ default_master_data (type='occupation', disabled=false). Chỉ hiện nếu account.occupation IS NULL. Ref: DEC-009
6Field Tỉnh/Thành phốInputContainer + DropdownButton với searchDùng danh sách province từ getProvincesData() (đã load sẵn tại splash). Chỉ hiện nếu account_address.province_code IS NULL
7Hint textText regular, size 12, muted colorHiển thị dưới mỗi field, nội dung từ fields[].hint
8Nút Cập nhậtThemeButton.primary full-widthMutation update account + upsert account_address
9Nút Bỏ quaTextButton full-width, muted styleText: "Bỏ qua"

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.

  • Dùng danh sách province từ getProvincesData() (63 tỉnh/thành)
  • 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_name vào account_address

Hành vi

Hành viChi 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 fieldsLưu fields đã chọn. Lần sau chỉ hiện fields còn thiếu (FR-002 AC)
Bấm "Bỏ qua" / nút XTă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 validationNgà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" disabledKhi 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ữa

State Matrix — SCR-02

StateĐiều kiệnHiển thị
Normal (3 fields)Cả 3 fields thiếu dataPopup đầy đủ 3 fields
Normal (1-2 fields)Một số fields đã có dataPopup chỉ hiện fields thiếu
LoadingĐang submit mutationNút "Cập nhật" hiện spinner, disable tap
ErrorAPI lỗi khi submitToast "Lỗi cập nhật. Vui lòng thử lại" + popup không đóng
Skip — đã đủ dataCả 3 fields NOT NULLKhông hiện popup
Skip — đã hoàn thànhupdate_info_completed = trueKhông hiện popup
Skip — hết lượtupdate_info_skip_count >= max_skipKhông hiện popup
Skip — chưa đủ lần mởapp_open_count < skip_count * reshow_after_opensKhông hiện popup
Skip — popup tắtprofile_update_info.enabled = falseKhông hiện popup

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/customerCustomerAndStaffAppEditor.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ầnComponentMô tả
1Stat cardsQCard grid (QRow + QCol)5 cards: Tổng KH, Đã consent (%), Có ngày sinh (%), Có nghề nghiệp (%), Có tỉnh/TP (%)
2Input tiêu đề consentQInputBind consent_config.title
3Textarea nội dungQInput type="textarea" rows=3Bind consent_config.body
4Bảng checkbox itemsQTable editable inlineColumns: #, Key, Label, Mô tả, Mặc định (checkbox). Row actions: xoá. Footer: + Thêm mục
5Nút LưuQBtn color="secondary"Update JSON, giữ version cũ
6Nút Lưu & Tăng versionQBtn color="primary"Mở dialog xác nhận → update JSON + version++
7Toggle bật/tắt popupQToggleBind profile_update_info.enabled
8Input tiêu đề popupQInputBind profile_update_info.title
9Textarea nội dung popupQInput type="textarea" rows=2Bind profile_update_info.body
10Input max_skipQInput type="number"Range 1-10, bind profile_update_info.max_skip
11Input reshow_after_opensQInput type="number"Range 1-20, bind profile_update_info.reshow_after_opens
12Link cấu hình nghề nghiệpQBanner info + RouterLinkHiể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
13Nút Lưu cài đặtQBtn color="primary"Update profile_update_info trong app_setting

Hành vi — Section Thống kê (FR-007)

Hành viChi tiết
Data loadQuery aggregate khi mount tab. Không cache — realtime mỗi lần mở
% tínhcount / tổng_KH * 100, hiển thị 1 decimal (VD: 41.6%)
Tổng KH = 0Hiển thị "0" cho count, "--" cho %
Hành viChi tiết
Nút LưuGọ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 versionMở 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 itemThêm row mới vào bảng: key (required, snake_case), label (required), description, default (checkbox, mặc định true)
Xoá checkbox itemXoá row. Không cần confirm nếu chưa save
Key validationKey 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 viChi tiết
Toggle tắtPopup sẽ không hiện trên app khách. Tất cả form fields vẫn editable
Nút Lưu cài đặtGọ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 validationSố nguyên, range 1-10. Ngoài range → border đỏ + hint "Giá trị từ 1 đến 10"
reshow_after_opens validationSố nguyên, range 1-20. Ngoài range → border đỏ + hint "Giá trị từ 1 đến 20"

State Matrix — SCR-03

StateĐiều kiệnHiển thị
NormalData load thành côngLayout đầy đủ như wireframe
LoadingĐang load app_setting + aggregate statsSkeleton loader trên stat cards + spinner trên form sections
Emptyapp_setting chưa có consent_config/profile_update_infoHiện form trống với giá trị mặc định (seed data)
ErrorAPI lỗi load hoặc saveToast "Lỗi tải/lưu cấu hình. Vui lòng thử lại" + nút "Thử lại"
No PermissionUser không phải adminTab "Consent" ẩn trong tab list (Diva RBAC: ẩn, không disable)
SavingĐang gọi mutation saveNút Lưu hiện spinner, disable tất cả input

Permission Matrix — SCR-03

RoleXem tabSửa configXem thống kêGhi chú
adminThấy tabSửa đượcXem đượcFull access
Không phải adminTab ẩn----Diva RBAC: ẩn menu/button

B5) User Flows

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

  └── Dashboard

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_setting

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.

KeyTextContext
consent.titleChà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.labelNhận thông tin khuyến mãiCheckbox label
consent.item.marketing.descSMS, push, ZaloCheckbox subtitle
consent.item.treatment_photo.labelHiển thị ảnh điều trịCheckbox label
consent.item.treatment_photo.desctrên app của bạnCheckbox subtitle
consent.legal_textBằng việc tiếp tục, bạn đồng ý với {link_privacy} và {link_terms} của chúng tôiLegal disclaimer
consent.link.privacyChính sách bảo mậtLink text
consent.link.termsĐiều khoản sử dụngLink text
consent.btn.acceptĐồng ý & Tiếp tụcCTA button
consent.error.load_failedKhông tải được cấu hình. Vui lòng thử lạiError toast
consent.btn.retryThử lạiRetry button

SCR-02: Profile Update Popup (Flutter)

KeyTextContext
profile_update.titleChúng em muốn hiểu thêm về bạnTitle — dynamic từ config
profile_update.bodyThô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ơnBody — dynamic từ config
profile_update.field.birthday.labelNgày sinhField label
profile_update.field.birthday.hintCập nhật để nhận voucher sinh nhật từ DivaHint text
profile_update.field.birthday.placeholderdd-mm-yyyyPlaceholder
profile_update.field.birthday.errorNgày sinh không hợp lệValidation error
profile_update.field.occupation.labelNghề nghiệpField label
profile_update.field.occupation.hintGiúp đề xuất dịch vụ phù hợpHint text
profile_update.field.occupation.placeholderChọn nghề nghiệpPlaceholder
profile_update.field.province.labelTỉnh/Thành phốField label
profile_update.field.province.hintGiúp gửi ưu đãi đúng khu vựcHint text
profile_update.field.province.placeholderChọn tỉnh/thànhPlaceholder
profile_update.field.province.searchTìm tỉnh/thành...Search input trong dropdown
profile_update.btn.submitCập nhậtCTA button
profile_update.btn.skipBỏ quaSkip button
profile_update.error.submit_failedLỗi cập nhật. Vui lòng thử lạiError toast
profile_update.successCả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
KeyTextContext
admin.consent.tab_labelConsentTab label
admin.consent.stats.titleThống kê tổng quanSection title
admin.consent.stats.total_customersTổng khách dùng appStat card label
admin.consent.stats.consentedĐã consentStat card label
admin.consent.stats.has_birthdayCó ngày sinhStat card label
admin.consent.stats.has_occupationCó nghề nghiệpStat card label
admin.consent.stats.has_provinceCó tỉnh/TPStat card label
admin.consent.config.titleCấu hình ConsentSection title
admin.consent.config.input_titleTiêu đềInput label
admin.consent.config.input_bodyNội dungTextarea label
admin.consent.config.items_titleDanh sách mục đồng ýTable title
admin.consent.config.col.keyKeyColumn header
admin.consent.config.col.labelLabelColumn header
admin.consent.config.col.descMô tảColumn header
admin.consent.config.col.defaultMặc địnhColumn header
admin.consent.config.add_item+ Thêm mụcButton
admin.consent.config.version_labelVersion hiện tại:Version display
admin.consent.config.btn_saveLưuButton
admin.consent.config.btn_save_versionLưu & Tăng versionButton
admin.consent.config.save_successĐã lưu cấu hình consentToast success
admin.consent.config.save_version_successĐã lưu + tăng version. Tất cả khách sẽ phải đồng ý lạiToast success
admin.consent.config.version_dialog.titleCảnh báo tăng version consentDialog title
admin.consent.config.version_dialog.bodyKhi 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_infoVersion hiện tại: {current} -> Version mới:Dialog version info
admin.consent.config.version_dialog.confirm_textBạn có chắc chắn muốn tiếp tục?Dialog confirm text
admin.consent.config.version_dialog.btn_cancelHuỷDialog cancel button
admin.consent.config.version_dialog.btn_confirmXác nhận tăngDialog confirm button
admin.update_info.titleĐề xuất cập nhật thông tinSection title
admin.update_info.toggle_labelHiển thị popup đề xuất cập nhật thông tinToggle label
admin.update_info.input_titleTiêu đề popupInput label
admin.update_info.input_bodyNội dung popupTextarea label
admin.update_info.input_max_skipSố lần bỏ qua tối đa (max_skip)Input label
admin.update_info.input_reshowHiện lại sau mỗi X lần mở app (reshow_after_opens)Input label
admin.update_info.occupation_noteQuản lý tại Cài đặt > Dữ liệu tham chiếu > Nghề nghiệpInfo banner text
admin.update_info.occupation_linkĐi đến cấu hìnhLink text
admin.update_info.btn_saveLưu cài đặtButton
admin.update_info.save_successĐã lưu cài đặt đề xuất cập nhậtToast success
admin.consent.error.load_failedLỗi tải cấu hình. Vui lòng thử lạiError toast
admin.consent.error.save_failedLỗi lưu cấu hình. Vui lòng thử lạiError toast
admin.consent.validation.key_duplicateKey đã tồn tạiValidation error
admin.consent.validation.key_formatKey chỉ chứa a-z, 0-9 và dấu gạch dướiValidation error
admin.consent.validation.range_max_skipGiá trị từ 1 đến 10Validation error
admin.consent.validation.range_reshowGiá trị từ 1 đến 20Validation error

B8) Analytics Events

EventTriggerPropertiesGhi chú
consent_screen_viewedConsentScreen mở thành công{ consent_version, is_new_user, source: "login" | "version_update" }Track impression
consent_acceptedKhá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_tappedKhách tap link CSBM hoặc DKSD{ link_type: "privacy_policy" | "term_condition" }Track engagement

SCR-02: Profile Update Popup

EventTriggerPropertiesGhi chú
profile_update_shownPopup hiện thành công{ show_count, fields_shown: ["birthday", "occupation", "province"], skip_count }Track impression
profile_update_submittedKhách bấm "Cập nhật"{ fields_updated: ["occupation", "province"], fields_skipped: ["birthday"], duration_seconds }Track conversion
profile_update_skippedKhách bấm "Bỏ qua" hoặc X{ skip_count, fields_shown, duration_seconds }Track drop-off
profile_update_field_interactionKhách tap vào 1 field{ field_key: "birthday" | "occupation" | "province" }Track field-level engagement
EventTriggerPropertiesGhi chú
admin_consent_tab_viewedTab Consent được mở{ admin_id }Track admin usage
admin_consent_config_savedAdmin bấm Lưu (consent config){ version_changed: false, items_count }Track config changes
admin_consent_version_bumpedAdmin xác nhận tăng version{ old_version, new_version }Track version bumps
admin_update_info_savedAdmin bấm Lưu cài đặt (popup config){ enabled, max_skip, reshow_after_opens }Track config changes

B9) Tooltip Dictionary

Màn hìnhField/IconTooltip 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 — ConsentLưu & Tăng versionTăng version sẽ yêu cầu TẤT CẢ khách hàng đồng ý lại consentHover nút
SCR-03 — Đề xuấtmax_skipSố lần tối đa khách được bỏ qua popup. Sau đó không hỏi nữaHover icon (i) cạnh label
SCR-03 — Đề xuấtreshow_after_opensSố lần mở app giữa 2 lần hiện popup. VD: 4 = sau 4 lần mở sẽ hiện lạiHover icon (i) cạnh label
SCR-03 — Đề xuấtNghề nghiệpDanh sách nghề nghiệp quản lý tại Cài đặt > Dữ liệu tham chiếu > Nghề nghiệpHover icon (i) cạnh banner
SCR-02 — Birthdayicon (i)Cập nhật để nhận voucher sinh nhật từ DivaLuôn hiện dưới field (hint text, không phải tooltip)
SCR-02 — Occupationicon (i)Giúp đề xuất dịch vụ phù hợpLuôn hiện dưới field (hint text)
SCR-02 — Provinceicon (i)Giúp gửi ưu đãi đúng khu vựcLuôn hiện dưới field (hint text)

B-Edge Cases

#Edge CaseĐiều kiệnHành vi mong đợi
EC-01-01Consent config chưa được seedapp_setting không có key consent_configSkip consent screen hoàn toàn, vào flow tiếp theo (DEC-008 fallback)
EC-01-02Khách kill app giữa consent screenBấm Home hoặc kill app khi đang ở SCR-01Lần mở app sau, login → hiện lại SCR-01 vì chưa có record customer_consent
EC-01-03Mất mạng khi submit consentBấm "Đồng ý & Tiếp tục" nhưng offlineToast "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-04Khách bỏ hết checkbox rồi bấm CTATất cả checkbox items uncheckedVẫn cho vào app. Lưu consent_data với tất cả values = false. Consent đã ghi nhận
EC-01-05Admin thay đổi consent content (giữ version)consent_config thay đổi title/body nhưng version không đổiKhách đã consent không bị hỏi lại. Chỉ khách mới thấy nội dung mới
EC-01-06consent_config.items rỗng (mảng trống)Admin xoá hết checkbox itemsHiệ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-07Khách đã consent version 1, admin tăng lên version 3customer_consent.consent_version = 1, config.version = 3Hiện consent screen (1 < 3). Sau khi accept → lưu consent_version = 3

SCR-02: Profile Update Popup

#Edge CaseĐiều kiệnHành vi mong đợi
EC-02-01Khách nhập birthday > ngày hiện tạiChọn ngày trong tương laiValidation: "Ngày sinh không hợp lệ". Nút Cập nhật disabled
EC-02-02Khách nhập birthday tuổi < 12VD: 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-03Province data chưa load xonggetProvincesData() chưa hoàn thành từ splashDropdown tỉnh/TP hiện spinner bên trong. Khi data load xong → hiện list bình thường
EC-02-04Khách cập nhật 1 field rồi kill appChọn occupation → kill app trước khi bấm Cập nhậtData 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-05Khá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-06profile_update_info config không tồn tạiapp_setting không có key profile_update_infoKhông hiện popup. Navigate thẳng Dashboard
EC-02-07profile_update_info.enabled = falseAdmin tắt popupKhông hiện popup cho bất kỳ khách nào. Navigate thẳng Dashboard
EC-02-08Khách chưa consent nhưng đã có đủ 3 fieldsSignup form đã nhập birthday, profile đã có occupation + provinceHiện consent screen (SCR-01), sau đó skip popup (đủ data) → Dashboard
EC-02-09Race condition: 2 thiết bị cùng accountKhách login trên 2 điện thoại, cả 2 hiện popupCả 2 đều có thể submit. Mutation cuối cùng wins (last-write-wins). Không conflict vì là upsert
#Edge CaseĐiều kiệnHành vi mong đợi
EC-03-01Admin nhập key trùng trong bảng checkbox itemsGõ key "marketing" khi đã tồn tạiBorder đỏ trên row trùng + hint "Key đã tồn tại". Nút Lưu disabled
EC-03-02Admin xoá hết checkbox items rồi bấm Lưuitems[] = []Cho phép lưu. Consent screen trên app sẽ hiện không có checkbox (EC-01-06)
EC-03-03Admin nhập max_skip = 0Input 0 vào max_skipValidation: "Giá trị từ 1 đến 10". Border đỏ. Nút Lưu disabled
EC-03-042 admin sửa consent config đồng thờiAdmin A và Admin B mở tab cùng lúc, cùng sửaLast-write-wins. Admin save sau sẽ ghi đè. Không có lock mechanism (acceptable cho M-size feature)
EC-03-05Tổng KH = 0Hệ thống mới chưa có khách nàoStat cards: count = 0, % hiển thị "--" (không hiện 0% hay NaN)
EC-03-06Admin muốn thêm/sửa nghề nghiệpClick "Đi đến cấu hình" trên tab ConsentNavigate đến /s/master-data/occupations. CRUD tại đó theo pattern Master Data (Ref: DEC-009)
EC-03-07Network timeout khi save configMutation timeout sau 30sToast "Lỗi lưu cấu hình. Vui lòng thử lại". Form giữ nguyên data đã nhập, không reset