Appearance
Đặc tả giao diện (UI Spec) — Tích hợp Pancake CRM (Pancake CRM Integration)
Phiên bản: 1.0 Ngày: 15/05/2026 Tác giả: PO/BA + AI Tech Lead Tham chiếu: PRD v1.0 + SOURCE_OF_TRUTH v1.0 + EVIDENCE_PACK v1.0 Profile: L (Lớn)
Mục đích: mô tả 5 màn Settings Pancake CRM mới (SCR-01..05) + 1 nhóm delta dropdown ticket source (SCR-06) trên 3 màn hiện hữu của module CRM. Đủ căn cứ để FE triển khai, designer hoàn thiện design system, QA viết test case, PO/BA review. Đọc trước:
decision-brief.md(nếu có) →B-PREDiscovery checklist →B0Hiện trạng UI →B1Bản đồ màn hình →B2.SCR-01..06→B-POSTVerification →B-QUALITYRà soát rủi ro. Văn phong: theo_LANGUAGE_RULES.md+_STYLE_GUIDE.md. Heading + label Mermaid/wireframe Việt-first. CTA dùngLưu / Hủy / Áp dụng / Xác nhận / Phát lại / Kiểm tra kết nối / Kích hoạt lại. Mọi copy hiển thị tiếng Việt tự nhiên ngữ cảnh SaaS spa/thẩm mỹ viện. Enum/code giữ backtick (received,dead_letter,outage_started). Phụ lục mở rộng đã load: EXT-3 (lifecycle ≥4 trạng thái), EXT-4 (RBAC field-level cho api_key), EXT-6 (L flow nhiều màn), EXT-9 (audit log Tab 3), EXT-12 (autosave conflict source routing), EXT-13 (bulk replay DLQ).
Tài liệu đầu vào chuẩn
| File | Vai trò | Nếu xung đột |
|---|---|---|
SOURCE_OF_TRUTH.md v1.0 | Nguồn sự thật chuẩn (25 DEC, Solution Lock) | Ưu tiên cao nhất |
EVIDENCE_PACK.md v1.0 | Layout As-Is, inventory hiện trạng, ứng viên reuse (file:line) | Ưu tiên bằng chứng code/screen |
prd.md v1.0 | FR-001..018, LIFECYCLE-001/002, A9 glossary, A10 công thức | Theo truth đã khóa |
Quy tắc: UI Spec là tài liệu dẫn xuất, không tự phát minh rule. Mọi
REMOVE/HIDEphải có DEC/FR/PO approval. Trace ngược: SCR-01..04 = FR-012 (Settings UI 4 tabs). SCR-05 = container XDetailLayout. SCR-06 = FR-018 (FE delta dropdown). DEC-016 = 4 tabs Admin only. DEC-021 = 3-mức feature flag. DEC-008 = VIP tag NAME case-insensitive. DEC-009 = test mode is_test. DEC-014 = opt-out marketing. DEC-024 = 3 notification template mới.
Hướng dẫn đọc
| Đối tượng | Section cần đọc |
|---|---|
| PO/BA | B-PRE → B0.4 Field × Surface → B1 Screen Map → B2.7 Copy text (B7) |
| Designer / FE Dev | B0.1 Inventory → B2 SCR-01..06 wireframe + variants → B-Microcopy → B-i18n → B-EdgeCases |
| QA | B0.5 State × Screen → B5 Permission Matrix → B6 State Matrix lifecycle → B-EdgeCases G1-G12 |
| Tech Lead | B0.8 Schema cross-check → B0.9 Interactive Inventory → B-QUALITY |
| Admin Diva (ops) | B2 SCR-01..04 → B7 Copy → B-Help |
B-PRE) Discovery checklist (đã hoàn thành Phase 3)
Lưu ý: discovery cho feature này đã hoàn thành đầy đủ ở Phase 3 ANALYZE và được canonical hóa trong
EVIDENCE_PACK.md. Section này ref evidence thay vì re-scan.
B-PRE.1) Tìm component/page hiện có liên quan
Đã scan ở Phase 3 (EVIDENCE_PACK §2-3):
| Hạng mục | Path đã verify | Evidence |
|---|---|---|
| Tickets list page | diva-admin/src/modules/crm/pages/Tickets.tsx | EVIDENCE_PACK §2.1 + line 128-149 (sourceDescriptions), 851-865 (XMultipleSelect filter), 915 (permission gate) |
| Ticket bulk create form | diva-admin/src/modules/crm/components/ticket/TicketMultipleAdd.tsx | §2.2 + line 758 (dropdown source) |
| Customer ticket drawer | diva-admin/src/modules/user/components/customer/CustomerTicketManager.tsx | §2.3 |
| Settings tab pattern reference | diva-admin/src/modules/settings/pages/AppSettingsSmsTemplate.tsx | §2.4 line 14-28 (XDetailLayout) |
| Ticket source array | diva-admin/src/modules/crm/types.ts:146-164 | §3.1 (8 slot hiện có) |
| i18n vi.ts | diva-admin/src/modules/crm/i18n/vi.ts:130-137 | §3.1 |
| Permission framework | diva-admin/src/stores/useGlobalStore.ts:169-182 | §2.5 (hasPermission(moduleId, actionId)) |
| GraphQL queries existing | diva-admin/src/modules/crm/graphql/ticket.graphql | §5 R15 (source_id nullable, không đổi) |
B-PRE.1.A) Scan 100% interactive elements trên existing screens (SCR-06 delta)
Áp dụng cho SCR-06 (FE delta dropdown ticket source). SCR-01..05 là build mới — không cần scan As-Is interactive.
| Loại tương tác | File hiện có | Line | Pattern reuse |
|---|---|---|---|
Filter dropdown Nguồn ticket | Tickets.tsx | 851-865 | XMultipleSelect :options="TicketSources" :label="$t('crm.ticket_source_X')" — auto render slot 9 khi types.ts có entry mới |
Form Tạo Ticket dropdown Nguồn | TicketMultipleAdd.tsx | 758 | dropdown loop — auto render |
| Drawer ticket KH | CustomerTicketManager.tsx | TBD-verify | reuse pattern XMultipleSelect — auto render |
| Tooltip mô tả source (hover icon ⓘ) | Tickets.tsx | 128-149 | sourceDescriptions[ticket_source_X] HTML span — phải ADD entry slot 9 |
| Row click drilldown ticket | Tickets.tsx | TBD-verify | reuse — slot 9 hành xử y như slot 1-8 |
Kết luận discovery: FE delta cho SCR-06 = 3 file thay đổi cứng (types.ts + i18n/vi.ts + Tickets.tsx sourceDescriptions). Không phát hiện hardcode 'ticket_source_1'..'ticket_source_8' ở module CRM (đã grep §3.2). Cảnh báo nhỏ: module report (customer-service, telesales) chưa verify — F18 phải grep audit trước deploy MVP-1 (RSK-005).
B-PRE.2) Bảng inventory phải điền TRƯỚC khi viết B0
| Hạng mục | Đã check? | File / màn hiện có | Ghi chú |
|---|---|---|---|
| Page / route hiện có | [x] | Settings/AppSettings* các loại | XDetailLayout pattern reuse |
| Component reusable | [x] | XDetailLayout, XMultipleSelect, XTable, XDrawer, UserSelect | Có sẵn |
| Form/dialog liên quan | [x] | TicketMultipleAdd (form drawer) | Pattern XDrawer |
| Field hiện có trên Tickets list | [x] | Mã / Trạng thái / Liên hệ / Chi nhánh / Người phụ trách / Nhãn KH / Ngày tạo | §2.1 |
| CTA hiện có | [x] | + Thêm Sửa Xóa toolbar, Áp dụng filter | §2.1 |
| Filter / search hiện có | [x] | Nguồn ticket (8 slot), Trạng thái (4), Người phụ trách, Người nhận bàn giao | §2.1 |
| State hiện có | [x] | Loading skeleton 5 row, Has-data 20/page pagination, Empty | §2 |
| Permission / role gating | [x] | globalStore.hasPermission('ticket_management', 'access') | §2.1 line 915 |
| Notification trigger hiện có | [x] | noti_ticket_reminder_today/tomorrow cron only | EVIDENCE_PACK G5 |
| Export columns hiện có | [x] | ExportTickets.tsx reuse — slot 9 source rendering auto | §3.2 |
| Tooltip / hint hiện có | [x] | sourceDescriptions HTML span hover icon | Tickets.tsx 128-149 |
| Mobile / responsive | [x] | Diva admin desktop-first, tablet OK | Pattern XDetailLayout |
| Analytics events hiện có | [x] | Chưa có analytics tracking cho ticket source change | Build mới (B8) |
B-PRE.3) Phân loại reuse (BẮT BUỘC, ref SOURCE_OF_TRUTH SL-10/SL-13)
| Phần feature | Phân loại | Evidence | Delta cần |
|---|---|---|---|
| Container XDetailLayout 4 tabs Pancake | ✅ Reuse | AppSettingsSmsTemplate.tsx:14-28 | Tạo PancakeCrmSetting.tsx copy pattern |
| Tab 1 form Kết nối | 🆕 Build mới | — | Form mới: workspace_id, api_key, webhook_url copy, kill switch, VIP tag multi-line, "Kiểm tra kết nối" button |
| Tab 2 Source routing table inline-edit | 🔧 Extend pattern | XTable + BranchSelect existing | Bảng mới với inline-edit autosave, "Đồng bộ từ Pancake" button |
| Tab 3 Audit + DLQ list | 🆕 Build mới | — | List event với filter combo, drawer detail raw_payload, replay action |
| Tab 4 Health metrics | 🆕 Build mới | — | Metric cards + outage history + chart trend |
| Dropdown ticket source slot 9 | 🔧 Extend (FE delta) | Tickets.tsx:851, TicketMultipleAdd.tsx:758, types.ts:146-164 | 3 file delta + 1 migration master data |
| Permission gating Admin only | ✅ Reuse | useGlobalStore.hasPermission | Register module pancake_crm_integration |
| Notification template | 🆕 Build mới (3 template) | notification-v2-api sendNotifications action | Seed 3 row vào notification_template |
B0) Hiện trạng UI và Delta Contract
B0.1) Bảng kiểm kê đầy đủ (As-Is + Delta Status)
Phạm vi inventory: (a) các vùng UI hiện hữu của 3 màn CRM mà SCR-06 delta đụng vào. (b) các vùng existing trên Settings sidebar nơi gắn menu "Tích hợp Pancake CRM".
B0.1.A) Tickets list (Tickets.tsx) — SCR-06 delta
| UI ID | Màn / route | Section | Block / field / action | Thứ tự | Hành vi hiện tại | Permission | Mobile? | Delta Status | Hành vi đích | Evidence |
|---|---|---|---|---|---|---|---|---|---|---|
SCR-06-BLK-01 | /crm/tickets | Panel trái filter | Header "Nguồn ticket" | 1 | Tiêu đề label | ticket_management.access | Tablet OK | KEEP | Giữ nguyên | Tickets.tsx:848 |
SCR-06-FLD-02 | /crm/tickets | Panel trái filter | XMultipleSelect dropdown 8 source | 2 | Render từ TicketSources array map qua i18n; multi-select; default "Tất cả" | ticket_management.access | Có | UPDATE | Render 9 source (thêm slot ticket_source_pancake = "Pancake CRM") | Tickets.tsx:851-865, types.ts:146-164 |
SCR-06-BLK-03 | /crm/tickets | Panel trái filter | Tooltip hover icon ⓘ mô tả source 7-8 | 3 | HTML span popup từ sourceDescriptions[ticket_source_X] | ticket_management.access | Có (long-press) | UPDATE | Add entry ticket_source_pancake: "Lead từ Pancake CRM (40+ kênh: FB, Zalo, TikTok, Google Ads...)" | Tickets.tsx:128-149 |
SCR-06-BLK-04 | /crm/tickets | Panel trái filter | Section "Trạng thái" (4 status) | 4 | Multi-select | ticket_management.access | Có | KEEP | Giữ nguyên | Tickets.tsx |
SCR-06-BLK-05 | /crm/tickets | Panel trái filter | Section "Người phụ trách" UserSelect | 5 | dropdown user | ticket_management.access | Có | KEEP | Giữ nguyên | Tickets.tsx |
SCR-06-BLK-06 | /crm/tickets | Panel trái filter | Section "Người nhận bàn giao" + checkbox | 6 | User select + checkbox | ticket_management.access | Có | KEEP | Giữ nguyên | Tickets.tsx |
SCR-06-BLK-07 | /crm/tickets | Panel phải toolbar | Buttons + Thêm Sửa Xóa + filter chip | 1 | Action toolbar | ticket_management.create/update/delete | Có | KEEP | Giữ nguyên (form Tạo Ticket sẽ render slot 9 trong dropdown source — xem SCR-06-BLK-09) | Tickets.tsx |
SCR-06-BLK-08 | /crm/tickets | Panel phải table | Cột # / Mã / Trạng thái / Liên hệ / Chi nhánh / Người phụ trách / Nhãn KH / Ngày tạo + pagination | 1 | Table 20 row/page | ticket_management.access | Card list mobile | KEEP | Giữ nguyên — ticket Pancake render trong cùng table | Tickets.tsx |
B0.1.B) Form Tạo Ticket (TicketMultipleAdd.tsx) — SCR-06 delta
| UI ID | Màn / route | Section | Block / field / action | Thứ tự | Hành vi | Permission | Mobile? | Delta Status | Hành vi đích | Evidence |
|---|---|---|---|---|---|---|---|---|---|---|
SCR-06-BLK-09 | /crm/tickets drawer | Form modal | Field Nguồn XMultipleSelect | (theo form layout) | Multi-select source từ TicketSources | ticket_management.create | Có | UPDATE | Render 9 source — slot 9 hiển thị "Pancake CRM" | TicketMultipleAdd.tsx:758 |
SCR-06-BLK-10 | /crm/tickets drawer | Form modal | Field Khách hàng *, Liên hệ *, Nhóm sản phẩm, ..., Người phụ trách, Ghi chú | (form) | Form drawer existing | ticket_management.create | Có | KEEP | Giữ nguyên | TicketMultipleAdd.tsx |
B0.1.C) Drawer ticket KH (CustomerTicketManager.tsx) — SCR-06 delta
| UI ID | Màn / route | Section | Block / field / action | Hành vi | Permission | Delta Status | Hành vi đích | Evidence |
|---|---|---|---|---|---|---|---|---|
SCR-06-BLK-11 | /customers/:id drawer | Drawer phải | List ticket của KH + nút "Thêm ticket" | List + drawer mở TicketMultipleAdd | ticket_management.access | KEEP | Slot 9 tự render trong drawer source nếu user mở form Tạo Ticket | CustomerTicketManager.tsx |
B0.1.D) Settings sidebar — vị trí gắn menu mới
| UI ID | Màn / route | Section | Block / field / action | Hành vi | Permission | Delta Status | Hành vi đích | Evidence |
|---|---|---|---|---|---|---|---|---|
SCR-05-BLK-01 | Sidebar /settings | Mục "Tích hợp" hoặc Settings list | Các mục SMS template, ZNS template, Notification, Print template... | Navigation existing | settings.access | KEEP | Giữ nguyên | MainSidebar.vue (suy luận) |
SCR-05-BLK-02 | Sidebar /settings | Mục "Tích hợp" | Mục menu "Tích hợp Pancake CRM" | — | pancake_crm_integration.access | NEW | Mục menu mới (v-if="hasPermission('pancake_crm_integration','access')") deeplink /settings/pancake-crm | FR-012, DEC-016 |
B0.1.E) SCR-01..04 — 4 tabs build mới (NEW)
| UI ID | Màn | Section | Block / field / action | Permission | Delta Status | Hành vi đích | Evidence |
|---|---|---|---|---|---|---|---|
SCR-01-* | /settings/pancake-crm/connection | Tab 1 Kết nối | Form workspace + api_key + webhook URL + kill switch + VIP tag | pancake_crm_integration.access/update | NEW | Toàn bộ build mới | FR-012, DEC-016, FR-015 |
SCR-02-* | /settings/pancake-crm/source-routing | Tab 2 Map nguồn | Table inline-edit + "Đồng bộ Pancake" button | pancake_crm_integration.access/update | NEW | Toàn bộ build mới | FR-012, FR-013 |
SCR-03-* | /settings/pancake-crm/audit | Tab 3 Lịch sử + DLQ | Filter + table + drawer detail + replay action | pancake_crm_integration.access/view_all/replay_dlq | NEW | Toàn bộ build mới | FR-012, FR-014, FR-016, FR-017 |
SCR-04-* | /settings/pancake-crm/health | Tab 4 Sức khỏe | Metric cards + outage history + alert config | pancake_crm_integration.access | NEW | Toàn bộ build mới | FR-014, FR-011 |
B0.2) Từ điển Delta Status
| Status | Ý nghĩa | Evidence bắt buộc |
|---|---|---|
KEEP | Giữ nguyên hành vi + layout | Ghi rõ vẫn hiển thị ở target wireframe (B2) |
UPDATE | Đổi copy / data / option list (không đổi layout) | PRD FR + ref file:line |
MOVE | Đổi vị trí, giữ behavior | Ghi trước/sau khu vực nào |
NEW | Thêm mới hoàn toàn | PRD FR + ref DEC |
REMOVE | Bỏ | DEC + FR/AC + PO approval (≥2 evidence) |
HIDE | Ẩn theo điều kiện (permission/state) | Permission + condition rõ |
Trong feature này:
- Không có dòng nào
REMOVE(không xóa UI hiện hữu). - Tất cả
HIDElà gating theo permissionpancake_crm_integration.accesscho menu Settings/Pancake (Diva pattern: ẩn hoàn toàn, không disable).
B0.3) Tiêu chí hoàn thành B0
- [x] 100% UI hiện tại của Tickets/TicketMultipleAdd/CustomerTicketManager (3 màn SCR-06 delta đụng) đã inventory ở B0.1 với Evidence file:line
- [x] Tất cả block existing có
Delta Status(KEEPcho 9/11 row,UPDATEcho 3/11 row,NEWcho 6 row mới) - [x] Không có
REMOVE/HIDEcần evidence PO approval (feature additive only) - [x] Target wireframe B2 SCR-06 hiển thị cả vùng
KEEP(filter section, table) bao quanh vùngUPDATE(dropdown 9 source) - [x] Đường gắn menu mới (B0.1.D) được verify với route Settings existing
B0.4) Ma trận Field × Surface + Validation (11 cột — BẮT BUỘC M+)
Bảng cover toàn bộ field mới xuất hiện trong UI Spec (cross-ref B0.4 dòng X từ B2.4 mỗi SCR). Field thuộc backend-only (vd
payload_hash) không nằm trong bảng này.
B0.4.A) Tab 1 Kết nối (SCR-01)
| Field | Loại | List | Detail/Form | Export | Search/Filter | Required | Format | Business rule | Error copy | Tooltip ref |
|---|---|---|---|---|---|---|---|---|---|---|
workspace_id | Text | — | SCR-01 input readonly sau setup | — | — | yes | UUID Pancake (≤64 ký tự) | Lock sau setup ban đầu, đổi qua "Reset connection" (DEC-021) | "Mã workspace Pancake không hợp lệ. Liên hệ Ops để xác nhận." | B9 row 1 |
api_key | Text (sensitive) | — | SCR-01 input password mask ●●●●●● | — | — | yes | API key Pancake (16-64 ký tự) | Encrypted at rest (KMS); chỉ Admin update; reveal toggle hover icon mắt | "API key sai. Kiểm tra trên trang Pancake admin." | B9 row 2 |
webhook_url | URL | — | SCR-01 readonly + copy icon | — | — | yes (auto-gen) | https://diva.com.vn/api/pancake/record/{token} | Auto-generate khi save lần đầu, không cho edit | (readonly, no error) | B9 row 3 |
webhook_token | Text (sensitive) | — | SCR-01 readonly mask + reveal | — | — | yes (auto-gen) | UUID v4 | Lock sau gen lần đầu; "Reset token" tạo mới + invalidate Pancake config | (readonly, hành động Reset yêu cầu confirm) | B9 row 4 |
kill_switch | Bool (toggle) | — | SCR-01 toggle | — | — | yes | enum | DEC-021 mức 1 — app_setting.pancake_integration.enabled | (toggle, no error) | B9 row 5 |
vip_tag_names | Text multi-line | — | SCR-01 textarea | — | — | no (optional) | mỗi dòng = 1 tag NAME, max 100 dòng, ≤60 ký tự/dòng | DEC-008: match case-insensitive với account.pancake_metadata.pancake_tag_names[]; trim whitespace + lowercase khi save | "Danh sách tag VIP vượt quá 100 dòng. Vui lòng giảm bớt." | B9 row 6 |
connection_status | Enum 4 | — | SCR-01 badge + SCR-04 metric | — | — | yes (auto) | enum {active, paused, suspended_by_pancake, error} | DEC-021 mức 2; auto-update từ Cron/health-check | (badge, no error) | B9 row 7 |
B0.4.B) Tab 2 Source routing (SCR-02)
| Field | Loại | List | Detail/Form | Export | Search/Filter | Required | Format | Business rule | Error copy | Tooltip ref |
|---|---|---|---|---|---|---|---|---|---|---|
pancake_source_id | Text (FK Pancake) | cột readonly | — | (audit log) | — | yes (auto từ /sources) | TEXT Pancake source id | Lock — không cho admin sửa, tự đồng bộ qua Cron 3 | (readonly) | B9 row 8 |
pancake_source_name | Text | cột readonly | — | (audit log) | text search | yes | ≤200 ký tự | Cập nhật qua Cron 3 hourly; nếu Pancake đổi tên → update tự động + log | (readonly) | B9 row 9 |
diva_branch_id | FK uuid | cột inline-edit | dropdown BranchSelect | (audit log) | dropdown filter | conditional (Loose mode cho phép NULL) | UUID branch | DEC-013 Loose mode: NULL → ticket tạo với branch=NULL, leader assign sau | "Chi nhánh không tồn tại. Vui lòng chọn chi nhánh hợp lệ." | B9 row 10 |
is_active | Bool (toggle) | cột inline-toggle | toggle | (audit log) | enum filter {Tất cả, Đang bật, Đang tắt} | yes | enum {true, false} | DEC-021 mức 3 per-source; tắt → status skipped_source_disabled | (toggle, no error) | B9 row 11 |
last_sync_at | DateTime | cột readonly | — | — | — | no | DD/MM/YYYY HH:mm | Auto-update mỗi lần Cron 3 hoặc manual sync | (readonly) | B9 row 12 |
B0.4.C) Tab 3 Audit + DLQ (SCR-03)
| Field | Loại | List | Detail/Form | Export | Search/Filter | Required | Format | Business rule | Error copy | Tooltip ref |
|---|---|---|---|---|---|---|---|---|---|---|
event_id | UUID | cột (8 ký tự đầu) | drawer detail full | (audit log) | search exact | yes (PK) | UUID v4 | Click row → mở drawer | (readonly) | B9 row 13 |
record_id | Text (FK Pancake) | cột truncate 20 ký tự | drawer detail full | (audit log) | search exact | yes | TEXT từ Pancake | Click → highlight các event cùng record_id | "Không tìm thấy record id này." | B9 row 14 |
status | Enum 12 | cột badge | drawer detail | (audit log) | multi-select dropdown | yes | enum 12 trạng thái (xem B9 row 15-26) | LIFECYCLE-001 — terminal vs non-terminal khác badge color | (no error, badge render) | B9 row 15-26 |
event_type | Enum | cột | drawer | (audit log) | dropdown | yes | enum {record, reconciled, polling_injected} | DEC-025 reconciliation inject với reconciled | (readonly) | B9 row 27 |
is_test | Bool | cột icon (test tube nếu true) | drawer toggle readonly | (audit log) | checkbox filter "Bao gồm test events" | no (default false) | enum {true, false} | DEC-009 test events KHÔNG count KPI, auto-cleanup 24h | (readonly) | B9 row 28 |
retry_count | Int | cột số | drawer | (audit log) | range filter | no (default 0) | INT 0-3 | retry_count >=3 → status permanently_failed | (readonly) | B9 row 29 |
created_at | DateTime | cột | drawer | (audit log) | date range picker | yes (auto) | DD/MM/YYYY HH:mm:ss | Mặc định filter "7 ngày gần nhất" | (readonly) | B9 row 30 |
processed_at | DateTime nullable | cột (— nếu NULL) | drawer | (audit log) | date range filter | no | DD/MM/YYYY HH:mm:ss hoặc — | NULL nếu chưa process (ingested/received/processing) | (readonly) | B9 row 31 |
error_message | Text nullable | cột truncate | drawer full text | (audit log) | text search | no | TEXT (max ~2KB hiển thị) | Chỉ có khi dead_letter/permanently_failed | (readonly) | B9 row 32 |
raw_payload | JSON | (không cột list) | drawer JSON viewer formatted | (audit log) | — | yes (auto) | JSON object indent 2 | "Sao chép JSON" button + collapse nested | (readonly) | B9 row 33 |
raw_headers | JSON | (không cột list) | drawer JSON viewer | (audit log) | — | yes (auto) | JSON object | (readonly) | (readonly) | B9 row 34 |
source_ip | INET | (không list) | drawer | (audit log) | — | yes (auto) | IPv4/IPv6 | Resolve qua X-Forwarded-For | (readonly) | B9 row 35 |
latency_p95 (derived) | Calculated (s) | SCR-04 metric card | — | — | — | N/A | 12,5s (1 dec) | FORMULA-001 PRD A10 | (calculated) | B9 row 36 |
B0.4.D) Tab 3 Filter bar + Replay action
| Field | Loại | List | Detail/Form | Export | Search/Filter | Required | Format | Business rule | Error copy | Tooltip ref |
|---|---|---|---|---|---|---|---|---|---|---|
filter_date_range | Date range | — | SCR-03 picker (từ — đến) | — | (chính nó) | yes | DD/MM/YYYY half-open | Mặc định 7 ngày gần nhất; max range 90 ngày (G11.1) | "Khoảng thời gian tối đa 90 ngày. Vui lòng thu hẹp." | B9 row 37 |
filter_status | Enum 12 | — | SCR-03 multi-select | — | (chính nó) | no | enum 12 | Default "Tất cả" | (no error) | B9 row 15-26 |
filter_source | FK | — | SCR-03 multi-select sources | — | (chính nó) | no | UUID list | Default "Tất cả" | (no error) | B9 row 9 |
filter_is_test | Bool | — | SCR-03 checkbox "Bao gồm test events" | — | (chính nó) | no | enum {bao gồm, ẩn} | Default "ẩn" (DEC-009 không pollute) | (no error) | B9 row 28 |
replay_action | Action | — | SCR-03 button row hoặc bulk | — | — | conditional (chỉ status dead_letter hoặc auth_failed) | — | DEC-015 layer 4 + DEC-021; bulk rate limit 5/s, batch ≤100 | "Không thể replay event với trạng thái {status}. Chỉ áp dụng cho 'Lỗi DLQ' hoặc 'Lỗi xác thực'." | B9 row 38 |
B0.4.E) Tab 4 Health (SCR-04)
| Field | Loại | List | Detail/Form | Export | Search/Filter | Required | Format | Business rule | Error copy | Tooltip ref |
|---|---|---|---|---|---|---|---|---|---|---|
event_success_24h | Calculated (%) | metric card 24h | — | — | — | N/A | 99,950% (3 dec) | FORMULA-004 PRD A10 | (calculated) | B9 row 39 |
event_success_7d | Calculated (%) | metric card 7d | — | — | — | N/A | 99,9XX% (3 dec) | FORMULA-004 7-day window | (calculated) | B9 row 39 |
latency_p95 | Calculated (s) | metric card | — | — | — | N/A | 12,5s (1 dec) | FORMULA-001 | (calculated) | B9 row 36 |
outage_state | Enum 3 | banner + card | — | — | — | yes (auto) | enum {healthy, outage_started, outage_recovered} | LIFECYCLE-002 | (badge) | B9 row 40-42 |
last_event_received_at | DateTime | metric card | — | — | — | yes (auto) | DD/MM/YYYY HH:mm:ss (ago N phút) | NULL nếu chưa nhận event nào | "Chưa có event nào nhận được" | B9 row 43 |
outage_history | List | table rows (10 gần nhất) | drill-down detail | (audit) | date range | no | row {started_at, ended_at, duration_min} | LIFECYCLE-002 transitions | (readonly) | B9 row 44 |
current_polling_interval | Int (s) | text | — | — | — | yes (auto) | 60-900s | Cron 6 adaptive; start 60s, max 900s | (readonly) | B9 row 45 |
auto_assign_correct_rate | Calculated (%) | metric card | — | — | — | N/A | 92,00% (2 dec) | FORMULA-003 PRD A10 | (calculated) | B9 row 46 |
Quy tắc B0.4: mọi cell có giá trị hoặc
—/N/A; tất cả error copy tiếng Việt; tất cả tooltip ref trỏ đến B9 row tương ứng.
B0.5) Ma trận State × Screen (≥6 state — BẮT BUỘC)
| SCR + Variant | Loading | Has-data | Empty-no-data | Empty-filter | Error | No-permission |
|---|---|---|---|---|---|---|
| SCR-01 Connection (Admin) | Skeleton form 5 row + spinner button "Kiểm tra kết nối" | Form filled với workspace_id + api_key mask + URL copy + toggle + tag list | Form rỗng + helper text "Nhập workspace + api_key để bắt đầu thiết lập" + CTA disabled "Kiểm tra kết nối" cho tới khi điền | N/A vì không có filter | Banner đỏ "Không thể lưu cấu hình. Mã sự cố: {trace_id}. Vui lòng báo Ops." + nút Thử lại | N/A — toàn route ẩn (xem hàng cuối) |
| SCR-01 (Manager / Telesale / POS) | N/A vì màn không hiển thị | N/A | N/A | N/A | N/A | Hidden completely — menu Sidebar Settings/Pancake không render; gõ URL → redirect /403 (toast "Bạn không có quyền truy cập Tích hợp Pancake CRM") |
| SCR-02 Source routing (Admin) | Skeleton table 5 row + button "Đồng bộ" loading state | Table với 40 source (FB Diva HCM Q1, Zalo OA, ...) + inline branch dropdown + toggle | "Chưa có nguồn nào. Click 'Đồng bộ từ Pancake' để bắt đầu." + CTA primary "Đồng bộ từ Pancake" + helper "Yêu cầu Tab 1 đã thiết lập kết nối thành công" | "Không có nguồn nào phù hợp với bộ lọc. Xóa bộ lọc?" + CTA "Xóa bộ lọc" | Banner "Không thể tải danh sách nguồn. Hiển thị bản cache từ {timestamp}." + Thử lại | Hidden completely |
| SCR-02 (Manager / Telesale) | N/A | N/A | N/A | N/A | N/A | Hidden — tab không hiển thị |
| SCR-03 Audit + DLQ (Admin) | Skeleton table 10 row + skeleton drawer rỗng | Table với 100 event/page + pagination + drawer detail khi click row | "Chưa nhận event nào trong khoảng {date_range}." + CTA "Mở rộng khoảng thời gian" | "Không có event nào khớp bộ lọc. {N} event tổng trong khoảng." + CTA "Xóa bộ lọc" | "Không thể tải audit log. {error}. Mã sự cố: {trace_id}." + Thử lại | Hidden completely |
| SCR-03 (Bulk replay) | Progress bar "Đang phát lại {X}/{N} event..." + spinner per row | Sau xong: banner xanh "Đã phát lại {N} event thành công. {Y} event lỗi: [Xem chi tiết]" | N/A | N/A | "Tỉ lệ phát lại lỗi cao bất thường ({Y}/{N}). Liên hệ Tech Lead." + button hủy bulk | Button replay hidden nếu thiếu replay_dlq |
| SCR-04 Health (Admin) | Skeleton cards (4 ô) + spinner chart | Metric cards filled + chart trend + outage history table | "Chưa có dữ liệu trong khoảng {timeframe}." | N/A vì không có filter (chỉ timeframe toggle 24h/7d) | "Không thể tải metric. Mã sự cố: {trace_id}." + Thử lại | Hidden completely |
| SCR-04 (Trạng thái outage) | (vẫn render cards với data cuối cùng + banner) | Banner đỏ "Pancake outage {duration_min} phút. Đã chuyển sang adaptive polling." | N/A | N/A | (banner đỏ luôn) | Hidden completely |
| SCR-05 Container (Admin) | Skeleton tab strip + spinner content | XDetailLayout 4 tabs + RouterView active tab | (delegate to each tab) | (delegate) | (delegate) | Hidden completely (route guard redirect /403) |
| SCR-06 Dropdown delta (mọi role có ticket_management.access) | Loading dropdown (skeleton option) | Dropdown 9 source: "Nguồn 1, ..., Nguồn 8, Pancake CRM" + hover tooltip | N/A (luôn có 9 option) | (filter dropdown empty result) "Không có nguồn nào khớp" | "Không tải được danh sách nguồn. Thử lại." | Dropdown hidden hoàn toàn cho user không có ticket_management.access |
6 state canonical đã cover toàn bộ. Mọi cell có giá trị (không có cell trống). Non-applicable đã ghi
N/A vì <lý do>.
B0.6) Wireframe Quality Contract
| Quy tắc | Tuân thủ |
|---|---|
| Số cột bảng ASCII | Header và data row cùng số │, không lệch quá 1 ký tự |
| Stepper labels | (N/A — feature không có wizard nhiều bước) |
| Dữ liệu mẫu | Dùng tên KH VN ("Nguyễn Thị Mai", "Trần Văn Hùng"), SĐT bắt đầu 09xxx/03xxx, branch name realistic ("Diva HCM Q1", "Diva Hà Nội Cầu Giấy", "Diva Đà Nẵng"), source name Pancake realistic ("FB Diva HCM Q1", "Zalo OA Diva Premium", "FB Bệnh viện Da liễu Diva", "Google Ads Diva Brand") |
| Không TBD / Lorem | Toàn bộ wireframe có copy thực tế |
| Không hard-code style | KHÔNG dùng cam đỏ, font 12px, emoji trong copy; dùng intent token warning/success/muted/negative-value/primary emphasis |
| Buttons label | Mọi button trong wireframe có dòng tương ứng ở B7 từ điển copy |
| Truncation | Cell dài → ghi … cuối hoặc xuống dòng có ↵ |
| Mobile preview | Diva admin desktop-first; SCR-01..04 desktop only — không cần mobile wireframe riêng (B8) |
B0.7) Cặp Code ↔ Display VI (Bilingual Pairing)
Áp dụng cho feature này vì có ≥3 enum (lifecycle 12 states + outage 3 states + connection_status 4 + event_type 3).
| Code | Display VI canonical | Phân biệt với | Dùng ở section |
|---|---|---|---|
ingested | "Đã nhận (chưa xác thực)" | "Đã xác thực" (received) | B6 lifecycle, B2.3 wireframe SCR-03 badge |
received | "Đã xác thực" | "Đã xử lý" (processed), "Đã nhận" (ingested) | B6 lifecycle |
processing | "Đang xử lý" | "Đã xử lý" (processed) | B6 lifecycle |
processed | "Đã xử lý" | "Đang xử lý" (processing) | B6 lifecycle |
auth_failed | "Lỗi xác thực" | "Lỗi IP" (ip_blocked), "Lỗi cấu hình" (parse_error) | B6 lifecycle |
ip_blocked | "Bị chặn IP" | "Lỗi xác thực" (auth_failed) | B6 lifecycle |
parse_error | "Lỗi cấu trúc dữ liệu" | "Lỗi xác thực" (auth_failed) | B6 lifecycle |
skipped_duplicate | "Bỏ qua (trùng lặp)" | "Bỏ qua (đã tắt nguồn)" (skipped_source_disabled) | B6 lifecycle |
skipped_source_disabled | "Bỏ qua (nguồn đã tắt)" | "Bỏ qua (đã tắt hệ thống)" (skipped_kill_switch) | B6 lifecycle |
skipped_kill_switch | "Bỏ qua (đã tắt hệ thống)" | "Bỏ qua (nguồn đã tắt)" | B6 lifecycle |
skipped_opt_out | "Bỏ qua (khách từ chối marketing)" | (no near duplicate) | B6 lifecycle |
dead_letter | "Lỗi DLQ (chờ phát lại)" | "Lỗi vĩnh viễn" (permanently_failed) | B6 lifecycle |
permanently_failed | "Lỗi vĩnh viễn (cần xem xét)" | "Lỗi DLQ (chờ phát lại)" | B6 lifecycle |
healthy | "Khỏe mạnh" | "Đang phục hồi" (outage_recovered) | B6 outage |
outage_started | "Mất kết nối" | "Khỏe mạnh" (healthy) | B6 outage |
outage_recovered | "Đang phục hồi" | "Khỏe mạnh" (healthy) | B6 outage |
active | "Đang hoạt động" (connection) | "Tạm dừng" (paused) | B6 connection_status |
paused | "Tạm dừng" | "Đang hoạt động" (active), "Bị Pancake khóa" (suspended_by_pancake) | B6 connection_status |
suspended_by_pancake | "Bị Pancake khóa" | "Tạm dừng" (paused, do admin) | B6 connection_status |
error | "Lỗi" (connection) | (chỉ context kết nối) | B6 connection_status |
record | "Sự kiện record" | "Đối soát" (reconciled), "Polling" (polling_injected) | B6 event_type |
reconciled | "Đối soát (Cron 7)" | "Polling (Cron 6)" (polling_injected) | B6 event_type |
polling_injected | "Polling (Cron 6)" | "Đối soát (Cron 7)" | B6 event_type |
Quy tắc: trong văn xuôi UI Spec dùng
Đã xác thực (received)lần đầu, sau đó chỉĐã xác thực. Trong Mermaid stateDiagram giữ code gốc (ingested → received). Trong dropdown filter SCR-03 hiển thị display VI; tooltip hover hiển thị code backtick để dev đối chiếu.
B0.8) Đối soát schema (Schema Cross-Check)
| Field UI cho phép NULL / blank | Cột schema | NOT NULL? | Cách UI xử lý |
|---|---|---|---|
diva_branch_id ở SCR-02 (Loose mode) | pancake_source_routing.diva_branch_id | NULL allowed (DEC-013 Loose mode) | UI cho phép dropdown rỗng + tooltip "Để trống = Loose mode, ticket sẽ tạo với chi nhánh NULL, leader assign sau" |
assignee_id của ticket Pancake | ticket.assignee_id | NULL allowed | (không thuộc UI Spec này — ticket auto-create từ webhook, render trong CRM existing) |
pancake_connection.vip_tag_names rỗng | pancake_connection.vip_tag_names text[] DEFAULT '{}' | NULL allowed (default empty) | UI textarea cho phép trống; nếu trống → không match VIP tag (DEC-008) |
error_message ở Tab 3 drawer | pancake_webhook_event.error_message | NULL allowed (chỉ có khi status dead_letter/permanently_failed) | Drawer hiển thị — nếu NULL |
processed_at ở Tab 3 | pancake_webhook_event.processed_at | NULL allowed (chỉ có khi status processed) | Cột hiển thị — nếu NULL; KHÔNG hiển thị 00:00:00 |
Lưu ý: không có field UI cho phép trống mà schema yêu cầu NOT NULL trong feature này. Tất cả trường required đã validate ở B0.4 cột
Required.
B0.9) Bảng kiểm kê tương tác (Interactive Element Inventory)
BẮT BUỘC M+ — liệt kê 100% interactive elements per SCR. Cross-ref từ B2.7E.
SCR-01 Connection
| SCR | Element | Loại tương tác | Behavior summary | Ref rule | State coverage |
|---|---|---|---|---|---|
| SCR-01 | Field api_key input password | Inline edit | Click → reveal/edit; mask ●●●●●● mặc định; hover icon mắt → reveal 5s rồi tự mask | B2.7E rule 6 | default+focused+revealed+error |
| SCR-01 | Icon mắt reveal/hide password | Secondary CTA | Toggle reveal API key; aria-label "Hiện/Ẩn API key"; reveal kèm banner "Đang hiển thị API key — không chia sẻ màn hình" | B2.7 | default+revealed+hover |
| SCR-01 | Button "Kiểm tra kết nối" | Primary CTA | Call Pancake /sources REST → hiển thị list sources detect hoặc error | B2.7 | default+loading+success+error+disabled (nếu workspace/api_key chưa điền) |
| SCR-01 | Icon copy URL webhook | Secondary CTA | Click → copy URL vào clipboard + toast "Đã sao chép URL webhook"; aria-label "Sao chép URL webhook" | B2.7 | default+hover+success-flash |
| SCR-01 | Icon copy token webhook | Secondary CTA | Click → copy token + toast; mask ●●●●●●●● mặc định, reveal toggle riêng | B2.7 | default+revealed+hover |
| SCR-01 | Button "Tạo lại token" | Destructive CTA | Confirm modal 2 bước "Token mới sẽ vô hiệu hóa cấu hình Pancake hiện tại. Gõ 'TẠO LẠI' để xác nhận." | B2.7 + B2.10 | default+confirm-modal+loading+success |
| SCR-01 | Toggle kill switch | Secondary CTA | Toggle on/off → confirm modal "Tắt sẽ ngừng nhận lead từ Pancake (mọi event sẽ chuyển trạng thái 'Bỏ qua (đã tắt hệ thống)'). Xác nhận?" | B2.7 + B2.10 | default+confirm+loading+success |
| SCR-01 | Textarea VIP tag names | Inline edit | Multi-line input; debounce 500ms; char counter {X}/100 dòng; trim+lowercase khi save | B2.7E rule 6 | default+focused+over-limit-warning+error |
| SCR-01 | Button "Lưu cấu hình" | Primary CTA | Explicit save toàn form; disabled nếu chưa thay đổi (form pristine) | B2.7 | default+loading+success+disabled-pristine+error |
| SCR-01 | Button "Kích hoạt lại webhook" (chỉ hiện khi status=suspended_by_pancake) | Primary CTA | Reset status → active; gọi Pancake re-enable; alert toast khi xong | B2.7 | hidden-by-default+visible-on-suspended+loading+success+error |
SCR-02 Source routing
| SCR | Element | Loại tương tác | Behavior summary | Ref rule | State coverage |
|---|---|---|---|---|---|
| SCR-02 | Button "Đồng bộ từ Pancake" | Primary CTA | Manual call Cron 3 logic; loading spinner; toast "Đã đồng bộ {N} nguồn ({M} nguồn mới)" | B2.7 | default+loading+success+error+disabled (nếu connection chưa active) |
| SCR-02 | Click row trong table | Drill-down | Click → drawer phải mở chi tiết source (lịch sử event 7d, KPI, raw metadata Pancake) | B2.7E rule 1 | default+hover+selected+disabled-deleted |
| SCR-02 | Inline edit cột Diva branch (dropdown) | Inline edit | Click cell → BranchSelect dropdown; autosave debounce 500ms; toast "Đã cập nhật mapping cho '{source_name}'" | B2.7E rule 6 | default+editing+saving+saved+error-conflict |
| SCR-02 | Inline toggle cột is_active | Inline edit | Toggle ngay → autosave; toast confirm; nếu off thì warn "Tắt nguồn sẽ ngừng nhận event từ '{source_name}'" cho khả năng undo 10s | B2.7E rule 6 + B2.14 | default+toggling+saved+undo-pending |
| SCR-02 | Text search "Tìm nguồn theo tên" | Search | Debounce 300ms client-side filter; min 2 ký tự; clear icon × | B2.13 | default+typing+filtered+cleared |
| SCR-02 | Filter dropdown Trạng thái | Filter | enum {Tất cả, Đang bật, Đang tắt}; reset trigger filter cho table | B2.5 | default+open+selected |
| SCR-02 | Bulk select checkbox header | Bulk action (defer MVP-2) | Hidden MVP-1 (DEC-022); placeholder cho MVP-2 bulk import CSV | (N/A MVP-1) | hidden-MVP-1 |
SCR-03 Audit + DLQ
| SCR | Element | Loại tương tác | Behavior summary | Ref rule | State coverage |
|---|---|---|---|---|---|
| SCR-03 | Date range picker filter | Filter | Default "7 ngày gần nhất"; max 90 ngày; auto-apply on change | B2.5 | default+open+applied+error-range-too-wide |
| SCR-03 | Multi-select status dropdown | Filter | 12 status; default "Tất cả"; auto-apply | B2.5 | default+open+selected+cleared |
| SCR-03 | Multi-select source dropdown | Filter | Sources mapped; default "Tất cả"; auto-apply | B2.5 | default+open+selected |
| SCR-03 | Checkbox "Bao gồm test events" | Filter | Default unchecked (DEC-009); auto-apply | B2.5 | default+checked+unchecked |
| SCR-03 | Button "Xóa bộ lọc" | Secondary CTA | Reset toàn bộ filter về default | B2.7 | default+hover+disabled (khi filter pristine) |
| SCR-03 | Click row event | Drill-down | Mở drawer phải chi tiết event (raw_payload, raw_headers, error_message, lifecycle trace) | B2.7E rule 1 | default+hover+selected+drawer-open |
| SCR-03 | Hover row → action icons (replay, copy json) | Hover state | Opacity 0 → 1 transition 200ms; action chỉ enable cho status dead_letter/auth_failed/parse_error (replay) hoặc luôn enable (copy) | B2.7E rule 3 | default+hover+actions-visible+keyboard-focus |
| SCR-03 | Button "Phát lại sự kiện" (single row) | Primary CTA | Confirm modal "Phát lại event {event_id}? Status sẽ chuyển '{current_status}' → 'Đã xác thực'." | B2.7 + B2.10 | default+confirm+loading+success+error |
| SCR-03 | Checkbox bulk select per row | Bulk action | Chỉ enable cho status replayable (dead_letter, auth_failed); selected count sticky bar | B2.14 + EXT-13 | default+selected+disabled-non-replayable |
| SCR-03 | Bulk action bar "Phát lại {N} event" | Bulk CTA | Confirm "Phát lại {N} event? Sẽ chạy với rate 5 event/giây (~{time} giây)." | B2.14 + EXT-13 | default+confirm+progress+success+partial-fail |
| SCR-03 drawer | Button "Sao chép JSON payload" | Secondary CTA | Click → copy raw_payload JSON formatted vào clipboard + toast | B2.7 | default+hover+success-flash |
| SCR-03 drawer | Tab strip "Payload / Headers / Lịch sử trạng thái" | Tab switch | Lazy load mỗi tab; URL hash ?tab=payload/headers/history | B2.7E rule 7 | default+active+loading-per-tab |
| SCR-03 drawer | Button "Xem các event cùng record_id" | Drill-down | Set filter record_id=current + close drawer | B2.7E rule 1 | default+hover |
| SCR-03 | Pagination | Load more | Default 100 event/page; cho phép 50/100/200; total count | B2.7E rule 5 | default+loading+exhausted |
SCR-04 Health
| SCR | Element | Loại tương tác | Behavior summary | Ref rule | State coverage |
|---|---|---|---|---|---|
| SCR-04 | Toggle timeframe 24h / 7d | Tab switch | Re-query metrics; aria-selected; URL hash ?timeframe=24h|7d | B2.7E rule 7 | default+active+loading |
| SCR-04 | Hover icon ⓘ cho mỗi metric | Hover state | Tooltip B9 3-part: định nghĩa + công thức + ví dụ | B2.7E rule 8 | default+hover-show |
| SCR-04 | Click metric card | Drill-down | Click "Tỉ lệ thành công" → mở SCR-03 với filter status non-success | B2.7E rule 1 | default+hover+pointer |
| SCR-04 | Banner outage (khi outage_started) | Notification | Banner đỏ sticky top + button "Xem chi tiết" + button "Tạm tắt cảnh báo (1h)" | B2.7 + B-Voice | always-visible-during-outage+hover-dismiss |
| SCR-04 | Outage history table row click | Drill-down | Drawer detail outage event với timeline (started/recovered) + count missing events injected | B2.7E rule 1 | default+hover+selected |
| SCR-04 | Button "Tải báo cáo PDF" (defer MVP-2) | Secondary CTA (hidden MVP-1) | placeholder | (N/A MVP-1) | hidden-MVP-1 |
SCR-05 Container
| SCR | Element | Loại tương tác | Behavior summary | Ref rule | State coverage |
|---|---|---|---|---|---|
| SCR-05 | Tab strip 4 tabs | Tab switch | Click → router-view child render; URL /settings/pancake-crm/{connection|source-routing|audit|health} | B2.7E rule 7 | default+active+keyboard-arrow-nav |
SCR-06 Dropdown delta (3 màn existing)
| SCR | Element | Loại tương tác | Behavior summary | Ref rule | State coverage |
|---|---|---|---|---|---|
| SCR-06 | Dropdown Nguồn ticket (Tickets list filter) | Filter | Render 9 option; option mới "Pancake CRM" với tooltip mô tả; multi-select | B2.5 | default+open+selected+cleared |
| SCR-06 | Dropdown Nguồn (Form Tạo Ticket) | Inline edit | Render 9 option trong form drawer | B2.7E rule 6 | default+open+selected |
| SCR-06 | Dropdown drawer KH (CustomerTicketManager) | Inline edit | Render 9 option khi mở form Tạo Ticket từ drawer KH | B2.7E rule 6 | default+open+selected |
| SCR-06 | Hover icon ⓘ tooltip "Pancake CRM" | Hover state | Hover → popup "Lead từ Pancake CRM (40+ kênh: FB, Zalo, TikTok, Google Ads, Whatsapp, Shopee...)" | B2.7E rule 8 | default+hover-show |
Tổng kết B0.9: 50+ interactive elements được liệt kê đầy đủ. 10 loại tương tác canonical đều được dùng. Mọi element có ref B2.7/B2.7E rule và state coverage.
B1) Bản đồ màn hình và hành trình
B1.1) Danh sách màn
| SCR | Tên | Route | Loại | Mô tả |
|---|---|---|---|---|
| SCR-01 | Kết nối Pancake | /settings/pancake-crm/connection | 🆕 Build mới | Form workspace + api_key + webhook URL/token + kill switch + VIP tag |
| SCR-02 | Map nguồn → chi nhánh | /settings/pancake-crm/source-routing | 🆕 Build mới | Table source ↔ Diva branch + is_active toggle |
| SCR-03 | Lịch sử event + DLQ | /settings/pancake-crm/audit | 🆕 Build mới | Audit log + filter + drawer detail + replay action |
| SCR-04 | Sức khỏe & cảnh báo | /settings/pancake-crm/health | 🆕 Build mới | Metrics 24h/7d + outage history + chart trend |
| SCR-05 | Container Tích hợp Pancake CRM | /settings/pancake-crm (default → /connection) | ✅ Reuse XDetailLayout | Tabs container + RouterView |
| SCR-06 | Dropdown ticket source (3 màn existing) | /crm/tickets, /crm/tickets drawer, /customers/:id drawer | 🔧 Extend (FE delta) | 9 source thay vì 8 — auto render từ TicketSources array sau 3 file delta |
B1.2) Hành trình end-to-end
B1.3) Hành trình theo vai trò
| Vai trò | Entry point | Đường màn | Kết quả đầu ra |
|---|---|---|---|
| Admin (setup lần đầu) | Sidebar Settings → "Tích hợp Pancake CRM" | SCR-05 → SCR-01 (nhập config + Kiểm tra kết nối) → SCR-02 (review + map branch) → SCR-04 (verify health) | Connection active, ≥1 source mapped, webhook URL configured ở Pancake admin |
| Admin (vận hành hàng ngày) | Sidebar Settings → "Tích hợp Pancake CRM" | SCR-05 → SCR-04 (xem health metric) → SCR-03 (nếu có dead_letter event → replay) | Event success ≥99,5%, zero DLQ tồn đọng |
| Admin (xử lý sự cố outage) | Notification push "Pancake outage X phút" → click → SCR-04 | SCR-05 → SCR-04 (banner đỏ + detail) → (đợi auto-recovery hoặc gọi Pancake support) | Outage recovered, MTTR ≤5 phút |
| Admin (thay đổi mapping) | Sidebar Settings → "Tích hợp Pancake CRM" | SCR-05 → SCR-02 (inline edit branch dropdown 1 row) | Source mới được route sang branch khác |
| Telesale | Notification push "Bạn vừa nhận lead Pancake CRM: {KH}" | /crm/tickets (existing) → filter source = "Pancake CRM" (SCR-06) → click ticket → drawer detail | Phản hồi khách trong 30 phút |
| Manager | /crm/tickets → filter chi nhánh + source = "Pancake CRM" (SCR-06) | (existing flow) → leader assign manual cho ticket branch=NULL | Branch coverage ≥90% |
| Non-admin (Manager/Telesale/POS) | Gõ URL /settings/pancake-crm trực tiếp | Redirect /403 + toast | Không truy cập Settings/Pancake |
B1.4) Phạm vi thay đổi tối thiểu (SCR-06 delta on existing)
| Màn hiện có | Bằng chứng | Thay đổi | Vị trí update | Vì sao đủ |
|---|---|---|---|---|
Tickets.tsx filter dropdown | Tickets.tsx:851-865 TicketSources.map(...) | Render slot 9 tự động | Không cần code change ngoài types.ts + i18n/vi.ts + sourceDescriptions (line 128-149) | Dropdown auto-render từ array; tooltip cần add entry |
TicketMultipleAdd.tsx:758 | Dropdown loop từ TicketSources | Render slot 9 tự động | Không cần code change | Auto từ types.ts |
CustomerTicketManager.tsx | reuse pattern | Render slot 9 tự động | Không cần code change | Auto |
| Module report (customer-service, telesales) | TBD-verify | Có thể cần delta nếu hardcode slot 1..8 | Phase 4 verify (RSK-005) | Out-of-band grep audit |
B2) Inventory màn — Canonical Interaction Rules
B2.7E) Quy ước tương tác phụ — Canonical Rules (Secondary Interactions Contract)
Canonical 8 rules — mọi
B2.SCR-XX.7Eper-SCR đều ref đến rule trong section này. B0.9 Interactive Inventory cũng refB2.7E rule N.
| # | Loại tương tác | Định nghĩa canonical | Behavior chuẩn | Áp dụng SCR |
|---|---|---|---|---|
| Rule 1 | Drill-down (row click) | Click row trong table mở drawer detail bên phải overlay (40% viewport) | (1) Single-click row → mở drawer. (2) ESC + click backdrop → đóng drawer. (3) Drawer read-only → không có draft state. (4) URL update với query param ?detail={id} cho deeplink. (5) Focus trap trong drawer (a11y). | SCR-02 (source detail), SCR-03 (event detail) |
| Rule 2 | Expand-Collapse | Mở rộng/thu gọn row trong table hoặc card | (1) Click chevron ▶/▼ toggle. (2) Animation 200ms ease-out. (3) State lưu localStorage để remember across reload. (4) Keyboard: Enter/Space toggle khi focus chevron. | SCR-04 (outage history detail), SCR-03 (group by record_id) |
| Rule 3 | Hover-action | Action icon ẩn → hiện khi hover row | (1) Opacity 0 → 1 transition 200ms. (2) Keyboard focus = hover (a11y). (3) Action chỉ enable theo condition (vd "Phát lại" chỉ enable cho status dead_letter/auth_failed/parse_error). (4) Trên touch device (tablet) → luôn visible. | SCR-02 (row actions), SCR-03 (replay + copy_json hover) |
| Rule 4 | Context menu | Right-click row hoặc click icon ⋮ mở menu | (1) Position auto-flip nếu sát viewport edge. (2) ESC + click outside → đóng. (3) Keyboard: Arrow + Enter navigate. (4) Items có separator nếu nhóm. (5) MVP-1 không dùng (preferred: visible buttons) — defer M2. | (defer M2) |
| Rule 5 | Load more / Pagination | Mở rộng kết quả khi danh sách dài | (1) Server-side pagination 20 row/page (mặc định). (2) Page size selector [10, 20, 50, 100]. (3) Total count hiển thị "Hiển thị {X}-{Y} / {Total}". (4) Keyboard: PgUp/PgDn navigate page. (5) URL update ?page=N&size=M. (6) Loading skeleton trên page change. | SCR-02 (sources pagination), SCR-03 (events pagination) |
| Rule 6 | Inline edit | Edit field tại chỗ không cần modal | (1) Click cell → field input render in-place. (2) Autosave debounce 500ms (cho dropdown), 1s (cho text input). (3) Save indicator inline: spinner small → checkmark 2s. (4) ESC → revert. (5) Tab/Enter → save + move next. (6) Concurrency: optimistic UI, conflict warning nếu server reject (LWW hoặc version-based). (7) Permission check ở client trước, server enforce. | SCR-01 (api_key, vip_tag_names textarea), SCR-02 (branch dropdown + is_active toggle) |
| Rule 7 | Tab switch | Chuyển tab trong tab-strip | (1) Lazy load content per tab (chỉ render tab active). (2) URL hash #tab=name để deeplink. (3) Tab indicator active highlight + aria-current. (4) Keyboard: Arrow Left/Right navigate, Tab key vào content. (5) Unsaved changes warning nếu user chuyển tab (link Pinia draft state). | SCR-05 (4 tab Settings), SCR-03 drawer (Payload/Headers/Lịch sử trạng thái tabs) |
| Rule 8 | Overlay (modal/drawer/popover) | Layer trên content chính | (1) Modal (center, blocking): confirm, form mới — ESC + click backdrop close (trừ destructive action). (2) Drawer (side panel 40% viewport, non-blocking edit): detail view, edit form — ESC + click backdrop close. (3) Popover (anchor, dismissible): tooltip, dropdown menu — click outside close. (4) Focus trap trong layer. (5) Stack max 2 (modal trong drawer OK, drawer trong modal KHÔNG). (6) z-index canonical: modal=2000, drawer=1500, popover=1000. | SCR-01 (confirm modal kill switch), SCR-02 (drawer source detail), SCR-03 (drawer event detail + replay confirm modal), SCR-04 (outage detail drawer) |
Quy tắc áp dụng:
- Mỗi
B0.9interactive element có cộtRef rule→ trỏ đếnB2.7E rule Ntrong section này (không phải per-SCR section) - Per-SCR section
B2.SCR-XX.7Echỉ liệt kê các tương tác SPECIFIC cho SCR đó, không repeat full definition - Conflict giữa rule canonical (B2.7E) vs per-SCR → ưu tiên per-SCR (vì có context cụ thể)
B2.SCR-01) Kết nối Pancake
B2.SCR-01.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Admin role (system-wide); module pancake_crm_integration.access/update |
| Vào màn để quyết định gì? | (1) Thiết lập lần đầu workspace_id + api_key Pancake. (2) Lấy URL + token webhook để paste vào Pancake admin. (3) Bật/tắt kill switch. (4) Quản lý danh sách VIP tag NAME. (5) Re-enable webhook khi bị Pancake suspend. |
| Dữ liệu chính | pancake_connection row duy nhất (single workspace MVP-1) + app_setting.pancake_integration.enabled |
| CTA chính / phụ | Primary: Lưu cấu hình (explicit save toàn form). Secondary: Kiểm tra kết nối, Sao chép URL, Sao chép token, Tạo lại token, Kích hoạt lại webhook (conditional) |
| Điều không được hiểu nhầm | "Kiểm tra kết nối" = test REST API call → KHÔNG có nghĩa "webhook đã active". "Tạo lại token" = invalidate cấu hình Pancake hiện tại, ops phải update Pancake admin sau. "Kill switch" = mức 1 (tổng) — tắt KHÔNG xóa cấu hình. "VIP tag" = NAME case-insensitive, KHÔNG phải tag ID. |
B2.SCR-01.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | Admin biết: connection có đang active không, URL webhook là gì, có kill switch đang bật/tắt |
| Thứ tự ưu tiên thông tin | (1) Connection status badge top-right, (2) Workspace + api_key (config tổng), (3) Webhook URL + token (cần copy sang Pancake), (4) Kill switch toggle (vận hành), (5) VIP tag list (nghiệp vụ) |
| Dữ liệu nhạy cảm | api_key + webhook_token mask ●●●●●●●● mặc định; reveal toggle hover icon mắt + banner cảnh báo "Đang hiển thị — không chia sẻ màn hình"; field KHÔNG có quyền update (Manager nếu future) → readonly với value masked qua API (không leak qua DOM) |
| Pending/partial data | Lần đầu vào: tất cả field rỗng, Kiểm tra kết nối disabled; status error (DB chưa có row). Sau "Lưu cấu hình" lần đầu → status active |
| Thuật ngữ không được đổi | "Workspace ID" (giữ EN vì Pancake terminology), "API key" (giữ), "Webhook URL", "Webhook token", "Kill switch" → đặt label tiếng Việt "Tắt khẩn cấp toàn hệ thống", "VIP tag (theo tên)" (giải thích "tên" để phân biệt tag ID) |
| Thiết kế thị giác | Dùng intent primary emphasis cho "Lưu cấu hình"; negative-value cho "Tạo lại token" (destructive); success badge cho active; warning cho paused; negative cho suspended_by_pancake/error |
| Không được tự suy diễn | KHÔNG thêm action "Xóa connection" (xóa qua DB migration only, không qua UI MVP-1). KHÔNG ẩn field api_key cho Admin (RBAC field-level reject). |
B2.SCR-01.2) Ma trận variant
| Variant ID | Điều kiện kích hoạt | Ai thấy | Wireframe | Field/block không render | QA ref |
|---|---|---|---|---|---|
Variant A | Default — connection.status='active' | Admin | Có (B2.SCR-01.3.A) | Banner "Kích hoạt lại webhook" ẩn | TC-UI-SCR01-001 |
Variant B | First-time setup — chưa có row pancake_connection | Admin | Có (B2.SCR-01.3.B) | Field webhook_url + webhook_token mask (chưa gen); badge status ẩn | TC-UI-SCR01-002 |
Variant C | No permission | Manager/Telesale/POS | Hidden completely — menu Sidebar không render; URL gõ trực tiếp → /403 | Toàn bộ màn | TC-PERM-SCR01-001 |
Variant D | Loading | Admin | Có (B2.SCR-01.3.D) — skeleton form 5 row | N/A | TC-UI-SCR01-003 |
Variant E | Error save | Admin | Có (B2.SCR-01.3.E) — banner đỏ "Không thể lưu. Mã: {trace_id}" | N/A | TC-UI-SCR01-004 |
Variant F | Suspended — connection.status='suspended_by_pancake' | Admin | Có (B2.SCR-01.3.F) — banner đỏ + button "Kích hoạt lại webhook" hiển thị | Toggle kill switch disabled (vì đã suspended bên Pancake) | TC-UI-SCR01-005 |
B2.SCR-01.3) Wireframes
Variant A — Default (active)
text
/settings/pancake-crm/connection
┌──────────────────────────────────────────────────────────────────────────┐
│ Tích hợp Pancake CRM [Trạng thái: Đang hoạt động ✓] │
│ ────────────────────────────────────────────────────────────────────── │
│ [Tab 1 Kết nối*] [Tab 2 Map nguồn] [Tab 3 Lịch sử + DLQ] [Tab 4 Sức khỏe] │
│ ────────────────────────────────────────────────────────────────────── │
│ │
│ Thông tin workspace Pancake │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Workspace ID * [ws_diva_2026_a8c3d4f5________________] │ │
│ │ API key * [●●●●●●●●●●●●●●●●●●●●] [👁 Hiện 5s] │ │
│ │ Giấy phép Pancake (PD-001) Trust IP + URL token (W1-W3) │ │
│ │ │ │
│ │ [Kiểm tra kết nối] ⓘ Test API GET /sources │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Cấu hình webhook (paste vào Pancake admin) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ URL webhook https://diva.com.vn/api/pancake/record/abc123token │ │
│ │ [📋 Sao chép URL] │ │
│ │ Token webhook [●●●●●●●●●●●●●●●●] [👁 Hiện] [📋 Sao chép] [↻ Tạo lại] │ │
│ │ ⓘ Token xác thực request từ Pancake. Tạo lại sẽ vô hiệu hóa │ │
│ │ cấu hình Pancake hiện tại. │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Tắt khẩn cấp toàn hệ thống (kill switch) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Đang nhận event Pancake [●━━ Bật] │ │
│ │ ⓘ Tắt sẽ chuyển mọi event sang trạng thái "Bỏ qua (đã tắt hệ thống)".│ │
│ │ Pancake không bị suspend (vẫn trả 200). │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Danh sách tag VIP (theo tên, không phân biệt hoa thường) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ⓘ Mỗi dòng = 1 tag. Khi Pancake gửi event chứa tag trùng tên, │ │
│ │ hệ thống coi khách là VIP và luôn tạo ticket dù chỉ update info. │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ VIP │ │ │
│ │ │ Hot Lead │ │ │
│ │ │ Khách lớn │ │ │
│ │ │ Khách quay lại │ │ │
│ │ │ Premium Member │ │ │
│ │ │ │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ 5 / 100 dòng │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Đã chỉnh sửa • Lưu lần cuối: 14/05/2026 09:42 bởi NGUYEN VAN A │
│ │
│ [Hủy thay đổi] [Lưu cấu hình ✓] │
└──────────────────────────────────────────────────────────────────────────┘Không render: button "Kích hoạt lại webhook" (chỉ hiện khi suspended). Recovery/next step: bấm "Lưu cấu hình" → toast xanh "Đã lưu cấu hình Pancake" → ở lại tab; bấm "Kiểm tra kết nối" → loading → toast "Kết nối thành công. Phát hiện {N} nguồn" hoặc banner đỏ "Không thể kết nối. Kiểm tra api_key + thử lại."
Variant B — First-time setup (chưa có connection)
text
/settings/pancake-crm/connection
┌──────────────────────────────────────────────────────────────────────────┐
│ Tích hợp Pancake CRM [Trạng thái: Chưa thiết lập] │
│ ────────────────────────────────────────────────────────────────────── │
│ [Tab 1 Kết nối*] [Tab 2 Map nguồn] [Tab 3 Lịch sử + DLQ] [Tab 4 Sức khỏe] │
│ ────────────────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ℹ️ Chào mừng đến với Tích hợp Pancake CRM │ │
│ │ Nhập thông tin workspace để bắt đầu nhận lead tự động từ Pancake. │ │
│ │ Cần Workspace ID + API key (lấy từ Pancake admin > Tích hợp). │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Thông tin workspace Pancake │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Workspace ID * [_______________________________________] │ │
│ │ API key * [_______________________________________] [👁] │ │
│ │ │ │
│ │ [Kiểm tra kết nối] (Vô hiệu — điền 2 field trên trước) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ⓘ URL + Token webhook sẽ được tự động tạo sau khi lưu thành công. │
│ │
│ [Hủy] [Lưu cấu hình] (Vô hiệu) │
└──────────────────────────────────────────────────────────────────────────┘Không render: kill switch (chưa có row), VIP tag list (sẽ hiện sau lưu), webhook URL/token. Recovery/next step: Admin điền workspace_id + api_key → "Kiểm tra kết nối" enable → bấm test → nếu pass → "Lưu cấu hình" enable → save → chuyển sang Variant A đầy đủ.
Variant D — Loading
text
/settings/pancake-crm/connection
┌──────────────────────────────────────────────────────────────────────────┐
│ Tích hợp Pancake CRM [▓▓▓▓▓ Đang tải]│
│ ────────────────────────────────────────────────────────────────────── │
│ [Tab 1 Kết nối*] [Tab 2 Map nguồn] [Tab 3 Lịch sử + DLQ] [Tab 4 Sức khỏe] │
│ ────────────────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘Variant E — Error save
text
┌──────────────────────────────────────────────────────────────────────────┐
│ ⚠️ Không thể lưu cấu hình. │
│ Mã sự cố: TRC-a8c3d4f5-2026-05-15. Vui lòng báo bộ phận Ops với mã.│
│ [Thử lại] [Đóng] │
└──────────────────────────────────────────────────────────────────────────┘
(banner đỏ sticky top form; form vẫn giữ data user đã điền, không reset)Variant F — Suspended by Pancake
text
┌──────────────────────────────────────────────────────────────────────────┐
│ Tích hợp Pancake CRM [Trạng thái: Bị Pancake khóa] │
│ ────────────────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 🚫 Pancake đã tự động khóa webhook │ │
│ │ Lý do: 80% error / 30 phút (suspension rule). │ │
│ │ Phát hiện lúc: 14/05/2026 14:23 (Cron 4 detect). │ │
│ │ Mọi event mới sẽ KHÔNG được Pancake gửi tới Diva. │ │
│ │ │ │
│ │ Hành động cần làm: │ │
│ │ 1. Vào Pancake admin > Tích hợp > Webhook → Bật lại thủ công │ │
│ │ 2. Sau đó bấm nút bên dưới để cập nhật trạng thái Diva │ │
│ │ │ │
│ │ [Kích hoạt lại webhook ↻] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ (Phần form workspace + tag VIP vẫn render, nhưng toggle kill switch │
│ bị disabled với tooltip "Webhook đang bị Pancake khóa") │
└──────────────────────────────────────────────────────────────────────────┘B2.SCR-01.4) Phân loại reuse + điểm update
| Phân loại | File hiện có | Vị trí update | Lý do vị trí |
|---|---|---|---|
| 🆕 Build mới | src/modules/settings/pages/pancake/PancakeConnection.tsx | Tab 1 trong XDetailLayout của SCR-05 | Đầu tiên — config nền tảng trước các tab khác |
| ✅ Reuse | XInput, XPassword, XToggle, XTextarea, XBadge, XSpinner components | Form fields | Diva component library |
B2.SCR-01.5) Quy ước field / cột / CTA (ref B0.4.A)
Mọi field đã liệt kê chi tiết ở B0.4.A. Section này chỉ ghi quy ước UI cụ thể không nằm trong B0.4.
| Field/CTA | Loại | Hiển thị ở | Mặc định | Validation | Điều kiện hiển thị | Tooltip |
|---|---|---|---|---|---|---|
Kiểm tra kết nối button | Primary CTA | SCR-01 dưới api_key | Disabled khi workspace_id hoặc api_key trống | call REST /sources; timeout 30s | Luôn hiện cho Admin | ref B9 row 47 |
Sao chép URL icon | Secondary CTA | Cạnh webhook_url readonly | Always enabled khi URL đã gen | navigator.clipboard.writeText | Luôn hiện sau setup | ref B9 row 48 |
Tạo lại token button | Destructive CTA | Cạnh webhook_token | Enabled cho Admin | Confirm modal 2 bước "TẠO LẠI" gõ tay | Luôn hiện sau setup | ref B9 row 49 |
Kích hoạt lại webhook button | Primary CTA | Banner Variant F | Enabled chỉ khi status='suspended_by_pancake' | Confirm modal "Đã bật lại trên Pancake chưa?" | Conditional Variant F | ref B9 row 50 |
B2.SCR-01.6) Thanh lọc — N/A (không có filter trên SCR-01)
B2.SCR-01.7) Quy ước tương tác
| Tình huống | Hành vi |
|---|---|
| Chế độ lưu | Explicit Lưu cấu hình button. Dirty form → dấu • cuối "Đã chỉnh sửa" + chặn chuyển trang (browser warn) |
| Có thay đổi chưa lưu | Banner sticky top "Có thay đổi chưa lưu" + buttons "Hủy thay đổi" / "Lưu" |
| Search/filter apply | N/A |
| Reset dây chuyền | "Tạo lại token" → invalidate webhook URL hiện tại (URL không đổi, nhưng token mới — Pancake side phải update) |
| Cập nhật async | Pessimistic — đợi server confirm trước khi update UI status badge |
| Tác vụ chạy lâu | "Kiểm tra kết nối" có thể chậm 5-10s (Pancake REST) — spinner + cho phép hủy sau 15s |
B2.SCR-01.7B) Form Interaction Deep (ref B2.7B template)
| Khía cạnh | Quy ước cho SCR-01 |
|---|---|
| Trigger validation | On blur cho api_key (format check ≥16 ký tự); on submit cho toàn form |
| Autosave | KHÔNG có autosave (cấu hình tổng — explicit save tránh accidental change) |
| Char counter | Textarea VIP tag: hiển thị {X}/100 dòng; đỏ khi >90 dòng |
| Max length cứng | workspace_id max 64 ký tự, api_key max 64 ký tự, tag NAME ≤60 ký tự/dòng |
| IME composing | Cho VIP tag textarea — KHÔNG validate trim/lowercase khi đang compose tiếng Việt |
| Paste rules | Cho phép paste vào api_key (admin thường copy từ Pancake admin) |
| Auto-format | Trim whitespace + lowercase tag NAME khi blur textarea; api_key KHÔNG format (giữ raw) |
| Required marker | * đỏ sau label workspace_id, api_key |
| Inline error | Dưới field; aria-live="assertive" |
| Field disabled vs readonly | webhook_url + webhook_token: readonly (tab-focusable, copyable); kill switch khi suspended: disabled (không tab-focusable) |
| Field locked theo lifecycle | workspace_id sau setup lần đầu: readonly với tooltip "Đổi workspace cần Reset connection (yêu cầu Ops)" |
B2.SCR-01.7C) Concurrency (ref B2.7C)
| Tình huống | Hành vi |
|---|---|
| 2 admin cùng mở SCR-01 | Banner cuối form "Đang chỉnh sửa cùng: NGUYEN VAN A (HCM)" — presence indicator |
| 2 admin cùng sửa, A lưu trước | B nhận modal "Cấu hình đã thay đổi từ thiết bị khác. [Tải lại] / [Ghi đè] (cần xác nhận)" |
| Server-pushed update khi đang xem | Banner "Có cập nhật mới từ user khác. [Tải lại]" — KHÔNG tự reload nếu user đang gõ |
B2.SCR-01.7D) Network Resilience (ref B2.7D)
| Tình huống | Hành vi |
|---|---|
| Mất mạng giữa "Kiểm tra kết nối" | Banner "Mất kết nối Diva server. Đang thử lại..." (autoretry 30s) |
| Mất mạng khi "Lưu cấu hình" | Disable button "Lưu", show "Đang chờ kết nối..."; giữ form data |
| Response > 5s | Spinner + "Pancake phản hồi chậm" + nút "Hủy" sau 10s |
| Response > 30s | Auto-hủy + thông báo "Quá lâu. Đã hủy. Vui lòng thử lại sau." |
B2.SCR-01.7E) Secondary Interactions Contract (ref B2.7E)
- Rule 1 Drill-down: N/A (không có row table trong SCR-01)
- Rule 2 Expand-Collapse: N/A
- Rule 3 Hover state: Hover icon mắt reveal api_key/token; hover icon ⓘ → tooltip B9
- Rule 4 Context menu: N/A
- Rule 5 Load more: N/A
- Rule 6 Inline edit: api_key + textarea VIP tag dùng pattern inline edit (explicit save)
- Rule 7 Tab switch: Cấp container SCR-05 tabs, không thuộc SCR-01
- Rule 8 Overlay: Confirm modal khi "Tạo lại token", "Kích hoạt lại webhook"; tooltip popover hover icon ⓘ
B2.SCR-01.8) Phân quyền (ref B5 chính)
| Action | Admin | Manager | Telesale | POS-only | Denied feedback |
|---|---|---|---|---|---|
Xem SCR-01 (access) | full | hidden | hidden | hidden | hidden (menu không render + redirect /403) |
Edit api_key (update) | full | hidden | hidden | hidden | hidden |
| Reveal api_key | full | (N/A) | (N/A) | (N/A) | (N/A) |
Tạo lại token (update) | full | hidden | hidden | hidden | hidden |
Toggle kill switch (update) | full | hidden | hidden | hidden | hidden |
EXT-4 RBAC field-level: Nếu future Manager được cấp
view(read-only) —api_keysẽ trảnulltừ Hasura permission (KHÔNG mask●●●●●●để tránh leak qua DOM cache). MVP-1 không applicable.
B2.SCR-01.9) Ma trận trạng thái (ref B0.5)
(xem B0.5 hàng SCR-01)
B2.SCR-01.10) Phản hồi sau thao tác
| Hành động | Phản hồi UI | Copy mẫu | Hành động tiếp |
|---|---|---|---|
| Lưu cấu hình thành công | Toast xanh top-right 3s | "Đã lưu cấu hình Pancake CRM." | Ở lại tab |
| Lưu cấu hình lần đầu | Toast xanh + auto-jump | "Đã lưu. Hệ thống đã tạo URL webhook. Tiếp theo: map nguồn → chi nhánh." + auto-redirect SCR-02 sau 3s | Modal hỏi "Sang Tab 2 ngay?" |
| Validation lỗi | Inline + banner tổng | "Workspace ID không hợp lệ. Vui lòng kiểm tra (≤64 ký tự, UUID Pancake)." | Focus field lỗi |
| Kiểm tra kết nối OK | Toast xanh + drawer hiển thị | "Kết nối thành công. Đã phát hiện 12 nguồn từ Pancake workspace." + link "Sang Tab 2 để map" | (button stay) |
| Kiểm tra kết nối FAIL | Banner đỏ form | "Không thể kết nối Pancake. Kiểm tra api_key + thử lại. Mã: {trace_id}" | Focus field api_key |
| Tạo lại token | Confirm modal 2 bước | "Token mới sẽ vô hiệu hóa cấu hình Pancake hiện tại. Mọi event tới URL cũ sẽ bị từ chối. Gõ 'TẠO LẠI' để xác nhận." | Modal → "Tạo lại" enabled khi gõ đúng |
| Toggle kill switch on (tắt) | Confirm modal | "Tắt sẽ ngừng nhận lead Pancake. Mọi event sẽ chuyển trạng thái 'Bỏ qua (đã tắt hệ thống)'. Xác nhận?" | Toggle + log audit |
| Toggle kill switch off (bật) | Toast + auto-save | "Đã bật lại nhận lead Pancake." | (no confirm — bật là an toàn) |
| Kích hoạt lại webhook | Confirm + loading | "Đã bật lại trên Pancake chưa? Sau khi bạn xác nhận, Diva sẽ chuyển trạng thái sang 'Đang hoạt động'." | Confirm → loading 3s → toast "Đã kích hoạt. Pancake bắt đầu gửi event." |
B2.SCR-01.10A) Phân loại lỗi (ref B2.10A)
| Loại lỗi | Trigger | UI pattern | Recovery |
|---|---|---|---|
| Validation client | api_key < 16 ký tự, workspace_id sai format | Inline dưới field | User sửa |
| Validation server | api_key đúng format nhưng Pancake reject | Banner đỏ form + correlation_id | Liên hệ Pancake support 0972273341 |
| Quyền 403 | Manager gõ URL direct | Redirect /403 + toast | Liên hệ Admin |
| Conflict 409 | 2 admin cùng save | Modal "Cấu hình đã đổi. [Tải lại] / [Ghi đè]" | User chọn |
| Network/timeout | Diva server chậm hoặc mất mạng | Banner + retry | Retry tự động/thủ công |
| Server 5xx | DB lỗi | Toast + correlation_id | Báo Ops |
| Rate limit Pancake 429 | "Kiểm tra kết nối" gọi quá nhiều | Banner đếm ngược | Chờ 1 phút |
B2.SCR-02) Map nguồn → chi nhánh
B2.SCR-02.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Admin role (system-wide) + Marketing manager (defer Dynamic Permission MVP-2). Module pancake_crm_integration.access/update/delete |
| Vào màn để quyết định gì? | (1) Map từng Pancake source (FB Diva HCM, Zalo OA, ...) → Diva branch. (2) Bật/tắt per-source flag pilot rollout (W1=1 source → W4=40+). (3) Đồng bộ source mới từ Pancake manual khi cần. (4) Xem last_sync_at để biết source nào lâu chưa update. |
| Dữ liệu chính | pancake_source_routing table — auto INSERT khi Cron 3 phát hiện source mới (is_active=false, branch_id=NULL); admin map sau |
| CTA chính / phụ | Primary: Đồng bộ từ Pancake. Inline-edit autosave (branch dropdown, is_active toggle). Drill-down click row → drawer detail |
| Điều không được hiểu nhầm | "Đồng bộ" = pull source list từ Pancake REST → KHÔNG xóa source đã có. Source bị xóa khỏi Pancake → vẫn giữ row + warning "Đã ngừng tồn tại trên Pancake". "Loose mode" = branch=NULL → ticket vẫn tạo, leader assign sau. is_active=false → status skipped_source_disabled |
B2.SCR-02.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | Admin nhìn thấy: tổng số source, số đã map (% phủ branch), source đang bật/tắt, source mới chưa map |
| Thứ tự ưu tiên thông tin | (1) Summary header "{N} nguồn, {M} đã map, {K} đang bật", (2) Table source với cột Tên Pancake source, Chi nhánh Diva, Trạng thái bật/tắt, Cập nhật cuối, (3) Filter + search, (4) Button đồng bộ |
| Dữ liệu nhạy cảm | Không có field nhạy cảm; tên source là public |
| Pending/partial data | Source mới detect (vừa qua Cron 3, branch=NULL, is_active=false) → highlight row với badge "Mới — cần map"; tooltip "Nguồn vừa được đồng bộ từ Pancake, chưa được map sang chi nhánh Diva" |
| Thuật ngữ canonical | "Nguồn Pancake" (≠ "Nguồn ticket" của Diva A9). "Chi nhánh Diva" (≠ "Source" của Pancake). "Loose mode" → label "Để trống (Loose mode)" trong dropdown |
| Intent token | warning cho row Loose mode (branch=NULL); success cho row mapped+active; muted cho row inactive |
| Không tự suy diễn | KHÔNG hard delete source khỏi DB (DEC-007 — soft delete với disabled cho audit); KHÔNG cho admin đổi pancake_source_id (lock from Pancake) |
B2.SCR-02.2) Ma trận variant
| Variant ID | Điều kiện | Ai thấy | Wireframe | QA ref |
|---|---|---|---|---|
Variant A | Default — đã đồng bộ ≥1 source | Admin | Có (B2.SCR-02.3.A) | TC-UI-SCR02-001 |
Variant B | Empty — chưa đồng bộ source nào | Admin | Có (B2.SCR-02.3.B) | TC-UI-SCR02-002 |
Variant C | No permission | Manager/Telesale/POS | Hidden completely | TC-PERM-SCR02-001 |
Variant D | Loading | Admin | Skeleton table 5 row | TC-UI-SCR02-003 |
Variant E | Error đồng bộ (Pancake REST timeout) | Admin | Banner "Hiển thị cache từ {timestamp}" + Thử lại | TC-UI-SCR02-004 |
Variant F | Connection chưa active (Tab 1 chưa setup) | Admin | Banner deeplink "Cần thiết lập kết nối ở Tab 1 trước" + button "Sang Tab 1" | TC-UI-SCR02-005 |
B2.SCR-02.3) Wireframes
Variant A — Default (có data)
text
/settings/pancake-crm/source-routing
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Tích hợp Pancake CRM [Trạng thái: Đang hoạt động ✓] │
│ [Tab 1 Kết nối] [Tab 2 Map nguồn*] [Tab 3 Lịch sử + DLQ] [Tab 4 Sức khỏe] │
│ ──────────────────────────────────────────────────────────────────────────────│
│ │
│ Tổng: 12 nguồn • Đã map chi nhánh: 9 • Đang bật: 7 • Mới chưa map: 2 │
│ │
│ [🔍 Tìm theo tên nguồn________________] [Trạng thái: Tất cả ▾] [Chi nhánh: Tất cả ▾] │
│ │
│ [↻ Đồng bộ từ Pancake] │
│ │
│ ┌────────────────────────────────────────────────────────────────────────────┐│
│ │ # │ Tên nguồn Pancake │ Chi nhánh Diva │ Bật/Tắt│ Cập nhật cuối ││
│ │───┼────────────────────────────┼──────────────────────┼────────┼───────────────││
│ │ 1 │ FB Diva HCM Q1 │ [Diva HCM Q1 ▾] │ [●━━] │ 15/05 09:30 ││
│ │ 2 │ FB Diva HCM Q3 │ [Diva HCM Q3 ▾] │ [●━━] │ 15/05 09:30 ││
│ │ 3 │ Zalo OA Diva Premium │ [Diva HCM Q1 ▾] │ [●━━] │ 15/05 09:30 ││
│ │ 4 │ Google Ads Diva Brand │ [Diva HCM Q1 ▾] │ [●━━] │ 15/05 09:30 ││
│ │ 5 │ FB Diva Hà Nội Cầu Giấy │ [Diva HN Cầu Giấy ▾] │ [●━━] │ 15/05 09:30 ││
│ │ 6 │ FB Diva Hà Nội Mỹ Đình │ [Diva HN Mỹ Đình ▾] │ [●━━] │ 15/05 09:30 ││
│ │ 7 │ FB Diva Đà Nẵng │ [Diva Đà Nẵng ▾] │ [●━━] │ 15/05 09:30 ││
│ │ 8 │ TikTok Diva Beauty │ [Để trống (Loose)▾] │ [●━━] │ 15/05 09:30 ││
│ │ 9 │ Shopee Diva Official │ [Để trống (Loose)▾] │ [━━●] │ 15/05 09:30 ││
│ │ │ ⓘ Đang tắt — KHÔNG nhận event │ │ ││
│ │10 │ FB Bệnh viện Da liễu Diva │ [Diva HCM Q1 ▾] │ [━━●] │ 15/05 09:30 ││
│ │11 │ FB Diva Cần Thơ [Mới] │ [— Chọn chi nhánh ▾] │ [━━●] │ 15/05 09:30 ││
│ │12 │ Zalo OA Diva HN [Mới] │ [— Chọn chi nhánh ▾] │ [━━●] │ 15/05 09:30 ││
│ └────────────────────────────────────────────────────────────────────────────┘│
│ │
│ ⓘ Click vào dòng để xem chi tiết nguồn + lịch sử event 7 ngày │
│ │
│ Hiển thị 12 / 12 [<< 1 >>] │
└──────────────────────────────────────────────────────────────────────────────────┘Hover row → action icons xuất hiện cuối row: [✏️ Sửa] [🗑️ Vô hiệu hóa] (chỉ Admin có delete permission). Click row → drawer phải mở:
text
╔════════════════════════════════════════╗
║ ⓘ Chi tiết nguồn Pancake [✕] ║
║ ────────────────────────────────────── ║
║ ║
║ Tên nguồn: FB Diva HCM Q1 ║
║ Pancake source ID: src_fb_hcm_q1_2024 ║
║ Chi nhánh Diva: Diva HCM Quận 1 ║
║ Trạng thái: Đang bật ║
║ Cập nhật cuối: 15/05/2026 09:30 ║
║ ║
║ ─── Hoạt động 7 ngày gần nhất ─── ║
║ ║
║ Tổng event nhận: 342 ║
║ Đã xử lý thành công: 340 (99,4%) ║
║ Bỏ qua trùng lặp: 1 ║
║ Lỗi DLQ: 1 ║
║ ║
║ Khách mới tạo: 287 ║
║ Khách update info: 55 ║
║ Ticket tạo mới: 312 ║
║ Đã assign telesale: 297 (95,2%) ║
║ ║
║ Tỉ lệ phản hồi telesale ≤30 phút: ║
║ 278 (89,1%) ║
║ ║
║ [Xem nhật ký event 7 ngày →] ║
║ (chuyển SCR-03 với filter) ║
╚════════════════════════════════════════╝Variant B — Empty (chưa đồng bộ)
text
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Tổng: 0 nguồn │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────────┐│
│ │ ││
│ │ ▣ Chưa có nguồn nào được đồng bộ từ Pancake ││
│ │ ││
│ │ Khi bạn click "Đồng bộ từ Pancake", hệ thống sẽ gọi Pancake API ││
│ │ và tự động thêm danh sách nguồn (FB page, Zalo OA, TikTok...). ││
│ │ Sau đó bạn map từng nguồn sang chi nhánh Diva tương ứng. ││
│ │ ││
│ │ [↻ Đồng bộ từ Pancake] ││
│ │ ││
│ │ ⓘ Cần Tab 1 đã thiết lập kết nối thành công. ││
│ │ ││
│ └──────────────────────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────────────────┘Variant E — Error đồng bộ
text
┌──────────────────────────────────────────────────────────────────────────────────┐
│ ⚠️ Không thể đồng bộ từ Pancake │
│ Pancake API không phản hồi (timeout 30s). Hiển thị danh sách cache từ │
│ 15/05/2026 08:30 (1 giờ trước). │
│ Mã sự cố: TRC-2026-05-15-a8c3d4. Sẽ tự động thử lại sau 30 phút. │
│ [Thử lại ngay] │
└──────────────────────────────────────────────────────────────────────────────────┘
(Table vẫn render với data cache; user có thể edit bình thường;
row last_sync_at hiển thị giá trị cũ)Variant F — Connection chưa active
text
┌──────────────────────────────────────────────────────────────────────────────────┐
│ ⓘ Chưa thiết lập kết nối Pancake │
│ Vui lòng hoàn tất Tab 1 (Kết nối) trước khi map nguồn → chi nhánh. │
│ [Sang Tab 1 →] │
└──────────────────────────────────────────────────────────────────────────────────┘B2.SCR-02.4) Phân loại reuse + điểm update
| Phân loại | File hiện có | Vị trí update | Lý do |
|---|---|---|---|
| 🆕 Build mới | src/modules/settings/pages/pancake/PancakeSourceRouting.tsx | Tab 2 SCR-05 | — |
| ✅ Reuse | XTable, BranchSelect (existing src/modules/user/components/BranchSelect.vue), XToggle, XSearchInput, XDrawer | — | Diva component library |
B2.SCR-02.5) Quy ước field (ref B0.4.B + B0.4.D)
| Field/CTA | Loại | Hiển thị ở | Mặc định | Validation | Tooltip |
|---|---|---|---|---|---|
Cột # (STT) | int | Table left | sequential | — | — |
Cột Tên nguồn Pancake | text readonly | Table | (auto từ Pancake) | text search match | B9 row 9 |
Cột Chi nhánh Diva | inline-edit FK | Table | NULL (Loose mode) | UUID branch valid | B9 row 10 |
Cột Bật/Tắt | inline-toggle | Table | false (mặc định khi source mới detect) | bool | B9 row 11 |
Cột Cập nhật cuối | datetime readonly | Table | auto | DD/MM HH:mm | B9 row 12 |
Đồng bộ từ Pancake button | Primary CTA | Top right | enabled | call REST /sources | B9 row 51 |
B2.SCR-02.6) Bảng — cấu hình cột
| Cột | Độ rộng | Căn lề | Định dạng | Sticky | Sortable |
|---|---|---|---|---|---|
| # | 40px | Giữa | int | Không | Không |
| Tên nguồn Pancake | flex (≥200px) | Trái | text + badge "Mới" | Không | Có (A-Z) |
| Chi nhánh Diva | 240px | Trái | dropdown | Không | Có |
| Bật/Tắt | 100px | Giữa | toggle | Không | Có |
| Cập nhật cuối | 140px | Phải | datetime | Không | Có (DESC default) |
B2.SCR-02.7) Quy ước tương tác
| Tình huống | Hành vi |
|---|---|
| Chế độ lưu | Autosave inline-edit (branch dropdown + is_active toggle) debounce 500ms |
| Đồng bộ | Manual button → loading 5-30s → toast "Đã đồng bộ {N} nguồn ({M} mới)" |
| Reset dây chuyền | Tắt is_active không ảnh hưởng branch dropdown |
| Cập nhật async | Optimistic — UI update ngay, rollback nếu server reject |
| Concurrency | 2 admin edit cùng row → modal "Bản ghi đã thay đổi từ user khác. [Tải lại]" (ref EXT-12) |
B2.SCR-02.7E) Secondary Interactions (ref B2.7E)
- Rule 1 Drill-down: Click row → drawer phải chi tiết source + lịch sử 7d
- Rule 2 Expand-Collapse: N/A
- Rule 3 Hover state: Hover row → action icons "✏️ Sửa" (edit name local nickname — defer MVP-2) + "🗑️ Vô hiệu hóa" (set is_active=false)
- Rule 4 Context menu: Click ⋮ button cuối row → dropdown "Xem chi tiết / Vô hiệu hóa / Đồng bộ riêng nguồn này"
- Rule 5 Load more: Pagination — default 50/page, cho phép 25/50/100 (source list nhỏ ~40 row)
- Rule 6 Inline edit: Branch dropdown + is_active toggle autosave (xem rule 6 chính)
- Rule 7 Tab switch: Cấp SCR-05
- Rule 8 Overlay: Drawer drill-down + confirm modal khi tắt is_active có ≥10 event trong 24h gần nhất
B2.SCR-02.7C) Concurrency (ref EXT-12)
| Tình huống | Hành vi |
|---|---|
| 2 admin cùng edit row N | A save trước → B nhận banner "Bản ghi 'FB Diva HCM Q1' đã thay đổi. [Tải lại]" |
| Optimistic update + server reject | Rollback UI + toast "Không thể lưu — bản ghi đã đổi. Đã tải lại." |
| Inline edit dirty + user close drawer | Confirm "Có thay đổi chưa lưu. [Lưu] / [Hủy]" |
B2.SCR-02.8) Phân quyền
| Action | Admin | Manager | Telesale | POS-only | Denied feedback |
|---|---|---|---|---|---|
Xem SCR-02 (access) | full | hidden | hidden | hidden | hidden |
Đồng bộ từ Pancake (update) | full | hidden | hidden | hidden | hidden |
Edit branch dropdown (update) | full | hidden | hidden | hidden | hidden |
Toggle is_active (update) | full | hidden | hidden | hidden | hidden |
Vô hiệu hóa source (delete) | full | hidden | hidden | hidden | hidden |
B2.SCR-02.9) Ma trận trạng thái (ref B0.5)
B2.SCR-02.10) Phản hồi sau thao tác
| Hành động | Phản hồi UI | Copy mẫu |
|---|---|---|
| Inline-edit branch dropdown save thành công | Toast 2s | "Đã cập nhật chi nhánh cho 'FB Diva HCM Q1' → 'Diva HCM Q1'." |
| Inline-edit toggle is_active sang off | Toast + undo 10s | "Đã tắt nguồn 'TikTok Diva Beauty'. Sẽ ngừng nhận event. [Hoàn tác]" |
| Đồng bộ Pancake thành công | Toast xanh | "Đã đồng bộ. Phát hiện 12 nguồn ({M} nguồn mới). Vui lòng map nguồn mới sang chi nhánh." |
| Đồng bộ Pancake fail | Banner top + cache fallback | "Pancake không phản hồi. Hiển thị bản cache từ {timestamp}. Sẽ tự thử lại sau 30 phút." |
| Branch dropdown chọn "Để trống (Loose mode)" | Confirm modal | "Loose mode: ticket sẽ tạo với chi nhánh NULL, leader manual assign sau. Xác nhận?" |
| Vô hiệu hóa source (set is_active=false) | Warning modal nếu ≥10 event 24h | "Nguồn 'FB Diva HCM Q1' có 87 event trong 24h gần nhất. Tắt sẽ ngừng nhận event. Xác nhận?" |
B2.SCR-03) Lịch sử event + DLQ replay
B2.SCR-03.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Admin (system-wide audit). Module pancake_crm_integration.access/view_all/replay_dlq |
| Vào màn để quyết định gì? | (1) Audit: xem mọi event Pancake đã nhận + status + lifecycle history. (2) Debug: xem raw payload + headers + error_message. (3) Replay: phát lại event status dead_letter hoặc auth_failed/parse_error (sau khi đã fix root cause). (4) Bulk replay nhiều event cùng lúc. (5) Filter test events. |
| Dữ liệu chính | pancake_webhook_event — capacity 100k events/tháng (DEC-023 capacity model); index (created_at, status) + record_id + is_test |
| CTA chính / phụ | Primary: Phát lại sự kiện (single row). Bulk: Phát lại {N} sự kiện đã chọn (rate limit 5/s, batch ≤100). Secondary: Sao chép JSON payload, Xem các event cùng record_id, Xóa bộ lọc |
| Điều không được hiểu nhầm | "Phát lại" KHÔNG gọi lại Pancake — chỉ reset status về received để Hasura trigger fire lại crm-api handler. "Trùng lặp (skipped_duplicate)" KHÔNG phát lại được (đúng nghiệp vụ — đã có row khác xử lý). Status processed không cho phát lại (idempotency). Test events ẩn mặc định (DEC-009). |
B2.SCR-03.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | Admin nhìn thấy: tổng event, % thành công, có DLQ tồn đọng cần xử lý không, event mới nhất là khi nào |
| Thứ tự ưu tiên thông tin | (1) Filter bar (date range mặc định 7 ngày), (2) Summary chip "Đã xử lý: 9985 (99,9%) • DLQ chờ replay: 10 • Test: 5", (3) Table list event với status badge nổi bật, (4) Drawer detail khi cần debug |
| Dữ liệu nhạy cảm | raw_payload có thể chứa SĐT, tên KH (PII) — chỉ Admin xem; KHÔNG export raw CSV mà không masking PII (defer MVP-2) |
| Pending/partial data | Event status ingested/received/processing (non-terminal) → badge muted + tooltip "Đang xử lý"; cell processed_at hiển thị — |
| Thuật ngữ canonical | "Sự kiện" (event) — KHÔNG "Webhook event" (technical). "DLQ" → "Lỗi DLQ (chờ phát lại)" trong UI label; tooltip giải thích "Dead Letter Queue". "Replay" → "Phát lại" |
| Intent token | success cho processed; negative-value cho dead_letter/permanently_failed; warning cho auth_failed/ip_blocked/parse_error; muted cho skipped_* |
| Không tự suy diễn | KHÔNG cho phép edit raw_payload (audit-only); KHÔNG cho phép delete event row (audit immutable) |
B2.SCR-03.2) Ma trận variant
| Variant ID | Điều kiện | Ai thấy | QA ref |
|---|---|---|---|
Variant A | Default — có event | Admin | TC-UI-SCR03-001 |
Variant B | Empty — chưa có event nào | Admin | TC-UI-SCR03-002 |
Variant B2 | Empty-filter — có event nhưng filter narrow | Admin | TC-UI-SCR03-003 |
Variant C | No permission | Manager/Telesale/POS | TC-PERM-SCR03-001 |
Variant D | Loading | Admin | TC-UI-SCR03-004 |
Variant E | Error tải | Admin | TC-UI-SCR03-005 |
Variant F | Bulk replay đang chạy | Admin | TC-UI-SCR03-006 |
Variant G | DLQ tồn đọng cao bất thường (≥50 dead_letter trong 1h) | Admin | TC-UI-SCR03-007 |
B2.SCR-03.3) Wireframes
Variant A — Default
text
/settings/pancake-crm/audit
┌────────────────────────────────────────────────────────────────────────────────────┐
│ Tích hợp Pancake CRM [Trạng thái: Đang hoạt động ✓] │
│ [Tab 1 Kết nối] [Tab 2 Map nguồn] [Tab 3 Lịch sử + DLQ*] [Tab 4 Sức khỏe] │
│ ────────────────────────────────────────────────────────────────────────────────│
│ │
│ Khoảng thời gian: [09/05/2026 - 15/05/2026 (7 ngày) ▾] │
│ Trạng thái: [Tất cả ▾] Nguồn: [Tất cả ▾] ☐ Bao gồm test events │
│ [Xóa bộ lọc] │
│ │
│ Tổng: 10.000 sự kiện • Đã xử lý: 9.985 (99,85%) • Lỗi DLQ: 10 • Bỏ qua: 5 │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ ☐ │ Mã sự kiện │ Record ID │ Trạng thái │ Nguồn │ Thời gian │ │
│ │───┼────────────┼────────────┼─────────────────┼───────────────┼──────────────│ │
│ │ ☐ │ a8c3d4f5 │ rec_xyz_01 │ ✓ Đã xử lý │ FB Diva HCM Q1│15/05 09:42:31│ │
│ │ ☐ │ b9d4e5f6 │ rec_xyz_02 │ ✓ Đã xử lý │ Zalo OA Diva │15/05 09:42:18│ │
│ │ ☐ │ c0e5f6a7 │ rec_xyz_03 │ ⚠ Lỗi DLQ │ FB Diva HN │15/05 09:41:55│ │
│ │ ☐ │ d1f6a7b8 │ rec_xyz_04 │ ✓ Đã xử lý │ Google Ads │15/05 09:41:30│ │
│ │ ☐ │ e2a7b8c9 │ rec_xyz_05 │ ◌ Bỏ qua (trùng)│ FB Diva HCM Q1│15/05 09:41:12│ │
│ │ ☐ │ f3b8c9d0 │ rec_xyz_06 │ ⚠ Lỗi DLQ │ FB Diva HCM Q3│15/05 09:40:48│ │
│ │ ☐ │ a4c9d0e1 │ rec_xyz_07 │ ⚠ Lỗi xác thực │ (không xác định)│15/05 09:40:25│ │
│ │ ☐ │ b5d0e1f2 │ rec_xyz_08 │ ◌ Bỏ qua (KH từ │ FB Diva HCM Q1│15/05 09:40:01│ │
│ │ │ │ │ chối marketing)│ │ │ │
│ │ ☐ │ c6e1f2a3 │ rec_xyz_09 │ ✓ Đã xử lý │ TikTok Diva │15/05 09:39:45│ │
│ │ ☐ │ d7f2a3b4 │ rec_xyz_10 │ ⏳ Đang xử lý │ FB Diva HCM Q1│15/05 09:39:20│ │
│ └──────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ Hover một dòng để hiện hành động: [Phát lại] [Sao chép JSON] [Xem cùng record] │
│ │
│ Hiển thị 1-100 / 10.000 [< Trước] Trang [1] / 100 [Sau >] 100/page ▾ │
└────────────────────────────────────────────────────────────────────────────────────┘
(Click dòng "c0e5f6a7" Lỗi DLQ → drawer phải mở)
╔═══════════════════════════════════════════════════╗
║ Chi tiết sự kiện [✕] ║
║ ────────────────────────────────────────────────── ║
║ ║
║ [Payload] [Headers] [Lịch sử trạng thái] ║
║ ║
║ Mã sự kiện: c0e5f6a7-1f8d-... ║
║ Record ID: rec_xyz_03 ║
║ [Xem các event cùng record →] ║
║ Trạng thái: ⚠ Lỗi DLQ (chờ phát lại) ║
║ Loại: Sự kiện record ║
║ Test event: Không ║
║ Số lần retry: 2 / 3 ║
║ Tạo lúc: 15/05/2026 09:41:55 ║
║ Xử lý lúc: — ║
║ IP nguồn: 103.20.149.117 ║
║ ║
║ ── Thông báo lỗi ── ║
║ ┌─────────────────────────────────────────────┐ ║
║ │ STEP 9 round-robin failed: no active │ ║
║ │ telesale found in branch 'Diva HN Cầu Giấy'.│ ║
║ │ Possible cause: ngoài giờ làm việc │ ║
║ │ (current time 09:41 < 09:00 không khớp). │ ║
║ └─────────────────────────────────────────────┘ ║
║ ║
║ ── Raw payload (JSON) ── [📋 Sao chép JSON] ║
║ ┌─────────────────────────────────────────────┐ ║
║ │ { │ ║
║ │ "record_id": "rec_xyz_03", │ ║
║ │ "phone_number": "+84912345678", │ ║
║ │ "name": "Nguyễn Thị Mai", │ ║
║ │ "source": [{ │ ║
║ │ "id": "src_fb_hn_caugiay", │ ║
║ │ "name": "FB Diva HN Cầu Giấy" │ ║
║ │ }], │ ║
║ │ "tags": ["VIP", "Hot Lead"], │ ║
║ │ "modified_on": "2026-05-15T09:41:50+07:00",│ ║
║ │ ... │ ║
║ │ } │ ║
║ └─────────────────────────────────────────────┘ ║
║ ║
║ [Hủy] [↻ Phát lại sự kiện] ║
╚═══════════════════════════════════════════════════╝Variant B — Empty (no event)
text
┌────────────────────────────────────────────────────────────────────────────────────┐
│ Tổng: 0 sự kiện trong khoảng 7 ngày │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ ▣ Chưa nhận sự kiện nào từ Pancake trong khoảng đã chọn ││
│ │ ││
│ │ Kiểm tra: ││
│ │ • Tab 1 Kết nối: trạng thái có 'Đang hoạt động' không? ││
│ │ • Tab 2 Map nguồn: ≥1 nguồn được bật? ││
│ │ • Pancake admin: URL webhook đã paste đúng? ││
│ │ ││
│ │ [Mở rộng khoảng thời gian (30 ngày)] [Sang Tab 4 xem health →] ││
│ │ ││
│ └──────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────────┘Variant B2 — Empty filter
text
┌────────────────────────────────────────────────────────────────────────────────────┐
│ Không có sự kiện nào khớp bộ lọc │
│ (10.000 sự kiện tổng trong khoảng 7 ngày — có lẽ bộ lọc quá hẹp) │
│ │
│ [Xóa bộ lọc] │
└────────────────────────────────────────────────────────────────────────────────────┘Variant F — Bulk replay đang chạy
text
┌────────────────────────────────────────────────────────────────────────────────────┐
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ ⏳ Đang phát lại 10 sự kiện DLQ... ││
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░ 4 / 10 (40%) ││
│ │ Rate: 5 sự kiện / giây (ước tính 2 giây còn lại) ││
│ │ [Hủy bulk] ││
│ └──────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────────┘
(Sau khi xong, banner đổi thành:)
┌────────────────────────────────────────────────────────────────────────────────────┐
│ ✓ Đã phát lại 8/10 sự kiện thành công. 2 sự kiện vẫn lỗi (chuyển sang DLQ). │
│ Lý do thường gặp: STEP 9 round-robin fail (không có telesale active). │
│ [Xem chi tiết 2 lỗi] │
└────────────────────────────────────────────────────────────────────────────────────┘Variant G — DLQ tồn đọng cao bất thường
text
┌────────────────────────────────────────────────────────────────────────────────────┐
│ 🚨 Cảnh báo: 87 sự kiện DLQ trong 1 giờ gần nhất (bình thường <5/giờ) │
│ Có thể có vấn đề hệ thống (round-robin, telesale roster). Kiểm tra Tab 4 │
│ Sức khỏe + báo Tech Lead nếu cần. │
│ [Lọc chỉ DLQ] [Sang Tab 4 →] │
└────────────────────────────────────────────────────────────────────────────────────┘
(banner đỏ sticky top, không tự đóng; user dismiss qua "Tạm tắt 1h")B2.SCR-03.4) Phân loại reuse
| Phân loại | File | Lý do |
|---|---|---|
| 🆕 Build mới | src/modules/settings/pages/pancake/PancakeAuditDlq.tsx | Tab 3 SCR-05 |
| ✅ Reuse | XTable, XDrawer, XDateRangePicker, XMultiSelect, XCheckbox, XJsonViewer (component có sẵn?) | TBD verify; nếu không có XJsonViewer → build mới (~0.3d) |
B2.SCR-03.5) Quy ước field (ref B0.4.C + B0.4.D)
B2.SCR-03.6) Bảng — cấu hình cột
| Cột | Độ rộng | Căn lề | Định dạng | Sticky | Sortable |
|---|---|---|---|---|---|
| Checkbox bulk | 40px | Giữa | bool | Có (header) | Không |
| Mã sự kiện | 100px | Trái | uuid 8 ký tự đầu | Không | Không |
| Record ID | 140px | Trái | text truncate ... | Không | Không |
| Trạng thái | 200px | Trái | badge + icon | Không | Có (group by status) |
| Nguồn | 180px | Trái | text | Không | Có |
| Thời gian | 160px | Phải | datetime | Không | Có (DESC default) |
B2.SCR-03.7) Quy ước tương tác
| Tình huống | Hành vi |
|---|---|
| Filter apply | Auto-apply on change; debounce 500ms cho date range |
| Drill-down | Click row → drawer (không full screen) — preserve scroll state khi đóng |
| Inline action hover | Action icons opacity 0 → 1; mobile long-press 500ms |
| Bulk replay | Chỉ chọn được row status replayable (dead_letter, auth_failed, parse_error); modal confirm + progress bar |
| Search payload | Defer MVP-2 (text search trong raw_payload JSON cần index riêng) |
B2.SCR-03.7C) Concurrency
| Tình huống | Hành vi |
|---|---|
| 2 admin cùng replay 1 event | A click trước → status chuyển received; B click thấy "Sự kiện đã được phát lại bởi {A} lúc {time}" |
| Bulk replay đang chạy + admin khác mở SCR-03 | Banner "Bulk replay đang chạy bởi {user}. Tránh chạy song song." |
B2.SCR-03.7E) Secondary Interactions (ref B2.7E)
- Rule 1 Drill-down: Click row → drawer detail (đã cover)
- Rule 2 Expand-Collapse: Trong drawer, JSON viewer cho phép collapse nested object (chevron ▶/▼)
- Rule 3 Hover state: Action icons "Phát lại / Sao chép / Xem cùng record" reveal on row hover
- Rule 4 Context menu: Click ⋮ → "Phát lại / Sao chép JSON / Xem cùng record / Đánh dấu permanently_failed"
- Rule 5 Load more: Pagination 100/page; cho phép 50/100/200; total count "10.000 sự kiện"
- Rule 6 Inline edit: N/A (audit immutable)
- Rule 7 Tab switch: Drawer có 3 tab "Payload / Headers / Lịch sử trạng thái" — lazy load mỗi tab
- Rule 8 Overlay: Drawer + confirm modal replay
B2.SCR-03.8) Phân quyền
| Action | Admin | Manager | Telesale | POS-only | Denied feedback |
|---|---|---|---|---|---|
Xem SCR-03 (access + view_all) | full | hidden | hidden | hidden | hidden |
| Xem raw_payload (PII KH) | full | hidden | hidden | hidden | hidden (API trả null) |
Phát lại single (replay_dlq) | full | hidden | hidden | hidden | hidden (button không render) |
Bulk replay (replay_dlq) | full | hidden | hidden | hidden | hidden |
| Export CSV (defer MVP-2) | (defer) | hidden | hidden | hidden | — |
B2.SCR-03.9) Ma trận trạng thái (ref B0.5)
B2.SCR-03.10) Phản hồi sau thao tác
| Hành động | Phản hồi UI | Copy mẫu |
|---|---|---|
| Phát lại 1 event thành công | Toast xanh 3s | "Đã phát lại sự kiện. Trạng thái chuyển 'Lỗi DLQ' → 'Đã xác thực'. Sẽ xử lý trong vài giây." |
| Phát lại fail (status không cho phép) | Toast cam | "Không thể phát lại sự kiện trạng thái '{status}'. Chỉ áp dụng cho 'Lỗi DLQ', 'Lỗi xác thực', 'Lỗi cấu trúc dữ liệu'." |
| Bulk replay đang chạy | Progress modal + banner sticky | "Đang phát lại {X}/{N} sự kiện. Rate 5/giây." |
| Bulk replay xong (full success) | Toast + banner xanh | "Đã phát lại {N} sự kiện thành công." |
| Bulk replay xong (partial) | Banner cam | "Đã phát lại {X}/{N} thành công. {Y} sự kiện vẫn lỗi DLQ. [Xem chi tiết]" |
| Sao chép JSON | Toast 1s | "Đã sao chép JSON payload vào clipboard." |
| Date range > 90 ngày | Inline error | "Khoảng thời gian tối đa 90 ngày. Vui lòng thu hẹp." |
B2.SCR-03.10A) Error Taxonomy
| Loại lỗi | Trigger | UI | Recovery |
|---|---|---|---|
| Validation client | Date range > 90 ngày | Inline error filter | User thu hẹp |
| Validation server | Event không cho phép replay | Toast cam | (đúng nghiệp vụ) |
| Conflict 409 | 2 admin replay cùng event | Modal | Tải lại |
| Network/timeout | Bulk replay 100 event timeout | Banner + retry | Retry batch nhỏ hơn |
| Server 5xx | Hasura action fail | Toast + correlation_id | Báo Ops |
| Rate limit 429 | Bulk replay > 5/s | Banner đếm ngược | Chờ |
B2.SCR-04) Sức khỏe & cảnh báo
B2.SCR-04.1) Ngữ cảnh nghiệp vụ
| Câu hỏi | Quyết định |
|---|---|
| Ai dùng? | Admin (ops monitoring). Module pancake_crm_integration.access |
| Vào màn để quyết định gì? | (1) Quick health check: tỉ lệ event thành công 24h/7d, latency p95, outage status hiện tại. (2) Outage history: xem các sự cố gần đây, MTTR. (3) Alert config (defer MVP-2). (4) Quick link sang SCR-03 khi metric bất thường. |
| Dữ liệu chính | Aggregate query từ pancake_webhook_event + pancake_outage_state |
| CTA chính / phụ | Primary: (không có CTA mutate — read-only dashboard). Secondary: toggle timeframe 24h/7d, click metric drill-down sang SCR-03, "Tải báo cáo PDF" (defer MVP-2) |
| Điều không được hiểu nhầm | "Tỉ lệ thành công" filter skipped_* ra khỏi denominator (FORMULA-004). "Latency p95" loại outlier > 5 phút. "Outage" detect dựa trên 0 event trong 5 phút giờ làm việc — KHÔNG phải Pancake đã suspend. |
B2.SCR-04.1A) Nguyên tắc UX bắt buộc
| Nguyên tắc | Quyết định |
|---|---|
| Mục tiêu user trong 5 giây | Admin nhìn thấy: hệ thống có khỏe không (success ≥99,5%), có outage đang xảy ra không, latency có trong target ≤30s không |
| Thứ tự ưu tiên thông tin | (1) Banner outage (nếu có) sticky top, (2) 4 metric cards lớn (Success rate, Latency p95, Auto-assign rate, Outage current state), (3) Chart trend 7 ngày, (4) Outage history table |
| Pending/partial data | Khi mới setup (chưa có event nào) → các card hiển thị — thay vì 0% để tránh hiểu nhầm "đang fail" |
| Intent token | success cho metric đạt target; warning cho metric trong cảnh báo (vd success 95-99.5%); negative-value cho dưới ngưỡng |
| Không tự suy diễn | KHÔNG render alert email/SMS từ UI MVP-1 (DEC-010 chỉ in-app push) |
B2.SCR-04.2) Ma trận variant
| Variant ID | Điều kiện | Wireframe | QA ref |
|---|---|---|---|
Variant A | Healthy — outage_state=healthy, success ≥99,5% | Có (B2.SCR-04.3.A) | TC-UI-SCR04-001 |
Variant B | Outage active — outage_state=outage_started | Có (B2.SCR-04.3.B) | TC-UI-SCR04-002 |
Variant B2 | Outage recovered (5 phút sau) | banner xanh | TC-UI-SCR04-003 |
Variant C | No permission | Hidden | TC-PERM-SCR04-001 |
Variant D | Loading | Skeleton | TC-UI-SCR04-004 |
Variant E | Error | Banner + cached | TC-UI-SCR04-005 |
Variant F | Success < 99,5% (warning) | Cards với badge cam | TC-UI-SCR04-006 |
B2.SCR-04.3) Wireframes
Variant A — Healthy
text
/settings/pancake-crm/health
┌────────────────────────────────────────────────────────────────────────────────────┐
│ Tích hợp Pancake CRM [Trạng thái: Đang hoạt động ✓] │
│ [Tab 1 Kết nối] [Tab 2 Map nguồn] [Tab 3 Lịch sử + DLQ] [Tab 4 Sức khỏe*] │
│ ────────────────────────────────────────────────────────────────────────────────│
│ │
│ Khoảng thời gian: [24 giờ] / [7 ngày] Cập nhật cuối: 15/05 09:42 │
│ │
│ ┌────────────────────┬────────────────────┬────────────────────┬────────────────┐│
│ │ Tỉ lệ thành công ⓘ │ Độ trễ webhook ⓘ │ Auto-assign ⓘ │ Trạng thái Pancake│
│ │ │ → ticket (p95) │ telesale (chính xác)│ │
│ │ │ │ │ ││
│ │ 99,950% │ 12,5 s │ 92,00% │ ✓ Khỏe mạnh ││
│ │ ──────────── │ ──────────── │ ──────────── │ ││
│ │ Mục tiêu ≥ 99,5% │ Mục tiêu ≤ 30s │ Mục tiêu ≥ 90% │ Nhận event ││
│ │ ✓ Đạt │ ✓ Đạt │ ✓ Đạt │ cuối: 38s trước│ │
│ │ │ │ │ ││
│ │ [Xem chi tiết →] │ [Xem chi tiết →] │ [Xem chi tiết →] │ [Xem lịch sử →]│ │
│ └────────────────────┴────────────────────┴────────────────────┴────────────────┘│
│ │
│ Xu hướng 7 ngày (Tỉ lệ thành công + Số event) │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ % │ │
│ │ 100 ──●──●──●──●──●──●──● (Tỉ lệ thành công, axis trái) │ │
│ │ 99 │ │ │ │ │ │ │ │ │ │
│ │ 98 │ │ │ │ │ │ │ │ │ │
│ │ 97 │ │ │ │ │ │ │ │ │ │
│ │ 09/05 10/05 11/05 12/05 13/05 14/05 15/05 │ │
│ │ Event nhận / ngày (axis phải): 1.5k 1.4k 1.6k 1.5k 1.4k 1.6k 1.5k │ │
│ └──────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ Lịch sử outage (10 gần nhất) │
│ ┌──────────────────────────────────────────────────────────────────────────────┐ │
│ │ Bắt đầu │ Kết thúc │ Thời lượng │ Event miss đã rescue │
│ │──────────────────────┼─────────────────────┼────────────┼───────────────────────│
│ │ 14/05/2026 14:23 │ 14/05/2026 14:31 │ 8 phút │ 0 (Cron 6 rescue) │ │
│ │ 12/05/2026 03:15 │ 12/05/2026 03:18 │ 3 phút │ 0 (Cron 6 rescue) │ │
│ │ 09/05/2026 21:50 │ 09/05/2026 22:12 │ 22 phút │ 2 (Cron 7 rescue) │ │
│ │ (...) │ │
│ └──────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ MTTR (Mean Time To Recovery): 4,40 phút (target ≤ 5 phút) ✓ │
└────────────────────────────────────────────────────────────────────────────────────┘Variant B — Outage active
text
┌────────────────────────────────────────────────────────────────────────────────────┐
│ 🚨 Pancake outage — không nhận event trong 5 phút gần nhất │
│ Bắt đầu: 15/05/2026 14:23 (8 phút trước). Trạng thái: outage_started. │
│ Cron 6 đã kích hoạt adaptive polling fallback (interval 60s). │
│ Đã rescue: 4 event miss tới giờ. │
│ │
│ [Tạm tắt cảnh báo (1h)] [Xem chi tiết outage →] │
└────────────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────────────┐
│ ┌────────────────────┬────────────────────┬────────────────────┬────────────────┐│
│ │ Tỉ lệ thành công │ Độ trễ webhook │ Auto-assign │ Trạng thái ││
│ │ │ → ticket │ telesale │ ││
│ │ │ │ │ ││
│ │ 99,945% │ 14,2 s │ 91,85% │ 🚨 Mất kết nối ││
│ │ ──────────── │ ──────────── │ ──────────── │ Đã 8 phút ││
│ │ Mục tiêu ≥ 99,5% │ Mục tiêu ≤ 30s │ Mục tiêu ≥ 90% │ Polling: 60s ││
│ │ ✓ Đạt │ ✓ Đạt │ ✓ Đạt │ fallback active││
│ │ [Xem chi tiết →] │ [Xem chi tiết →] │ [Xem chi tiết →] │ [Lịch sử →] ││
│ └────────────────────┴────────────────────┴────────────────────┴────────────────┘│
└────────────────────────────────────────────────────────────────────────────────────┘Variant F — Warning (success < 99,5%)
text
┌────────────────────────────────────────────────────────────────────────────────────┐
│ ⚠️ Tỉ lệ thành công thấp bất thường (96,2% — bình thường ≥99,5%) │
│ Khả năng: tăng DLQ trong vài giờ qua. Kiểm tra Tab 3 lọc status 'Lỗi DLQ'. │
│ [Sang Tab 3 lọc DLQ →] │
└────────────────────────────────────────────────────────────────────────────────────┘
(card "Tỉ lệ thành công" hiển thị 96,2% với badge cam "Cần xem xét" thay vì xanh)B2.SCR-04.4) Phân loại reuse
| Phân loại | File | Lý do |
|---|---|---|
| 🆕 Build mới | src/modules/settings/pages/pancake/PancakeHealth.tsx | Tab 4 SCR-05 |
| ✅ Reuse | XMetricCard (?), XChart (echarts wrapper), XTable | TBD verify |
B2.SCR-04.5..10) Quy ước chi tiết
| Mục | Quy ước |
|---|---|
| Refresh | Polling 60s (ref EXT-14.1 dashboard tổng); KHÔNG polling khi tab unfocus |
| Realtime banner outage | Banner sticky top trong khi outage_state=outage_started |
| Click metric card | Drill-down sang SCR-03 với pre-filter (vd "Tỉ lệ thành công" → filter status NOT IN ('processed', 'skipped_*')) |
| Toggle 24h/7d | URL hash ?timeframe=24h|7d; lưu sessionStorage |
| Chart hover | Tooltip hiển thị giá trị chính xác cho ngày đó |
| Outage history click row | Drawer detail outage với timeline + count missing events rescued |
B2.SCR-04.7E) Secondary Interactions
- Rule 1 Drill-down: Click metric card / row outage history
- Rule 3 Hover state: Hover icon ⓘ → tooltip B9 công thức
- Rule 7 Tab switch: Toggle 24h/7d
- Rule 8 Overlay: Banner sticky + drawer outage detail
B2.SCR-04.8) Phân quyền
Tất cả Admin only — Hidden cho non-admin.
B2.SCR-05) Container Tích hợp Pancake CRM
B2.SCR-05.1) Ngữ cảnh
Container XDetailLayout 4 tabs (DEC-016). Reuse pattern AppSettingsSmsTemplate.tsx:14-28.
Route: /settings/pancake-crm (redirect default → /connection).
B2.SCR-05.2) Wireframe
text
/settings/pancake-crm
┌────────────────────────────────────────────────────────────────────────────────────┐
│ ← Settings │
│ │
│ Tích hợp Pancake CRM │
│ Trạng thái: [Đang hoạt động ✓] • Workspace: ws_diva_2026_a8c3d4f5 │
│ ────────────────────────────────────────────────────────────────────────────────│
│ [📡 Kết nối] [🔗 Map nguồn] [📋 Lịch sử + DLQ] [💚 Sức khỏe] │
│ ────────────────────────────────────────────────────────────────────────────────│
│ │
│ <RouterView /> │
│ (Active tab content: SCR-01 / SCR-02 / SCR-03 / SCR-04) │
│ │
└────────────────────────────────────────────────────────────────────────────────────┘B2.SCR-05.3) Quy ước
| Mục | Quy ước |
|---|---|
| Default tab | connection (Tab 1) khi user vào lần đầu |
| Preserve state | sessionStorage pancake-crm-last-tab — quay lại tab cũ trừ khi user click "← Settings" rồi quay lại |
| Permission | pancake_crm_integration.access cho cả route container — Hidden hoàn toàn nếu thiếu |
| Keyboard | ←→ chuyển tab; Tab phím tự nhiên focus next field |
| Header badge | Sync với pancake_connection.status realtime (polling 60s) |
B2.SCR-06) Dropdown ticket source slot 9 (FE delta on 3 màn existing)
B2.SCR-06.1) Ngữ cảnh
Đây là FE delta trên 3 màn hiện hữu của module CRM (DEC-017, FR-018). Slot 9 = ticket_source_pancake với label "Pancake CRM".
3 file cần thay đổi:
diva-admin/src/modules/crm/types.ts:146-164— ADD const + array entrydiva-admin/src/modules/crm/i18n/vi.ts:130-137— ADD i18n keydiva-admin/src/modules/crm/pages/Tickets.tsx:128-149— ADD sourceDescriptions entry
Sau khi 3 file thay đổi, dropdown filter ở Tickets.tsx, dropdown form ở TicketMultipleAdd.tsx, dropdown drawer ở CustomerTicketManager.tsx tự render slot 9 mà KHÔNG cần code change thêm.
B2.SCR-06.2) Wireframe gắn vào layout existing
Demo trên Tickets.tsx — Filter dropdown
text
/crm/tickets
┌─────────────────────────────────────┬───────────────────────────────────────────┐
│ Panel filter trái (existing KEEP) │ Panel data table (existing KEEP) │
├─────────────────────────────────────┼───────────────────────────────────────────┤
│ │ Toolbar: │
│ "Nguồn ticket" │ [+ Thêm] [Sửa] [Xóa] Chip filter active│
│ ┌─────────────────────────────────┐ │ │
│ │ ☐ Tất cả [Áp dụng]│ │ # │ Mã │ Trạng thái│ Liên hệ │... │
│ │ ☐ Nguồn 1 ⓘ │ │───┼───────┼───────────┼──────────────┼...│
│ │ ☐ Nguồn 2 │ │ 1 │TK-001│ Mới │Nguyễn Thị Mai│ │
│ │ ☐ Nguồn 3 │ │ 2 │TK-002│ Mới │Trần Văn Hùng │ │
│ │ ☐ Nguồn 4 │ │ 3 │TK-003│ Đang gọi │Lê Thị Hoa │ │
│ │ ☐ Nguồn 5 │ │ │ │ │ │ │
│ │ ☐ Nguồn 6 │ │ Chi nhánh: Diva HCM Q1 - Q3 │
│ │ ☐ Nguồn 7 ⓘ │ │ Người phụ trách: telesale_a, _b, _c │
│ │ ☐ Nguồn 8 ⓘ │ │ │
│ │ >>> ☑ Pancake CRM ⓘ <<< │ │ │
│ │ (slot 9 mới — selected) │ │ Pagination: 1/100 (20/page) │
│ └─────────────────────────────────┘ │ │
│ │ │
│ ⓘ Hover slot 9 → tooltip: │ │
│ "Lead từ Pancake CRM (40+ kênh: │ │
│ FB, Zalo, TikTok, Google Ads, │ │
│ Whatsapp, Shopee...)" │ │
│ │ │
│ "Trạng thái" (existing KEEP) │ │
│ "Người phụ trách" (existing KEEP) │ │
│ "Người nhận bàn giao" (KEEP) │ │
│ │ │
└─────────────────────────────────────┴───────────────────────────────────────────┘
Khi user chọn "Pancake CRM" trong filter:
→ Table phải hiển thị các ticket có source_id='ticket_source_pancake'
→ Chip filter active hiển thị "Nguồn: Pancake CRM" (xóa được)Demo trên TicketMultipleAdd.tsx — Form Tạo Ticket dropdown
text
/crm/tickets — Click [+ Thêm] → Drawer phải mở:
╔════════════════════════════════════════════════════════════╗
║ Tạo ticket mới [✕] ║
║ ────────────────────────────────────────────────────────── ║
║ ║
║ Khách hàng * [Nguyễn Thị Mai (0912345678) ▾] ║
║ Liên hệ * [+84 912 345 678 ▾] ║
║ Nhóm sản phẩm [Liệu trình da liễu ▾] ║
║ Dịch vụ [Liệu trình trị mụn 5 buổi ▾] ║
║ Chiến dịch tiếp thị [Promotion 30/04 ▾] ║
║ Chi nhánh * [Diva HCM Q1 ▾] ║
║ Ngày hết hạn * [15/05/2026 23:59 📅] ║
║ Nhóm đối tượng * [Telesale ▾] ║
║ Người phụ trách [— Tự động phân công — ▾] ║
║ Người nhận bàn giao [— ▾] ║
║ Ghi chú [_______________________________] ║
║ ║
║ >>> Nguồn [Pancake CRM ▾] (slot 9 mới — chọn được) <<< ║
║ Dropdown 9 option: Nguồn 1, ..., Nguồn 8, Pancake CRM ║
║ ║
║ [Hủy] [Tạo ticket] ║
╚════════════════════════════════════════════════════════════╝
(Note: Khi telesale tạo ticket manual với Nguồn = "Pancake CRM",
ticket vẫn được tạo bình thường; chỉ là field source_id metadata.
Tự động flow vẫn là webhook → auto-create ticket, KHÔNG qua form này.)Demo trên CustomerTicketManager.tsx — Drawer KH
text
/customers/c8f7a3e4-... — Khi click "Lịch sử ticket" panel phải:
╔════════════════════════════════════════╗
║ Khách hàng: Nguyễn Thị Mai [✕] ║
║ SĐT: +84 912 345 678 ║
║ ────────────────────────────────────── ║
║ ║
║ Lịch sử ticket (5 gần nhất) ║
║ ║
║ • TK-100 [Đã hoàn thành] ║
║ Nguồn: Pancake CRM ║
║ Ngày: 10/05/2026 ║
║ ║
║ • TK-085 [Đã hoàn thành] ║
║ Nguồn: Nguồn 1 ║
║ Ngày: 25/04/2026 ║
║ ║
║ [+ Thêm ticket] ║
║ ║
║ (Click "+ Thêm ticket" mở ║
║ TicketMultipleAdd với khách hàng ║
║ pre-filled. Dropdown source render ║
║ 9 option như SCR-06.2 trên.) ║
╚════════════════════════════════════════╝B2.SCR-06.3) Phân loại reuse
| File | Phân loại | Delta |
|---|---|---|
types.ts:146-164 | 🔧 Extend | ADD const TICKET_SOURCE_PANCAKE = "ticket_source_pancake" + append vào array TicketSources |
i18n/vi.ts:130-137 | 🔧 Extend | ADD key ticket_source_pancake: "Pancake CRM" |
i18n/en.ts (nếu có) | 🔧 Extend | ADD key ticket_source_pancake: "Pancake CRM" (giữ EN brand name) |
Tickets.tsx:128-149 | 🔧 Extend | ADD entry vào sourceDescriptions: ticket_source_pancake: '<span>Lead từ Pancake CRM (40+ kênh: FB, Zalo, TikTok, Google Ads, Whatsapp, Shopee...)</span>' |
Tickets.tsx:851-865 (filter dropdown) | ✅ Reuse | Auto-render slot 9 (no code change) |
TicketMultipleAdd.tsx:758 (form dropdown) | ✅ Reuse | Auto-render |
CustomerTicketManager.tsx drawer | ✅ Reuse | Auto-render |
Module report (customer-service, telesales) filters | TBD-verify Phase 4 | Grep audit 'ticket_source_' — nếu hardcode → +0.5d delta |
B2.SCR-06.4) Quy ước
| Mục | Quy ước |
|---|---|
| Migration ordering | Migration INSERT crm_master_data row ticket_source_pancake PHẢI run TRƯỚC FE deploy — tránh dropdown render text undefined |
| Permission | Mọi user có ticket_management.access đều thấy slot 9 trong filter; khác với Settings/Pancake (Admin only) |
| i18n fallback | Nếu i18n key thiếu → fallback display "ticket_source_pancake" (technical) — KHÔNG render trống |
| Tooltip | Hover icon ⓘ cạnh "Pancake CRM" hiển thị mô tả (HTML span) như B2.SCR-06.2 |
B2.SCR-06.5) Variants
| Variant ID | Điều kiện | Wireframe | QA ref |
|---|---|---|---|
Variant A | Default — slot 9 hiển thị 9 option | (B2.SCR-06.2) | TC-UI-SCR06-001 |
Variant B | Migration chưa run, FE đã deploy | Fallback display "ticket_source_pancake" + log warning | TC-UI-SCR06-002 |
Variant C | User không có ticket_management.access | Dropdown hidden hoàn toàn | TC-PERM-SCR06-001 |
B2.SCR-06.6) Hành vi tương tác (đã liệt kê ở B0.9 SCR-06)
B3) Luồng người dùng (Flows — BẮT BUỘC Profile L)
Luồng 1 — Setup ban đầu (Admin first-time)
Thời gian ước tính: 15 phút (giả định Admin đã có Pancake credentials).
Luồng 2 — Replay DLQ event (admin recovery)
Luồng bulk replay:
Luồng 3 — Outage detected (auto + admin verify)
MTTR target: ≤5 phút sau khi Pancake recovery.
Luồng 4 — Telesale nhận lead Pancake (auto-rendered, ref SCR-06)
Lưu ý: Telesale flow chạy trên UI existing (Tickets.tsx + ticket detail page) — SCR-06 chỉ là delta dropdown source. Không thuộc UI Spec này về detail.
Luồng 5 — Loose mode (branch=NULL, admin manual assign)
B4) Đặc tả thông báo (Notification Spec)
3 template mới (DEC-024) + dispatch qua
notification-v2-apiHasura actionsendNotifications.
| Điều kiện kích hoạt | Tên template | Kênh | Recipient | Mẫu nội dung | Chống trùng (dedupe) |
|---|---|---|---|---|---|
| Ticket Pancake được auto-assign cho telesale (FR-011 STEP 12) | noti_ticket_assigned_pancake | In-app push + (email/SMS theo preference telesale) | telesale assignee_id | "Bạn vừa nhận lead Pancake CRM: {customer_name} ({phone}). Nguồn: {source_name}. Phản hồi trong 30 phút." | 1 notification / ticket_id (không gửi lại nếu re-assign) |
| Source mới detect chưa map branch (Cron 3) HOẶC ticket tạo với branch=NULL (Loose mode) | noti_pancake_unmapped_branch | In-app push | Admin role + branch-manager NULL | "Có nguồn Pancake mới chưa map chi nhánh: '{source_name}'. {N} ticket đang chờ map. Click để xem." | Dedupe 1h per source_id |
Outage detected (Cron 4 status → outage_started) | noti_pancake_outage | In-app push (DEC-012 default) | Admin role | "Pancake không nhận event {duration} phút. Đã chuyển sang polling fallback (Cron 6). Click xem chi tiết." | 1 notification / outage_started event (PD-012 confirm) |
Outage recovered (status → outage_recovered) | noti_pancake_outage (variant) | In-app push | Admin role | "Pancake đã khôi phục. Outage tổng {duration_min} phút. Đã rescue {N} event miss." | 1 / recovery event |
| Bulk replay xong (partial fail) | (existing pattern, reuse) | In-app push | Admin (who triggered) | "Bulk replay hoàn tất: {X}/{N} thành công. {Y} vẫn lỗi DLQ." | 1 / bulk_replay_id |
| DLQ tồn đọng cao bất thường (≥50 trong 1h — Cron health-alert) | (extend noti_pancake_outage variant dlq_high) | In-app push | Admin role | "Cảnh báo: {N} sự kiện DLQ trong 1 giờ. Có thể có lỗi hệ thống. Click xem." | Dedupe 30 phút |
B4.1) Template seed (master data migration)
sql
INSERT INTO notification_template (id, name, channel, ...)
VALUES
('noti_ticket_assigned_pancake', 'Lead Pancake CRM được phân công', 'push', ...),
('noti_pancake_unmapped_branch', 'Nguồn Pancake chưa map chi nhánh', 'push', ...),
('noti_pancake_outage', 'Pancake outage cảnh báo', 'push', ...);Detail SQL →
dev-spec.mdC4 + C7 migration.
B4.2) UI notification badge
- Telesale
noti_ticket_assigned_pancake: hiện trên notification bell + badge count; click → deeplink/crm/tickets/{ticket_id}(detail page). - Admin
noti_pancake_unmapped_branch: click → deeplink/settings/pancake-crm/source-routing?filter=unmapped. - Admin
noti_pancake_outage: click → deeplink/settings/pancake-crm/health.
B5) Ma trận phân quyền (Permission Matrix — BẮT BUỘC M+)
Mỗi action × role × screen có 3 cột: Render condition + Denied feedback + Khi thu hồi quyền.
B5.1) Module pancake_crm_integration (admin portal)
| Module | Action | Role | Render condition | Denied feedback | Khi thu hồi quyền |
|---|---|---|---|---|---|
| pancake_crm_integration | access | Admin | always render | — | ẩn sau refetch/relogin (toast "Quyền của bạn đã thay đổi") |
| pancake_crm_integration | access | Manager | KHÔNG render (default seed) | hidden (menu Sidebar không có DOM) | giữ hidden |
| pancake_crm_integration | access | Telesale | KHÔNG render | hidden | giữ hidden |
| pancake_crm_integration | access | POS-only | KHÔNG render | hidden | giữ hidden |
| pancake_crm_integration | create | Admin | render SCR-02 button "Đồng bộ", SCR-03 button "Phát lại" | — | ẩn sau refetch |
| pancake_crm_integration | create | non-Admin | KHÔNG render | hidden | — |
| pancake_crm_integration | update | Admin | render SCR-01 form save, SCR-02 inline-edit, SCR-01 toggle kill switch | — | inline-edit disabled + tooltip "Quyền đã thay đổi" |
| pancake_crm_integration | update | non-Admin | KHÔNG render | hidden | — |
| pancake_crm_integration | delete | Admin | render SCR-02 hover action "Vô hiệu hóa source" | — | ẩn sau refetch |
| pancake_crm_integration | delete | non-Admin | KHÔNG render | hidden | — |
| pancake_crm_integration | view_all | Admin | render SCR-03 full audit list + drawer raw_payload | — | drawer raw_payload trả null từ API |
| pancake_crm_integration | view_all | non-Admin | KHÔNG render | hidden (kể cả URL direct) | — |
| pancake_crm_integration | replay_dlq | Admin | render SCR-03 button "Phát lại" + bulk action | — | button hidden sau refetch |
| pancake_crm_integration | replay_dlq | non-Admin | KHÔNG render | hidden (button không có DOM) | — |
B5.2) Module ticket_management (SCR-06 — không thay đổi)
| Module | Action | Role | Render condition | Denied feedback |
|---|---|---|---|---|
| ticket_management | access | Admin/Manager/Telesale | render dropdown 9 source (slot 9 = Pancake CRM) | — |
| ticket_management | access | POS-only | KHÔNG render Tickets module | hidden (toàn route) |
SCR-06 không tạo quyền mới — reuse
ticket_managementexisting permission.
B5.3) 3 mode Denied feedback canonical (chọn 1 cho mỗi action)
| Mode | Khi dùng | Diva pattern |
|---|---|---|
hidden | User KHÔNG nên biết feature tồn tại (Settings/Pancake cho non-admin) | Menu không có DOM + route guard redirect /403 |
disabled | User nên biết feature có nhưng cần quyền (vd inline-edit branch khi network fail, KHÔNG phải permission case) | Grey button + tooltip lý do |
redirect | User gõ URL direct mà không có quyền | Route guard → /403 + toast |
Trong feature này: Toàn bộ permission denied feedback = hidden (Diva rule "ẩn menu/button, không disable").
B5.4) EXT-4 RBAC field-level (defer MVP-2 ready)
| Field | Admin | Manager | Telesale | Portal | View mode | API behavior |
|---|---|---|---|---|---|---|
api_key | full (encrypted display sau reveal) | hidden | hidden | admin | null cho non-admin | Hasura permission filter cột |
webhook_token | full sau reveal | hidden | hidden | admin | null | Hasura permission |
raw_payload (audit) | full | hidden | hidden | admin | null | Hasura permission |
vip_tag_names[] | full | hidden | hidden | admin | null | Hasura permission |
| Source name (Pancake) | full | full (defer Marketing role MVP-2 read-only) | hidden | admin | full hoặc summary | Hasura |
Enforcement: FE hide chỉ là UX; backend Hasura permission canonical — field nhạy cảm trả
nullcho non-admin (KHÔNG mask●●●●●●trong DOM để tránh DOM cache leak).
B6) Ma trận trạng thái (State Matrix — BẮT BUỘC khi có lifecycle)
B6.1) pancake_webhook_event.status lifecycle (12 trạng thái — LIFECYCLE-001)
Ref PRD LIFECYCLE-001. Section này map UI/action theo state.
| Trạng thái (code) | Display VI | Badge intent | UI badge | Action cho phép (SCR-03 row) | Action ẩn/khóa | Field cho phép edit | Side effect |
|---|---|---|---|---|---|---|---|
ingested | Đã nhận (chưa xác thực) | muted | [Đã nhận] xám | "Sao chép JSON", "Xem cùng record" | "Phát lại" (chưa xử lý gì) | (audit immutable) | non-terminal — chờ verify |
received | Đã xác thực | warning | [Đã xác thực] cam | "Sao chép JSON", "Xem cùng record" | "Phát lại" (đang xử lý) | — | non-terminal — Hasura trigger fired |
processing | Đang xử lý | warning | [Đang xử lý] cam-spin | "Sao chép JSON" | "Phát lại" | — | non-terminal — handler running |
processed | Đã xử lý | success | [Đã xử lý ✓] xanh | "Sao chép JSON", "Xem cùng record", "Xem ticket đã tạo" | "Phát lại" (idempotency — không cho replay) | — | terminal — ticket created |
auth_failed | Lỗi xác thực | negative | [Lỗi xác thực ⚠] đỏ | "Sao chép JSON", "Phát lại" (sau khi fix token) | — | — | terminal — alert ops nếu > 10/min |
ip_blocked | Bị chặn IP | negative | [Bị chặn IP ⚠] đỏ | "Sao chép JSON" | "Phát lại" (cần fix IP whitelist trước) | — | terminal |
parse_error | Lỗi cấu trúc dữ liệu | negative | [Lỗi cấu trúc ⚠] đỏ | "Sao chép JSON", "Phát lại" (sau khi Pancake fix payload) | — | — | terminal |
skipped_duplicate | Bỏ qua (trùng lặp) | muted | [Bỏ qua (trùng)] xám | "Sao chép JSON", "Xem cùng record" | "Phát lại" (đúng nghiệp vụ) | — | terminal — last_received_at++ |
skipped_source_disabled | Bỏ qua (nguồn đã tắt) | muted | [Bỏ qua (nguồn tắt)] xám | "Sao chép JSON", "Xem cùng record" | "Phát lại" (cần bật source trước qua SCR-02) | — | terminal |
skipped_kill_switch | Bỏ qua (đã tắt hệ thống) | muted | [Bỏ qua (kill switch)] xám | "Sao chép JSON" | "Phát lại" (cần bật kill switch SCR-01) | — | terminal |
skipped_opt_out | Bỏ qua (khách từ chối marketing) | muted | [Bỏ qua (opt-out)] xám | "Sao chép JSON", "Xem cùng record" | "Phát lại" (đúng compliance) | — | terminal |
dead_letter | Lỗi DLQ (chờ phát lại) | negative | [Lỗi DLQ ⚠] đỏ | "Phát lại", "Sao chép JSON", "Xem cùng record", "Đánh dấu permanently_failed" | — | — | non-terminal — bulk select OK |
permanently_failed | Lỗi vĩnh viễn (cần xem xét) | negative-dark | [Lỗi vĩnh viễn ⚠] đỏ đậm | "Sao chép JSON", (manual escalate) | "Phát lại" (đã hết retry budget) | — | terminal — alert manual review |
Mermaid lifecycle (ref PRD):
B6.2) pancake_outage_state.status lifecycle (3 trạng thái — LIFECYCLE-002)
| Trạng thái | Display VI | Badge intent | UI render | Cron behavior |
|---|---|---|---|---|
healthy | Khỏe mạnh | success | SCR-04 card "✓ Khỏe mạnh" + chart xanh | Cron 6 không chạy |
outage_started | Mất kết nối | negative | SCR-04 banner đỏ sticky + card "🚨 Mất kết nối" | Cron 6 adaptive polling kích hoạt (60-900s) |
outage_recovered | Đang phục hồi | warning | SCR-04 banner xanh "Đang phục hồi (chờ sustain 5 phút)" | Cron 6 stop sau 5 phút sustain |
B6.3) pancake_connection.status (4 trạng thái — không phải lifecycle phức tạp nhưng cần map UI)
| Trạng thái | Display VI | Badge | SCR-01 UI behavior |
|---|---|---|---|
active | Đang hoạt động | success ✓ xanh | Default Variant A; tất cả field editable; kill switch hoạt động |
paused | Tạm dừng | warning ⏸ vàng | Form vẫn editable; banner cam "Đã tạm dừng — webhook không nhận event"; button "Bật lại" |
suspended_by_pancake | Bị Pancake khóa | negative 🚫 đỏ | Variant F; banner đỏ + button "Kích hoạt lại webhook"; kill switch disabled |
error | Lỗi | negative ⚠ đỏ | Banner "Lỗi cấu hình: {error_message}"; button "Kiểm tra kết nối" để re-verify |
B6.4) Ticket source slot 9 (KHÔNG có lifecycle riêng)
Ticket Pancake follow ticket lifecycle existing (ticket_status_new → ... → ticket_status_done). KHÔNG đổi state machine ticket. Chỉ field source_id='ticket_source_pancake' tăng coverage trong filter UI.
B6.5) EXT-3 Lifecycle mở rộng — action × state × persona
Đã cover trong B6.1 trên (12 trạng thái + action allowed). Không cần bảng riêng vì chỉ Admin persona dùng UI (chứ không có multi-role approval flow phức tạp).
B7) Từ điển nội dung hiển thị (Copy Text Dictionary)
Liệt kê mọi copy mới/thay đổi xuất hiện trong UI Spec. Cross-ref từ wireframes B2 và variant.
B7.1) Header / Tabs / Sidebar
| Key | Tiếng Việt | EN (i18n nếu cần) | Ngữ cảnh |
|---|---|---|---|
pancake.sidebar.menu | Tích hợp Pancake CRM | Pancake CRM Integration | Sidebar Settings entry |
pancake.scr05.header | Tích hợp Pancake CRM | Pancake CRM Integration | SCR-05 page title |
pancake.tab.connection | Kết nối | Connection | Tab 1 label |
pancake.tab.source_routing | Map nguồn | Source mapping | Tab 2 label |
pancake.tab.audit | Lịch sử + DLQ | Audit + DLQ | Tab 3 label |
pancake.tab.health | Sức khỏe | Health | Tab 4 label |
B7.2) SCR-01 Connection — labels + buttons + copy
| Key | Tiếng Việt | Ngữ cảnh |
|---|---|---|
scr01.section.workspace | Thông tin workspace Pancake | Section header |
scr01.field.workspace_id | Workspace ID | Field label |
scr01.field.workspace_id.placeholder | Mã workspace Pancake | Placeholder |
scr01.field.api_key | API key | Field label |
scr01.field.api_key.placeholder | Nhập API key từ Pancake admin | Placeholder |
scr01.btn.test_connection | Kiểm tra kết nối | Primary CTA |
scr01.btn.reveal_password | Hiện 5s | Toggle reveal API key |
scr01.section.webhook | Cấu hình webhook (paste vào Pancake admin) | Section header |
scr01.field.webhook_url | URL webhook | Readonly label |
scr01.btn.copy_url | Sao chép URL | Icon button |
scr01.field.webhook_token | Token webhook | Readonly label |
scr01.btn.copy_token | Sao chép | Icon button |
scr01.btn.regenerate_token | Tạo lại | Destructive button |
scr01.section.kill_switch | Tắt khẩn cấp toàn hệ thống (kill switch) | Section header |
scr01.kill_switch.label | Đang nhận event Pancake | Toggle label |
scr01.kill_switch.tooltip | Tắt sẽ chuyển mọi event sang trạng thái "Bỏ qua (đã tắt hệ thống)". Pancake không bị suspend (vẫn trả 200). | Tooltip |
scr01.section.vip_tags | Danh sách tag VIP (theo tên, không phân biệt hoa thường) | Section header |
scr01.vip_tags.helper | Mỗi dòng = 1 tag. Khi Pancake gửi event chứa tag trùng tên, hệ thống coi khách là VIP và luôn tạo ticket dù chỉ update info. | Helper text |
scr01.vip_tags.counter | {X} / 100 dòng | Char counter |
scr01.btn.save | Lưu cấu hình | Primary CTA |
scr01.btn.cancel | Hủy thay đổi | Secondary CTA |
scr01.last_modified | Lưu lần cuối: {datetime} bởi | Footer info |
scr01.dirty_indicator | Đã chỉnh sửa | Indicator |
scr01.suspended.banner.title | Pancake đã tự động khóa webhook | Variant F |
scr01.suspended.banner.body | Lý do: 80% error / 30 phút (suspension rule). Phát hiện lúc: {datetime} (Cron 4 detect). Mọi event mới sẽ KHÔNG được Pancake gửi tới Diva. | Variant F body |
scr01.suspended.action_required | Hành động cần làm:\n1. Vào Pancake admin > Tích hợp > Webhook → Bật lại thủ công\n2. Sau đó bấm nút bên dưới để cập nhật trạng thái Diva | Variant F instructions |
scr01.btn.reactivate_webhook | Kích hoạt lại webhook | Variant F CTA |
scr01.first_time.welcome.title | Chào mừng đến với Tích hợp Pancake CRM | Variant B welcome |
scr01.first_time.welcome.body | Nhập thông tin workspace để bắt đầu nhận lead tự động từ Pancake. Cần Workspace ID + API key (lấy từ Pancake admin > Tích hợp). | Variant B body |
scr01.first_time.webhook_pending | URL + Token webhook sẽ được tự động tạo sau khi lưu thành công. | Variant B helper |
scr01.toast.save_success | Đã lưu cấu hình Pancake CRM. | Toast success |
scr01.toast.save_first_time | Đã lưu. Hệ thống đã tạo URL webhook. Tiếp theo: map nguồn → chi nhánh. | Toast first-time |
scr01.toast.test_success | Kết nối thành công. Đã phát hiện {N} nguồn từ Pancake workspace. | Toast test OK |
scr01.error.test_fail | Không thể kết nối Pancake. Kiểm tra api_key + thử lại. Mã: | Banner test fail |
scr01.modal.regen_token.title | Tạo lại Token webhook | Confirm modal title |
scr01.modal.regen_token.body | Token mới sẽ vô hiệu hóa cấu hình Pancake hiện tại. Mọi event tới URL cũ sẽ bị từ chối. Gõ 'TẠO LẠI' để xác nhận. | Modal body |
scr01.modal.regen_token.confirm_word | TẠO LẠI | Confirm word user phải gõ |
scr01.modal.kill_switch_on.title | Tắt nhận event Pancake | Confirm modal |
scr01.modal.kill_switch_on.body | Tắt sẽ ngừng nhận lead Pancake. Mọi event sẽ chuyển trạng thái 'Bỏ qua (đã tắt hệ thống)'. Xác nhận? | Modal body |
scr01.validation.workspace_id | Workspace ID không hợp lệ. Vui lòng kiểm tra (≤64 ký tự, UUID Pancake). | Inline error |
scr01.validation.api_key_short | API key phải ≥16 ký tự. | Inline error |
scr01.validation.api_key_invalid | API key sai. Kiểm tra trên trang Pancake admin. | Server validation |
B7.3) SCR-02 Source routing
| Key | Tiếng Việt | Ngữ cảnh |
|---|---|---|
scr02.summary | Tổng: {N} nguồn • Đã map chi nhánh: {M} • Đang bật: {K} • Mới chưa map: | Summary chip |
scr02.search.placeholder | Tìm theo tên nguồn... | Search input |
scr02.filter.status.all | Tất cả | Filter default |
scr02.filter.status.active | Đang bật | Filter option |
scr02.filter.status.inactive | Đang tắt | Filter option |
scr02.filter.branch.all | Tất cả | Filter default |
scr02.btn.sync | Đồng bộ từ Pancake | Primary CTA |
scr02.col.idx | # | Column header |
scr02.col.source_name | Tên nguồn Pancake | Column header |
scr02.col.branch | Chi nhánh Diva | Column header |
scr02.col.is_active | Bật/Tắt | Column header |
scr02.col.last_sync_at | Cập nhật cuối | Column header |
scr02.branch.loose_mode | Để trống (Loose mode) | Branch dropdown option |
scr02.branch.choose | — Chọn chi nhánh | Default placeholder dropdown |
scr02.row.new_badge | Mới | Badge cho source chưa map |
scr02.row.inactive_label | Đang tắt — KHÔNG nhận event | Row helper khi is_active=false |
scr02.row.helper | Click vào dòng để xem chi tiết nguồn + lịch sử event 7 ngày | Helper text |
scr02.empty.title | Chưa có nguồn nào được đồng bộ từ Pancake | Variant B title |
scr02.empty.body | Khi bạn click "Đồng bộ từ Pancake", hệ thống sẽ gọi Pancake API và tự động thêm danh sách nguồn (FB page, Zalo OA, TikTok...). Sau đó bạn map từng nguồn sang chi nhánh Diva tương ứng. | Variant B body |
scr02.empty.precondition | Cần Tab 1 đã thiết lập kết nối thành công. | Variant B helper |
scr02.empty_filter.title | Không có nguồn nào khớp bộ lọc. {N} nguồn tổng. | Empty filter |
scr02.error.title | Không thể đồng bộ từ Pancake | Variant E |
scr02.error.body | Pancake API không phản hồi (timeout 30s). Hiển thị danh sách cache từ {timestamp} ({ago} trước). Mã sự cố: {trace_id}. Sẽ tự động thử lại sau 30 phút. | Variant E body |
scr02.precondition.title | Chưa thiết lập kết nối Pancake | Variant F |
scr02.precondition.body | Vui lòng hoàn tất Tab 1 (Kết nối) trước khi map nguồn → chi nhánh. | Variant F body |
scr02.btn.goto_tab1 | Sang Tab 1 | Variant F CTA |
scr02.toast.branch_updated | Đã cập nhật chi nhánh cho '{source_name}' → '{branch_name}'. | Toast inline-edit success |
scr02.toast.toggle_off | Đã tắt nguồn '{source_name}'. Sẽ ngừng nhận event. | Toast + undo |
scr02.toast.undo | Hoàn tác | Undo toast button |
scr02.toast.sync_success | Đã đồng bộ. Phát hiện {N} nguồn ({M} nguồn mới). Vui lòng map nguồn mới sang chi nhánh. | Toast sync OK |
scr02.modal.loose_mode.title | Loose mode (chi nhánh trống) | Confirm modal |
scr02.modal.loose_mode.body | Loose mode: ticket sẽ tạo với chi nhánh NULL, leader manual assign sau. Xác nhận? | Modal body |
scr02.modal.disable_with_traffic.title | Tắt nguồn đang có traffic | Confirm modal |
scr02.modal.disable_with_traffic.body | Nguồn '{source_name}' có {N} event trong 24h gần nhất. Tắt sẽ ngừng nhận event. Xác nhận? | Modal body |
scr02.drawer.title | Chi tiết nguồn Pancake | Drawer title |
scr02.drawer.activity_7d | Hoạt động 7 ngày gần nhất | Drawer section |
scr02.drawer.btn.view_audit | Xem nhật ký event 7 ngày | Drill-down sang SCR-03 |
B7.4) SCR-03 Audit + DLQ
| Key | Tiếng Việt | Ngữ cảnh |
|---|---|---|
scr03.filter.date_range | Khoảng thời gian | Filter label |
scr03.filter.date_range.default | 7 ngày gần nhất | Default display |
scr03.filter.status | Trạng thái | Filter label |
scr03.filter.source | Nguồn | Filter label |
scr03.filter.include_test | Bao gồm test events | Checkbox |
scr03.btn.clear_filter | Xóa bộ lọc | Secondary CTA |
scr03.summary | Tổng: {N} sự kiện • Đã xử lý: {M} ({pct}%) • Lỗi DLQ: {K} • Bỏ qua: | Summary chip |
scr03.col.checkbox | (no header text) | Bulk select |
scr03.col.event_id | Mã sự kiện | Column header |
scr03.col.record_id | Record ID | Column header |
scr03.col.status | Trạng thái | Column header |
scr03.col.source | Nguồn | Column header |
scr03.col.created_at | Thời gian | Column header |
scr03.row.actions_hint | Hover một dòng để hiện hành động: Phát lại / Sao chép JSON / Xem cùng record | Helper text |
scr03.empty.title | Chưa nhận sự kiện nào từ Pancake trong khoảng đã chọn | Variant B |
scr03.empty.checklist | Kiểm tra:\n• Tab 1 Kết nối: trạng thái có 'Đang hoạt động' không?\n• Tab 2 Map nguồn: ≥1 nguồn được bật?\n• Pancake admin: URL webhook đã paste đúng? | Variant B body |
scr03.empty.btn.expand_range | Mở rộng khoảng thời gian (30 ngày) | Variant B CTA |
scr03.empty_filter.title | Không có sự kiện nào khớp bộ lọc | Variant B2 |
scr03.empty_filter.body | {N} sự kiện tổng trong khoảng {timeframe} — có lẽ bộ lọc quá hẹp | Variant B2 body |
scr03.bulk.progress | Đang phát lại {X} / {N} sự kiện DLQ... | Variant F |
scr03.bulk.rate_helper | Rate: 5 sự kiện / giây (ước tính {N} giây còn lại) | Variant F |
scr03.bulk.btn.cancel | Hủy bulk | Variant F |
scr03.bulk.success | Đã phát lại {X}/{N} sự kiện thành công. {Y} sự kiện vẫn lỗi (chuyển sang DLQ). Lý do thường gặp: STEP 9 round-robin fail (không có telesale active). | Variant F success |
scr03.bulk.view_failures | Xem chi tiết {N} lỗi | Variant F |
scr03.dlq_alert.title | Cảnh báo: {N} sự kiện DLQ trong 1 giờ gần nhất (bình thường <5/giờ) | Variant G |
scr03.dlq_alert.body | Có thể có vấn đề hệ thống (round-robin, telesale roster). Kiểm tra Tab 4 Sức khỏe + báo Tech Lead nếu cần. | Variant G body |
scr03.dlq_alert.btn.filter | Lọc chỉ DLQ | Variant G CTA |
scr03.dlq_alert.btn.health | Sang Tab 4 | Variant G CTA |
scr03.btn.replay_single | Phát lại sự kiện | Row action / drawer CTA |
scr03.btn.copy_json | Sao chép JSON | Row action |
scr03.btn.view_same_record | Xem cùng record | Row action |
scr03.drawer.title | Chi tiết sự kiện | Drawer title |
scr03.drawer.tab.payload | Payload | Tab |
scr03.drawer.tab.headers | Headers | Tab |
scr03.drawer.tab.history | Lịch sử trạng thái | Tab |
scr03.drawer.error_message | Thông báo lỗi | Section |
scr03.drawer.raw_payload | Raw payload (JSON) | Section |
scr03.drawer.field.event_id | Mã sự kiện | Field |
scr03.drawer.field.record_id | Record ID | Field |
scr03.drawer.field.status | Trạng thái | Field |
scr03.drawer.field.event_type | Loại | Field |
scr03.drawer.field.is_test | Test event | Field (Yes/No) |
scr03.drawer.field.retry_count | Số lần retry | Field |
scr03.drawer.field.created_at | Tạo lúc | Field |
scr03.drawer.field.processed_at | Xử lý lúc | Field |
scr03.drawer.field.source_ip | IP nguồn | Field |
scr03.modal.replay.title | Phát lại sự kiện | Confirm modal |
scr03.modal.replay.body | Phát lại sự kiện {event_id_short}? Trạng thái sẽ chuyển '{current_status}' → 'Đã xác thực'. | Modal body |
scr03.modal.bulk_replay.title | Phát lại bulk {N} sự kiện | Confirm modal |
scr03.modal.bulk_replay.body | Phát lại {N} sự kiện? Sẽ chạy với rate 5 sự kiện/giây (~{seconds} giây). | Modal body |
scr03.toast.replay_success | Đã phát lại sự kiện. Trạng thái chuyển 'Lỗi DLQ' → 'Đã xác thực'. Sẽ xử lý trong vài giây. | Toast |
scr03.toast.replay_not_allowed | Không thể phát lại sự kiện trạng thái '{status_vi}'. Chỉ áp dụng cho 'Lỗi DLQ', 'Lỗi xác thực', 'Lỗi cấu trúc dữ liệu'. | Toast |
scr03.toast.copy_json_success | Đã sao chép JSON payload vào clipboard. | Toast |
scr03.validation.date_range_too_wide | Khoảng thời gian tối đa 90 ngày. Vui lòng thu hẹp. | Inline error |
scr03.error.title | Không thể tải audit log. {error}. Mã sự cố: {trace_id}. | Variant E |
B7.5) SCR-04 Health
| Key | Tiếng Việt | Ngữ cảnh |
|---|---|---|
scr04.timeframe.24h | 24 giờ | Toggle |
scr04.timeframe.7d | 7 ngày | Toggle |
scr04.last_updated | Cập nhật cuối: | Header info |
scr04.card.success_rate.title | Tỉ lệ thành công | Metric card |
scr04.card.success_rate.target | Mục tiêu ≥ 99,5% | Metric card target |
scr04.card.latency.title | Độ trễ webhook → ticket (p95) | Metric card |
scr04.card.latency.target | Mục tiêu ≤ 30 giây | Metric card target |
scr04.card.auto_assign.title | Auto-assign telesale (chính xác) | Metric card |
scr04.card.auto_assign.target | Mục tiêu ≥ 90% | Metric card target |
scr04.card.pancake_state.title | Trạng thái Pancake | Metric card |
scr04.card.pancake_state.healthy | Khỏe mạnh | Status |
scr04.card.pancake_state.healthy_helper | Nhận event cuối: | Helper |
scr04.card.pancake_state.outage | Mất kết nối | Status |
scr04.card.pancake_state.outage_duration | Đã {minutes} phút | Helper |
scr04.card.pancake_state.polling_helper | Polling: {seconds}s fallback active | Helper |
scr04.card.target_passed | Đạt | Badge ✓ |
scr04.card.target_warning | Cần xem xét | Badge ⚠ |
scr04.card.btn.detail | Xem chi tiết | Drill-down CTA |
scr04.chart.title | Xu hướng 7 ngày (Tỉ lệ thành công + Số event) | Chart title |
scr04.chart.legend.success_rate | Tỉ lệ thành công | Legend |
scr04.chart.legend.event_count | Event nhận / ngày | Legend |
scr04.outage_history.title | Lịch sử outage (10 gần nhất) | Section |
scr04.outage_history.col.started_at | Bắt đầu | Column |
scr04.outage_history.col.ended_at | Kết thúc | Column |
scr04.outage_history.col.duration | Thời lượng | Column |
scr04.outage_history.col.miss_rescued | Event miss đã rescue | Column |
scr04.outage_history.row.via_polling | (Cron 6 rescue) | Sub-label |
scr04.outage_history.row.via_reconciliation | (Cron 7 rescue) | Sub-label |
scr04.mttr | MTTR (Mean Time To Recovery): {minutes} phút (target ≤ 5 phút) | Footer |
scr04.outage_banner.title | Pancake outage — không nhận event trong 5 phút gần nhất | Variant B banner |
scr04.outage_banner.body | Bắt đầu: {datetime} ({ago} trước). Trạng thái: outage_started. Cron 6 đã kích hoạt adaptive polling fallback (interval {seconds}s). Đã rescue: {N} event miss tới giờ. | Variant B body |
scr04.outage_banner.btn.snooze | Tạm tắt cảnh báo (1h) | Banner CTA |
scr04.outage_banner.btn.detail | Xem chi tiết outage | Banner CTA |
scr04.warning.success_low.title | Tỉ lệ thành công thấp bất thường ({pct}% — bình thường ≥99,5%) | Variant F |
scr04.warning.success_low.body | Khả năng: tăng DLQ trong vài giờ qua. Kiểm tra Tab 3 lọc status 'Lỗi DLQ'. | Variant F body |
scr04.warning.success_low.btn | Sang Tab 3 lọc DLQ | Variant F CTA |
B7.6) SCR-06 Dropdown delta
| Key | Tiếng Việt | Ngữ cảnh |
|---|---|---|
ticket_source_pancake | Pancake CRM | i18n key — dropdown label slot 9 |
sourceDescriptions.ticket_source_pancake | Lead từ Pancake CRM (40+ kênh: FB, Zalo, TikTok, Google Ads, Whatsapp, Shopee...) | sourceDescriptions HTML span hover |
B7.7) Notification copy
| Template | Title (push) | Body | Deeplink |
|---|---|---|---|
noti_ticket_assigned_pancake | Lead mới từ Pancake CRM | Bạn vừa nhận lead Pancake: {customer_name} ({phone}). Nguồn: {source_name}. Phản hồi trong 30 phút. | /crm/tickets/{ticket_id} |
noti_pancake_unmapped_branch | Nguồn Pancake chưa map | Có nguồn '{source_name}' chưa map chi nhánh. {N} ticket đang chờ map. | /settings/pancake-crm/source-routing?filter=unmapped |
noti_pancake_outage | Pancake outage cảnh báo | Pancake không nhận event {duration_min} phút. Đã chuyển sang polling fallback. | /settings/pancake-crm/health |
noti_pancake_outage (recovery) | Pancake đã khôi phục | Outage tổng {duration_min} phút. Đã rescue {N} event miss. | /settings/pancake-crm/health |
B7.8) Mapping thuật ngữ chuẩn (A9) ↔ UI label
| Canonical (A9 PRD) | UI label người dùng thấy | Tooltip/help | Lý do |
|---|---|---|---|
| Pancake CRM | Pancake CRM (giữ EN brand) | "Sản phẩm CRM của công ty Pancake (≠ Pancake POS)" | Brand name |
| Record (Pancake) | "Sự kiện record" (trong UI) hoặc "Lead Pancake" (trong notification) | "Mỗi 'record' Pancake = 1 khách hàng tiềm năng" | Tiếng Việt thân thiện hơn |
| Source (Pancake) | "Nguồn Pancake" hoặc "Nguồn" (trong context) | "Page Pancake (FB page, Zalo OA, TikTok...)" | Phân biệt rõ với "Nguồn ticket" của Diva |
| Idempotency 3-tuple | (không hiện UI — backend only) | — | Không user-facing |
| Smart update Q2.b | "Chỉ tạo ticket khi thay đổi quan trọng" | (chi tiết trong B9) | Q2.b là code name, không nên hiện UI |
| Loose mode | "Để trống (Loose mode)" trong dropdown branch | "Ticket sẽ tạo với chi nhánh NULL, leader assign sau" | Có giải thích trong tooltip |
| Round-robin per branch | "Tự động phân công" trong placeholder | "Hệ thống tự xoay vòng telesale trong chi nhánh" | Đơn giản hơn |
| Advisory lock | (không hiện UI — backend only) | — | — |
| Fail-open | (không hiện UI — backend behavior) | — | — |
| Pancake suspension | "Bị Pancake khóa" (badge) | "Pancake tự khóa khi error >80%/30 phút" | — |
| DLQ replay | "Phát lại sự kiện" (button) + "Lỗi DLQ (chờ phát lại)" (badge) | "Dead Letter Queue — sự kiện lỗi đang chờ xử lý lại" | Phát lại = ngôn ngữ tự nhiên |
| Webhook receiver | (không hiện UI — Settings/Pancake là wrap context) | — | — |
| VIP tag | "Tag VIP (theo tên)" | "Match theo tên tag, không phân biệt hoa thường" | Giải thích NAME (không phải ID) |
| Test mode event | "Test event" + "Bao gồm test events" (checkbox) | "Event đánh dấu is_test=true. KHÔNG tính KPI, tự xóa sau 24h." | — |
| Opt-out marketing | "Khách từ chối marketing" | "Khách có consent_data.marketing=false" | — |
B7.9) Empty / Loading / Error chung
| Mục | Tiếng Việt | Ngữ cảnh |
|---|---|---|
| Loading skeleton | (no copy, skeleton bars) | All variants |
| Error generic | Không thể tải dữ liệu. Vui lòng thử lại. Mã: | All SCR error |
| Network timeout | Không thể kết nối Diva server. Đang thử lại... | Toast banner |
| Permission denied (URL direct) | Bạn không có quyền truy cập Tích hợp Pancake CRM. Liên hệ Admin nếu cần. | Toast /403 |
B8) Sự kiện phân tích (Analytics — BẮT BUỘC L)
Track key user actions để đo adoption, friction setup, success vận hành.
| Sự kiện (event_name) | Điều kiện kích hoạt | Thuộc tính (properties) | KPI liên quan |
|---|---|---|---|
pancake.setup.tab1_opened | Admin mở SCR-01 lần đầu | user_id, branch_id, is_first_time (bool), connection_exists (bool), client_ts, server_ts, correlation_id, feature_flag_state | Adoption funnel: % admin mở Tab 1 |
pancake.setup.test_connection_clicked | Click button "Kiểm tra kết nối" | user_id, attempt_count (session), result ∈ {success, fail_auth, fail_timeout, fail_other}, latency_ms | Setup friction |
pancake.setup.completed | Lần đầu lưu config thành công + có ≥1 source mapped + active | user_id, time_from_first_open_minutes, sources_count | KPI Manager adoption (PRD A8) — target 100% sau W4 |
pancake.source_routing.synced | Click "Đồng bộ từ Pancake" thành công | user_id, sources_total, sources_new, sources_updated, latency_ms | Sources discovery rate |
pancake.source_routing.branch_mapped | Inline-edit branch dropdown autosave thành công | user_id, source_id, branch_id, loose_mode (bool, branch_id IS NULL) | % source mapped (mỗi week) |
pancake.source_routing.toggled | Inline-toggle is_active | user_id, source_id, is_active_new, event_count_24h_before | Pilot rollout tracking W1-W4 |
pancake.audit.filter_applied | User thay đổi filter SCR-03 | user_id, filter_status[], filter_source_count, filter_date_range_days, include_test | Debug UX usage |
pancake.audit.dlq_replay_single | Click "Phát lại sự kiện" single | user_id, event_id, status_before, retry_count_before | DLQ recovery rate |
pancake.audit.dlq_replay_bulk | Bulk replay xong | user_id, bulk_count, success_count, fail_count, latency_ms | Bulk replay efficiency |
pancake.audit.json_copied | Click "Sao chép JSON" | user_id, event_id | Debug pattern |
pancake.health.timeframe_toggled | Toggle 24h/7d | user_id, timeframe_new ∈ {24h, 7d} | Health monitoring usage |
pancake.health.metric_drilldown | Click metric card | user_id, metric_name, target_screen (SCR-03 with filter) | Drill-down usage |
pancake.kill_switch.toggled | Toggle kill switch | user_id, kill_switch_new (bool), reason (free text optional) | Pilot rollback events |
pancake.webhook.outage_detected | Cron 4 detect (backend event, không phải UI) | outage_started_at, event_count_5min_before | KPI outage MTTR (FORMULA-006) |
pancake.webhook.outage_recovered | Cron status → outage_recovered | outage_started_at, outage_ended_at, duration_min, miss_count_rescued | KPI MTTR |
pancake.scr06.dropdown_used | User chọn "Pancake CRM" trong filter Tickets / Form | user_id, role, surface ∈ {tickets_filter, form_add, customer_drawer} | Adoption FE delta |
B8.1) Schema event chuẩn (ref EXT B7A)
json
{
"event_name": "pancake.setup.completed",
"user_id": "uuid",
"branch_id": "uuid",
"portal": "admin",
"correlation_id": "uuid-v4",
"feature_flag_state": "enabled",
"client_ts": "2026-05-15T09:42:18+07:00",
"server_ts": "2026-05-15T09:42:18.345+07:00",
"properties": {
"time_from_first_open_minutes": 15,
"sources_count": 12
},
"event_version": 1
}B8.2) Sampling & retention
| Event | Sampling | Retention |
|---|---|---|
pancake.setup.* | 100% | Hot 90 ngày + warehouse 2 năm |
pancake.source_routing.* | 100% | 90d + 2y |
pancake.audit.* | 100% (critical for compliance) | 90d + 2y |
pancake.audit.dlq_replay_* | 100% (audit-compliance critical) | 90d + 2y |
pancake.health.* | 10% (UI rendering — không critical) | 90d |
pancake.kill_switch.toggled | 100% (vận hành critical) | 90d + 2y |
pancake.webhook.outage_* | 100% (backend) | 90d + 2y |
pancake.scr06.dropdown_used | 10% (UI metric, không critical) | 90d |
B8.3) PII rules
- KHÔNG track raw
phone_number,customer_nametrong analytics event (đã được track trongpancake_webhook_eventaudit DB). - KHÔNG track
api_key,webhook_tokenraw. branch_idOK (không phải PII).correlation_idreuse từ webhook handler để cross-trace.
B9) Từ điển tooltip (BẮT BUỘC M+ tightened, 3-part cho calculated)
3 case BẮT BUỘC tooltip: (1) Calculated/Derived field — 3-part định nghĩa + công thức + ví dụ. (2) Status/Lifecycle term. (3) Icon-only button.
| # | Màn | Field/Icon | Loại | Định nghĩa | Công thức / Điều kiện | Ví dụ | Hiện khi |
|---|---|---|---|---|---|---|---|
| 1 | SCR-01 | Workspace ID | Text term | Mã định danh workspace Pancake CRM (1 workspace = 1 doanh nghiệp dùng Pancake) | Lấy từ Pancake admin > Cài đặt > Tích hợp | ws_diva_2026_a8c3d4f5 | Hover icon ⓘ |
| 2 | SCR-01 | API key | Text term | Chìa khóa xác thực với Pancake REST API (đọc danh sách nguồn) | Lấy từ Pancake admin > Cài đặt > API. Lưu encrypted at rest trong DB Diva | (mask) | Hover icon ⓘ + reveal toggle |
| 3 | SCR-01 | URL webhook | Text term | URL Pancake gửi webhook tới Diva. Paste vào Pancake admin > Tích hợp > Webhook | Tự động gen theo https://diva.com.vn/api/pancake/record/{token} | https://diva.com.vn/api/pancake/record/abc123token | Hover icon ⓘ |
| 4 | SCR-01 | Token webhook | Text term | Token xác thực request từ Pancake. Là phần {token} trong URL webhook | UUID v4 auto-gen. Tạo lại sẽ invalidate URL cũ | (mask) | Hover icon ⓘ + reveal toggle |
| 5 | SCR-01 | Kill switch (Tắt khẩn cấp) | Status | Tắt tổng — toàn bộ event Pancake bị bỏ qua | Khi off (toggle hiện cam) → mọi event chuyển status 'Bỏ qua (đã tắt hệ thống)' | "Tạm dừng nhận lead Pancake trong giờ bảo trì" | Hover icon ⓘ |
| 6 | SCR-01 | Tag VIP | Term | Tag NAME (text, không phân biệt hoa thường) đánh dấu khách quan trọng | Match account.pancake_metadata.pancake_tag_names[] với pancake_connection.vip_tag_names[] lowercase | "VIP", "Hot Lead" → match "vip", "HOT LEAD" cũng OK | Hover icon ⓘ |
| 7 | SCR-01 | Trạng thái Pancake active | Status | Kết nối Pancake đang hoạt động, sẵn sàng nhận event | Vào state khi save config + Kiểm tra kết nối pass | Badge xanh trên header | Luôn hiện trên badge |
| 8 | SCR-02 | Pancake source ID | Text term | Mã định danh page/kênh trong Pancake (readonly) | Đồng bộ tự động qua Cron 3 hourly. KHÔNG sửa được | src_fb_hcm_q1_2024 | Hover icon ⓘ |
| 9 | SCR-02 | Tên nguồn Pancake | Text term | Tên page/kênh hiển thị trên Pancake admin | Đồng bộ qua Cron 3. Nếu Pancake đổi tên → tự update | "FB Diva HCM Q1", "Zalo OA Diva Premium" | (always display) |
| 10 | SCR-02 | Chi nhánh Diva | FK term | Chi nhánh Diva sẽ nhận ticket từ nguồn Pancake này | NULL = Loose mode (leader assign manual) | "Diva HCM Q1" | Hover icon ⓘ + dropdown |
| 11 | SCR-02 | Bật/Tắt (is_active) | Status | Per-source feature flag (DEC-021 mức 3) | Tắt → event status 'Bỏ qua (nguồn đã tắt)' | "Bật cho pilot W1=1 source" | Hover icon ⓘ |
| 12 | SCR-02 | Cập nhật cuối (last_sync_at) | Datetime | Thời điểm Cron 3 sync metadata Pancake gần nhất (KHÔNG phải event nhận cuối) | Auto-update Cron 3 hourly | "15/05/2026 09:30 (12 phút trước)" | (always display) |
| 13 | SCR-03 | Mã sự kiện (event_id) | Text term | UUID identifier của row pancake_webhook_event | PK auto-gen | a8c3d4f5-1f8d-... (hiển thị 8 ký tự đầu) | Hover full ID |
| 14 | SCR-03 | Record ID | Text term | ID record bên Pancake (1 khách = 1 record) | Pancake gửi qua webhook payload | rec_xyz_03 | Hover icon ⓘ |
| 15 | SCR-03 | Trạng thái ingested | Status | Đã nhận (chưa xác thực) — webhook handler vừa INSERT raw event, chưa verify | Vào: INSERT step 1. Ra: verify → received hoặc fail | "Lần đầu thấy" | Badge tooltip |
| 16 | SCR-03 | Trạng thái received | Status | Đã xác thực — pass token + IP + schema + dedup, sẵn sàng cho crm-api xử lý | Vào: webhook UPDATE. Ra: Hasura trigger fire → processing | "Pass verify" | Badge tooltip |
| 17 | SCR-03 | Trạng thái processing | Status | Đang xử lý — crm-api handler đang chạy STEP 0-14 | Vào: handler started. Ra: STEP 14 OK → processed, exception → dead_letter | "Đang tạo account/ticket" | Badge tooltip |
| 18 | SCR-03 | Trạng thái processed | Status | Đã xử lý — hoàn tất tạo account + ticket + notification | Vào: STEP 14 COMMIT. Terminal. | "Lead đã được rescue thành ticket" | Badge tooltip |
| 19 | SCR-03 | Trạng thái auth_failed | Status | Lỗi xác thực — token URL sai | Vào: webhook verify step 2. Terminal — alert ops nếu > 10/min | "Có thể spoof attempt — báo Ops" | Badge tooltip |
| 20 | SCR-03 | Trạng thái ip_blocked | Status | Bị chặn IP — IP nguồn không trong whitelist | Vào: webhook verify step 2 (sau PD-002 resolve). Terminal | "Pancake không gửi từ IP đó — có thể spoof" | Badge tooltip |
| 21 | SCR-03 | Trạng thái parse_error | Status | Lỗi cấu trúc dữ liệu — JSON schema invalid | Vào: webhook verify step 3. Terminal | "Pancake gửi payload sai schema" | Badge tooltip |
| 22 | SCR-03 | Trạng thái skipped_duplicate | Status | Bỏ qua (trùng lặp) — 3-tuple (record_id, modified_on, payload_hash) đã tồn tại | Vào: webhook step 4. Terminal — update last_received_at | "Pancake retry cùng payload" | Badge tooltip |
| 23 | SCR-03 | Trạng thái skipped_source_disabled | Status | Bỏ qua (nguồn đã tắt) — pancake_source_routing.is_active=false | Vào: webhook step 5. Terminal | "Admin đã tắt nguồn này" | Badge tooltip |
| 24 | SCR-03 | Trạng thái skipped_kill_switch | Status | Bỏ qua (đã tắt hệ thống) — app_setting.pancake_integration.enabled=false | Vào: webhook step 5. Terminal | "Admin đã tắt toàn hệ thống" | Badge tooltip |
| 25 | SCR-03 | Trạng thái skipped_opt_out | Status | Bỏ qua (khách từ chối marketing) — customer_consent.marketing=false | Vào: crm-api STEP 7. Terminal | "Compliance — không tạo ticket cho khách opt-out" | Badge tooltip |
| 26 | SCR-03 | Trạng thái dead_letter | Status | Lỗi DLQ — STEP 1-14 exception, chờ admin replay | Vào: handler catch exception. Ra: admin replay → received hoặc retry_count>=3 → permanently_failed | "STEP 9 round-robin fail (no telesale active)" | Badge tooltip |
| 27 | SCR-03 | Trạng thái permanently_failed | Status | Lỗi vĩnh viễn (cần xem xét) — đã retry 3 lần fail | Vào: Cron retry check. Terminal — alert manual review | "Cần Tech Lead xem xét root cause" | Badge tooltip |
| 28 | SCR-03 | Test event (is_test) | Status | Event được đánh dấu test (QA/Dev test) | Pancake payload có is_test=true hoặc URL ?test=1. KHÔNG count KPI. Auto-cleanup 24h | (icon test tube) | Hover icon ⓘ |
| 29 | SCR-03 | Số lần retry (retry_count) | Int | Số lần Cron retry đã thử | Tăng mỗi lần Cron DLQ retry. >= 3 → permanently_failed | "2 / 3" | (cột số) |
| 30 | SCR-03 | Tạo lúc (created_at) | Datetime | Thời điểm webhook handler INSERT row | Auto NOW() | "15/05/2026 09:41:55" | (always display) |
| 31 | SCR-03 | Xử lý lúc (processed_at) | Datetime | Thời điểm status chuyển processed | Auto NOW() khi STEP 14 success. NULL nếu chưa processed | "15/05/2026 09:42:08" hoặc "—" | (always display) |
| 32 | SCR-03 | Thông báo lỗi (error_message) | Text | Exception text từ handler khi dead_letter/permanently_failed | Auto-capture từ catch block | "STEP 9 round-robin failed: no active telesale" | (drawer field) |
| 33 | SCR-03 | Raw payload | JSON | Full body JSON Pancake gửi (lossless audit) | INSERT raw NOT NULL | (xem drawer) | (drawer) |
| 34 | SCR-03 | Raw headers | JSON | Full HTTP headers Pancake gửi | INSERT raw | (xem drawer) | (drawer) |
| 35 | SCR-03 | IP nguồn (source_ip) | INET | IP server Pancake gửi request từ | Resolve qua X-Forwarded-For reverse proxy | 103.20.149.117 | (drawer) |
| 36 | SCR-04 | Độ trễ webhook → ticket (p95) | Calculated | Thời gian từ Pancake gửi webhook đến khi ticket Diva được tạo. Đo trên 95% request trong 24h gần nhất | latency_p95 = percentile_cont(0.95) WITHIN GROUP (ORDER BY (processed_at - created_at)) FROM pancake_webhook_event WHERE status='processed' AND created_at > NOW() - 24h | 100 event 24h, p95 = 12,5s → "12,5s ✓" (target ≤ 30s). Outlier > 5 phút loại khỏi p95. | Hover icon ⓘ |
| 37 | SCR-03 | Khoảng thời gian (date range) | Filter | Cửa sổ thời gian filter event | Default 7 ngày gần nhất. Max 90 ngày để không overload query | "09/05/2026 - 15/05/2026 (7 ngày)" | (always display) |
| 38 | SCR-03 | Phát lại sự kiện (action) | Action | Reset event status về received để Hasura trigger fire lại crm-api handler | Chỉ enable cho status dead_letter, auth_failed, parse_error. KHÔNG enable cho processed (idempotency) hay skipped_* (intentional) | "Replay 1 event DLQ sau khi fix telesale roster" | Hover button |
| 39 | SCR-04 | Tỉ lệ thành công | Calculated | Tỉ lệ event xử lý thành công trên tổng event nhận trong window. Loại trừ skipped_* và test events | success_rate = count(status='processed') / count(status NOT IN ('skipped_*') AND is_test=false) × 100 (3 chữ số thập phân) | 10000 event 24h, 9985 processed, 10 dead_letter, 5 skipped_* → success = 9985/(9985+10) × 100 = 99,900% ✓ (target ≥ 99,5%) | Hover icon ⓘ |
| 40 | SCR-04 | Trạng thái healthy | Status | Bình thường, webhook nhận event đều đặn | (initial state) | Card xanh ✓ | Badge tooltip |
| 41 | SCR-04 | Trạng thái outage_started | Status | Mất kết nối — Cron 4 detect 0 event trong 5 phút (giờ làm việc) | Cron 6 adaptive polling kích hoạt (interval 60-900s) | Banner đỏ "🚨 Mất kết nối" | Badge tooltip |
| 42 | SCR-04 | Trạng thái outage_recovered | Status | Đang phục hồi — webhook nhận event mới, đang sustain 5 phút | Sau 5 phút sustain → chuyển healthy | Banner cam "Đang phục hồi" | Badge tooltip |
| 43 | SCR-04 | Nhận event cuối (last_event_received_at) | Datetime | Thời điểm webhook nhận event hợp lệ gần nhất | Update mỗi event status received | "38 giây trước" | (always display) |
| 44 | SCR-04 | Lịch sử outage | Audit log | Danh sách 10 outage gần nhất với started/ended/duration/miss_rescued | Reuse pancake_outage_state history | (table row) | (always display) |
| 45 | SCR-04 | Polling interval hiện tại | Int (s) | Khoảng cách giữa 2 lần Cron 6 polling Pancake REST /records | Start 60s khi outage_started, tăng dần max 900s (15 phút) | "60s fallback active" | Hover icon ⓘ |
| 46 | SCR-04 | Auto-assign telesale (chính xác) | Calculated | Tỉ lệ ticket Pancake được auto-assign telesale trên tổng ticket Pancake trong 7 ngày | correct_rate = count(ticket WHERE source_id='ticket_source_pancake' AND assignee_id IS NOT NULL AND created_by='system_pancake_webhook') / count(ticket WHERE source_id='ticket_source_pancake') × 100 (2 chữ số thập phân) | 1000 ticket 7 ngày, 920 có assignee → 920/1000 × 100 = 92,00% ✓ (target ≥ 90%) | Hover icon ⓘ |
| 47 | SCR-01 | Kiểm tra kết nối (button) | Action | Gọi Pancake REST GET /workspaces/{ws}/sources để verify api_key | Disabled khi workspace_id hoặc api_key trống. Timeout 30s | "Phát hiện 12 nguồn" | Hover button |
| 48 | SCR-01 | Sao chép URL (icon) | Action | Copy URL webhook vào clipboard | navigator.clipboard.writeText | "Đã sao chép" toast | Hover icon |
| 49 | SCR-01 | Tạo lại token (button) | Action | Tạo token mới + invalidate URL cũ | Confirm 2 bước gõ 'TẠO LẠI'. Sau đó admin phải update Pancake admin | (destructive) | Hover button |
| 50 | SCR-01 | Kích hoạt lại webhook (button) | Action | Reset pancake_connection.status từ suspended_by_pancake về active | Chỉ hiện khi suspended. Yêu cầu admin đã bật lại bên Pancake trước | (conditional) | (only Variant F) |
| 51 | SCR-02 | Đồng bộ từ Pancake (button) | Action | Manual call Cron 3 logic — pull source list từ Pancake REST | Loading 5-30s. Update last_sync_at + auto-insert source mới | "Phát hiện 12 nguồn (2 nguồn mới)" | Hover button |
Quy tắc lock-in:
- 100% Calculated/Derived field (rows 36, 39, 46) có 3-part đầy đủ
- 100% Status text trong wireframe (rows 7, 11, 15-27, 28, 40-42) có row B9 giải thích
- 100% Icon-only button (rows 47-51) có row B9 + aria-label tương ứng (xem B-Microcopy)
- Mọi cell
Định nghĩa≤80 ký tự — chi tiết → ref A9 PRD hoặc dev-spec C3- Mọi cell
Ví dụcó số liệu thật (không placeholder)
B-Microcopy) Quy ước vi-copy form
| Mẫu | Quy ước SCR-01..06 |
|---|---|
| Required marker | * đỏ sau label cho workspace_id, api_key ở SCR-01 |
| Optional marker | KHÔNG dùng (Tuỳ chọn) — context rõ ràng |
| Help text | Dưới field VIP tag textarea: "Mỗi dòng = 1 tag..." |
| Tooltip | Hover icon ⓘ; chi tiết phụ; ref B9 |
| Char count | {X}/100 dòng ở textarea VIP tag; đỏ khi >90% |
| Placeholder | "Mã workspace Pancake", "Nhập API key từ Pancake admin", "Tìm theo tên nguồn..." |
| Inline error | "API key phải ≥16 ký tự.", "Workspace ID không hợp lệ. Vui lòng kiểm tra (≤64 ký tự, UUID Pancake)." |
| Inline success | "Đã đồng bộ. Phát hiện 12 nguồn." (toast — không dùng inline) |
| Reveal password | Icon mắt 👁; tooltip "Hiện/Ẩn API key" |
| Auto-format | Trim whitespace + lowercase tag NAME khi blur |
| Reset field | Search input có nút × clear; date range picker "Đặt lại" |
| aria-label icon-only | Icon Sao chép URL: aria-label "Sao chép URL webhook". Icon Sao chép token: "Sao chép token webhook". Icon mắt: "Hiện/Ẩn API key". Icon ⓘ: "Xem giải thích". Icon Phát lại: "Phát lại sự kiện". Icon ⋮: "Tùy chọn khác" |
B-Voice) Quy ước giọng thương hiệu
| Tình huống | Tone | Ví dụ đúng | Ví dụ sai |
|---|---|---|---|
| Hướng dẫn setup | Trang trọng, hỗ trợ | "Vui lòng hoàn tất Tab 1 (Kết nối) trước khi map nguồn → chi nhánh." | "Phải làm Tab 1 trước nha bạn!" |
| Toast thành công | Khẳng định, súc tích | "Đã lưu cấu hình Pancake CRM." | "Yay, lưu xong rồi!" |
| Banner cảnh báo outage | Khẩn cấp, rõ ràng | "Pancake outage — không nhận event trong 5 phút gần nhất" | "Có lỗi gì đó với Pancake" |
| Lỗi user-induced | Trung tính, hướng dẫn | "API key phải ≥16 ký tự." | "API key sai rồi" |
| Lỗi system | Xin lỗi nhẹ + trace | "Không thể lưu. Mã sự cố: TRC-2026-05-15-a8c3d4. Vui lòng báo bộ phận Ops với mã." | "Server lỗi không xác định" |
| Cảnh báo destructive | Rõ ràng, có tác động | "Token mới sẽ vô hiệu hóa cấu hình Pancake hiện tại. Gõ 'TẠO LẠI' để xác nhận." | "Bạn có chắc không?" |
| Hỏi xác nhận | Lễ phép, rõ tác động | "Tắt sẽ ngừng nhận lead Pancake. Mọi event sẽ chuyển trạng thái 'Bỏ qua (đã tắt hệ thống)'. Xác nhận?" | "Tắt thật à?" |
| Empty state | Hướng dẫn next step | "Chưa có nguồn nào. Click 'Đồng bộ từ Pancake' để bắt đầu." | "Không có dữ liệu" |
Quy tắc chung:
- KHÔNG dùng emoji trong copy hành chính (form label, error, validation). Cho phép dùng badge icon trong wireframe.
- KHÔNG viết tắt nghệ thuật ("kb", "đc")
- KHÔNG dùng English UI ("Submit", "Cancel", "OK") — dùng "Lưu", "Hủy", "Xác nhận"
- "Bạn" cho user nội bộ (Admin) — formal-professional
- Câu chủ động, ngắn gọn
B-i18n) Quy ước quốc tế hóa / format
| Khía cạnh | Quy ước Diva (vi-VN) |
|---|---|
| Ngày | DD/MM/YYYY (KHÔNG MM/DD) |
| Ngày + giờ | DD/MM/YYYY HH:mm hoặc DD/MM/YYYY HH:mm:ss (audit log) — 24h |
| Giờ | HH:mm:ss |
| Timezone | Asia/Ho_Chi_Minh (UTC+7); KHÔNG có DST |
| Số nguyên | 1.500 (dấu chấm phân ngàn) |
| Số thập phân | 12,5 (dấu phẩy thập phân) |
| Phần trăm | 99,950% (3 thập phân cho success rate), 92,00% (2 thập phân cho auto-assign), 37,50% (2 thập phân chung) |
| Thời gian khoảng (latency) | 12,5s (1 thập phân), 4,40 phút (2 thập phân) |
| SĐT | +84 912 345 678 hoặc 0912 345 678 (cách nhóm 4-3-3) |
| Tên KH | Capitalize từng chữ; giữ dấu (Nguyễn Thị Mai, Đặng Thị Ánh Sương) |
| Sort tiếng Việt | Collation vi_VN (Postgres) |
| Tooltip "ago" | "38 giây trước", "12 phút trước", "2 giờ trước", "3 ngày trước" — relative format |
B-i18n canonical keys (vi.ts module crm)
typescript
// diva-admin/src/modules/crm/i18n/vi.ts (line 130-137 — chỉ delta)
export default {
// ... existing keys ticket_source_1 .. ticket_source_8
ticket_source_pancake: 'Pancake CRM', // NEW slot 9
};B-i18n canonical keys (vi.ts module settings — NEW)
typescript
// diva-admin/src/modules/settings/i18n/vi.ts (NEW section pancake)
export default {
// ... existing settings keys
pancake: {
sidebar: { menu: 'Tích hợp Pancake CRM' },
scr05: { header: 'Tích hợp Pancake CRM' },
tab: {
connection: 'Kết nối',
source_routing: 'Map nguồn',
audit: 'Lịch sử + DLQ',
health: 'Sức khỏe',
},
scr01: { /* ... keys từ B7.2 ... */ },
scr02: { /* ... keys từ B7.3 ... */ },
scr03: { /* ... keys từ B7.4 ... */ },
scr04: { /* ... keys từ B7.5 ... */ },
common: {
loading: 'Đang tải',
no_data: 'Chưa có dữ liệu',
error: 'Không thể tải dữ liệu. Vui lòng thử lại.',
},
},
};B-i18n EN fallback (en.ts — minimal)
typescript
// diva-admin/src/modules/settings/i18n/en.ts
pancake: {
sidebar: { menu: 'Pancake CRM Integration' },
// ... brand name giữ EN
}Quy tắc: Diva chủ yếu vi-VN. en.ts giữ minimal fallback cho nhân sự nước ngoài (rare case Admin). MVP-1 SCR-01..05 chỉ cần vi-VN đầy đủ.
B-Versioning) Quy ước UI cho feature flag / staged rollout
Feature deploy theo pilot 4 tuần (DEC-022).
| Tình huống | Quy ước UI |
|---|---|
| User ngoài pilot (W1-W3) | (Admin Diva luôn trong pilot — không có concept "user ngoài"). Telesale ngoài source W1 → không thấy ticket source Pancake CRM (vì source is_active=false) |
| User trong pilot | Hiện feature đầy đủ |
| Staged rollout per-source | DEC-021 mức 3 pancake_source_routing.is_active — Admin toggle per row SCR-02 |
| Kill switch tổng | DEC-021 mức 1 app_setting.pancake_integration.enabled — SCR-01 toggle |
| Rollback W2→W1 | Admin tắt is_active của 4 source ở W2 → quay về 1 source W1; không ảnh hưởng ticket đã tạo |
| Migration period | KHÔNG có old UI vs new UI (feature mới hoàn toàn) — Settings/Pancake là route mới |
KHÔNG có version doc cho settings (live edit): thay đổi config (workspace_id, vip_tag_names, branch mapping) áp dụng ngay — KHÔNG có draft/review workflow. Lý do: số lượng admin nhỏ (1-3 người), audit log đủ tracking; nếu sai → rollback nhanh bằng inline-edit ngược lại. Kill switch là rollback nhanh nhất khi cần.
B-Help) Quy ước điểm hỗ trợ / Help Touchpoint
| Khía cạnh | Quy ước |
|---|---|
| Help link cuối SCR-01..04 | Footer mỗi tab: "Cần hỗ trợ? → [Xem hướng dẫn Tích hợp Pancake CRM]" — deeplink https://docs.diva-group.com/admin-guide/integrations/pancake-crm |
| Tooltip ⓘ | Mọi field/term phức tạp có icon ⓘ + tooltip B9 |
| Onboarding tour | Lần đầu Admin vào SCR-05 (Variant B SCR-01 first-time) → tour 4 step: (1) Welcome card SCR-01, (2) Sau khi save lần đầu → auto-redirect SCR-02 + tooltip "Tiếp theo: map nguồn", (3) Sau map ≥1 source → tooltip SCR-04 "Verify health", (4) Done |
| Empty state with hint | Mỗi empty state có CTA + link "Xem hướng dẫn" |
| Error with support hint | Error 5xx kèm correlation_id + "Báo bộ phận Ops với mã: {trace_id}" |
| Contact support | Floating "Cần hỗ trợ?" button góc phải dưới chỉ ở SCR-05 (defer MVP-2 if needed) |
| Feedback channel | "Phản hồi tính năng" link footer Settings (defer) |
| Runbook ops | DevOps runbook: https://docs.diva-group.com/runbook/pancake-integration — link từ banner outage Variant B SCR-04 |
B-Trường hợp cá biệt (Edge Cases — 12 nhóm G1-G12 BẮT BUỘC L)
Nhóm G1 — Quyền và phân quyền
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G1.1 | Manager gõ URL /settings/pancake-crm trực tiếp | Route guard redirect /403 + toast "Bạn không có quyền truy cập Tích hợp Pancake CRM. Liên hệ Admin nếu cần." |
| G1.2 | Admin bị thu hồi quyền pancake_crm_integration.access giữa session | Request kế tiếp 401/refetch → menu Sidebar ẩn ngay; nếu đang ở SCR-01..04 → redirect home + toast "Quyền đã thay đổi" |
| G1.3 | Admin được cấp view_all nhưng KHÔNG có replay_dlq | SCR-03 list xem được + drawer xem được; button "Phát lại" hidden hoàn toàn (kể cả bulk select bar) |
| G1.4 | Admin chuyển sang Manager role giữa session đang xem SCR-03 | API request kế trả 401/403 → redirect + toast |
| G1.5 | Multi-role (Admin + Manager) | Effective permission = union; xem được SCR-01..04 + ticket dashboard Manager |
| G1.6 | Dynamic Permission UI override (defer MVP-2) | Default seed Admin only; admin có thể grant view_all cho Marketing role qua DP UI sau — UI Spec MVP-1 chỉ chuẩn bị structure |
Nhóm G2 — Mạng và độ tin cậy
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G2.1 | Mất mạng giữa "Lưu cấu hình" SCR-01 | Banner "Mất kết nối. Đã lưu nháp local IndexedDB. Đang thử lại..." (autoretry); KHÔNG mất form data |
| G2.2 | Có mạng lại sau outage 30s | Auto-retry queue → toast "Đã đồng bộ thay đổi" |
| G2.3 | Pancake REST /sources timeout 30s khi "Đồng bộ" | Banner "Pancake không phản hồi. Hiển thị cache. Thử lại sau 30 phút." (Variant E SCR-02) |
| G2.4 | Diva API 5xx khi load SCR-03 | Toast "Có lỗi từ hệ thống. Mã: {trace_id}." + nút Thử lại |
| G2.5 | Rate limit Pancake 429 khi "Kiểm tra kết nối" gọi nhanh | Banner đếm ngược "Bạn đã thử quá nhanh. Vui lòng chờ {N}s." |
| G2.6 | Bulk replay 100 event timeout giữa chừng | Progress dừng + banner "Đã thực hiện X/100. {Y} còn lại sẽ retry tự động." |
Nhóm G3 — Dữ liệu biên / boundary
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G3.1 | Pancake gửi record_id rất dài (>200 ký tự) | Cột Record ID truncate ... cuối với tooltip full ID; drawer hiển thị full text |
| G3.2 | 1M+ event trong audit log (≥1 năm retention) | Server-side pagination + index (created_at, status) — không client filter > 5k row |
| G3.3 | 1000+ source routing rows (extreme) | Pagination 100/page; defer MVP-2 nếu thực tế cần (volume hiện 40-50 source) |
| G3.4 | VIP tag list 100+ entries | Block keystroke + char counter đỏ ">90%" + toast 1 lần "Tối đa 100 dòng" |
| G3.5 | Tên KH có dấu Unicode (Đặng Thị Ánh Sương) | Render đúng dấu trong wireframe + drawer; sort theo collation vi_VN |
| G3.6 | Tên source Pancake có emoji (vd "FB Diva 💎 Premium") | Render thuần text (emoji OK) — không filter; sort theo Unicode |
| G3.7 | SĐT Pancake gửi với prefix +84 vs 0 | Hiển thị normalized E.164 trong drawer raw_payload; UI label dùng +84 912 345 678 |
| G3.8 | Workspace_id rỗng → Save | Inline error "Workspace ID là bắt buộc." |
| G3.9 | API key 1000 ký tự (vượt giới hạn) | Block keystroke max 64 + toast |
| G3.10 | Branch dropdown: 50+ branches | Searchable dropdown (debounce 200ms); render top 20 mặc định |
Nhóm G4 — Thời gian / múi giờ
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G4.1 | Cron 4 chạy lúc đổi giờ DST (Asia/HCM không có DST) | N/A — VN không có DST; consistent UTC+7 |
| G4.2 | due_date today 23:59:59 Asia/Ho_Chi_Minh khi webhook đến lúc 23:58 | Ticket due_date = today 23:59:59 → telesale có 1 phút để xử lý? → defer rule: nếu webhook đến sau 22:00 → due_date = tomorrow 23:59:59 (out of scope UI Spec, ref dev-spec STEP 10) |
| G4.3 | Year boundary 31/12 → 01/01 trong audit log | Filter date range cross-year hoạt động đúng |
| G4.4 | last_event_received_at hiển thị "1 năm trước" | Relative format "1 năm trước" (không absolute) |
| G4.5 | Outage kéo dài > 1 giờ vắt sang ngày mới | History row hiển thị 2 datetime đầy đủ (started 23:55 → ended 01:30 hôm sau, duration 95 phút) |
Nhóm G5 — Concurrency
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G5.1 | 2 Admin cùng mở SCR-01 đang edit | Banner presence "Đang chỉnh sửa cùng: NGUYEN VAN A" cuối form |
| G5.2 | A save SCR-01 trước, B save sau | B nhận modal conflict "Cấu hình đã thay đổi. [Tải lại] / [Ghi đè]" |
| G5.3 | 2 Admin cùng inline-edit cùng row SCR-02 | A save trước → optimistic update B; B save thấy banner "Bản ghi đã đổi. [Tải lại]" |
| G5.4 | 2 Admin cùng replay 1 event SCR-03 | A click trước → status đã chuyển received; B click thấy "Sự kiện đã được phát lại bởi {A} lúc {time}" |
| G5.5 | Admin mở 2 tab cùng SCR-03 → bulk replay tab 1, tab 2 view | BroadcastChannel notify tab 2 "Đang có bulk replay từ tab khác" |
| G5.6 | Admin A toggle kill switch off, A2 cùng thấy kill switch hiện vẫn on | Realtime polling 60s → A2 thấy update sau ≤60s; KHÔNG cần WebSocket cho settings (DEC-014 PD-009) |
Nhóm G6 — Validation
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G6.1 | api_key 15 ký tự (under min 16) | Inline error "API key phải ≥16 ký tự." + focus field |
| G6.2 | api_key đúng format nhưng Pancake reject | Banner đỏ "API key sai. Kiểm tra trên trang Pancake admin." + correlation_id |
| G6.3 | Pancake gửi payload schema mới (field unknown) | Backend tolerant — vẫn process; field unknown lưu trong raw_payload; log warning |
| G6.4 | Phone Pancake format edge 912abc345 | Backend libphonenumber parse fail → log warning, fallback raw; UI drawer hiển thị raw text |
| G6.5 | Date range filter SCR-03 > 90 ngày | Inline error "Khoảng thời gian tối đa 90 ngày." + reset về 7 ngày |
| G6.6 | VIP tag mỗi dòng > 60 ký tự | Inline warning "Tag dài bất thường — có thể không match. Kiểm tra lại?" |
Nhóm G7 — i18n / locale
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G7.1 | Admin browser locale en-US | Diva admin default vi-VN; en-US user thấy en.ts fallback minimal — primary VN |
| G7.2 | Source name Unicode (FB Diva ★ Premium) | Render đúng Unicode; sort vi_VN collation |
| G7.3 | Branch name có dấu (Diva HCM Cầu Giấy) | Dropdown render đúng dấu; search debounce match dấu/không dấu (TBD-verify) |
Nhóm G8 — Print / Export
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G8.1 | Export audit log SCR-03 (defer MVP-2) | UI placeholder button "Xuất Excel"; alert "Tính năng đang phát triển — sẽ có ở MVP-2" |
| G8.2 | Bulk export 100k row | Defer MVP-2 — async background job pattern (ref B5A Export Deep Contract) |
| G8.3 | Print PDF outage history | Defer MVP-2 |
Nhóm G9 — Accessibility
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G9.1 | Keyboard only nav SCR-01 | Tab order: workspace_id → api_key → reveal eye → Test → URL copy → token copy → token reveal → token regenerate → kill switch → VIP textarea → Cancel → Save; Esc đóng confirm modal |
| G9.2 | Screen reader cho drawer SCR-03 | role="dialog" + aria-labelledby="drawer-title"; focus trap; close khi Esc |
| G9.3 | Color contrast badge status | Mọi badge status có icon + text (không chỉ dựa vào màu); contrast ≥4.5:1 WCAG AA |
| G9.4 | aria-live cho toast | Toast success: aria-live="polite"; error: aria-live="assertive" |
| G9.5 | Banner outage SCR-04 | role="alert" + auto-announce screen reader |
Nhóm G10 — Mobile / responsive
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G10.1 | SCR-01..05 trên tablet (768-1024px) | Responsive: tab strip horizontal scroll nếu cần; form 1 column; drawer SCR-03 full width |
| G10.2 | SCR-01..05 trên mobile (<768px) | OUT OF SCOPE MVP-1 — Diva admin desktop-first; mobile defer MVP-2 (Pancake settings không cần mobile vì Admin desktop) |
| G10.3 | SCR-06 dropdown trên mobile crm (telesale Flutter app) | OUT OF SCOPE UI Spec này — Flutter app có UI riêng (handled bởi diva-flutter team) |
Nhóm G11 — Search / Filter
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G11.1 | Date range SCR-03 > 90 ngày | Inline error + reset; lý do: performance (query 1M+ row) |
| G11.2 | Filter combo phức tạp (5 status + 10 source + date 30d + checkbox test) | Server-side query; auto-apply debounce 500ms; loading skeleton |
| G11.3 | Search source SCR-02 không khớp | Empty filter "Không có nguồn nào khớp." + CTA "Xóa bộ lọc" |
| G11.4 | Search trong raw_payload SCR-03 drawer | Defer MVP-2 (text search JSON cần Postgres tsvector index) |
Nhóm G12 — Business
| # | Trường hợp | Hành vi kỳ vọng |
|---|---|---|
| G12.1 | Pancake outage > 1h | Banner SCR-04 vẫn đỏ; admin có thể bấm "Tạm tắt cảnh báo (1h)" nếu đã nắm; KHÔNG auto-recovery cho đến khi webhook nhận event mới |
| G12.2 | Diva DB downtime — webhook không INSERT được | webhook trả 500 (Pancake retry); KHÔNG count vào outage detection vì Cron 4 cũng không chạy được; recovery khi DB lên lại |
| G12.3 | Manual intervention cần (vd permanently_failed >10 row) | Tech Lead xem qua SCR-03 filter status permanently_failed; mỗi event 1 lý do khác; KHÔNG có bulk action — fix root cause + manual reset từ DB |
| G12.4 | Pilot rollback W3 → W1 | Admin tắt 14/15 source ở SCR-02 toggle is_active → còn 1 source active |
| G12.5 | Pancake suspension > 24h (lỡ rule) | Variant F SCR-01 hiển thị tiếp; Cron 6 polling vẫn rescue được lead trong window; Cron 7 reconciliation catch miss |
| G12.6 | Khách opt-out marketing rồi opt-in lại | Webhook đến → STEP 7 query customer_consent.marketing → nếu mới opt-in → tạo ticket bình thường (DEC-014 không có grace period) |
| G12.7 | Source được map sang branch không active (branch.disabled=true) | Defer MVP-2 — UI cảnh báo "Branch đã vô hiệu hóa" trong dropdown; ticket vẫn tạo với branch_id (data integrity, leader manual xử lý) |
B-POST) Verification (BẮT BUỘC trước DELIVER)
B-POST.1) Checkpoint completeness (As-Is + Delta)
- [x] As-Is đầy đủ: B0.1 inventory bao phủ 100% UI hiện hữu của Tickets + TicketMultipleAdd + CustomerTicketManager (3 màn SCR-06 delta đụng) + Settings sidebar — kèm Evidence file:line từ EVIDENCE_PACK
- [x] Delta status đầy đủ: 11/11 row B0.1.A-D có Delta Status (
KEEP/UPDATE/NEW) - [x] Field × Surface matrix B0.4 (4 nhóm A-E, 30+ field) — mọi cell có giá trị
- [x] State × Screen matrix B0.5: 6 state canonical × 6 SCR × variant (Admin/non-admin)
- [x] Reuse classification: SCR-01..04 = 🆕 Build mới, SCR-05 = ✅ Reuse XDetailLayout, SCR-06 = 🔧 Extend
- [x] Wireframe context: SCR-06 vẽ vùng KEEP (filter panel + table) xung quanh UPDATE (slot 9 dropdown)
- [x] UX principles: mỗi SCR có B2.x.1A (5-giây, ưu tiên, dữ liệu nhạy cảm, pending, canonical term, intent token, no-suy-diễn)
- [x] Variant coverage: B2.x.2 có A (default) + B (empty/first-time) + C (no-perm) + D (loading) + E (error) + F (lifecycle special) per SCR
- [x] Role × Variant: B5.1 + EXT-4 B5.4 không mâu thuẫn PRD
- [x] CTA hierarchy: mỗi SCR có primary CTA + secondary + denied feedback
- [x] Validation đầy đủ: B0.4 mọi field input có rule + error copy
- [x] Permission matrix B5: cover module × action × role × portal × denied feedback
- [x] Confirm modal: destructive actions (Tạo lại token, kill switch off, vô hiệu hóa source có traffic, bulk replay) có copy đầy đủ
- [x] Empty với CTA: SCR-02 Variant B + SCR-03 Variant B đều có CTA next step
- [x] Format VN: tiền không applicable, ngày
DD/MM/YYYY, giờHH:mm:ss, %99,950%/92,00%/37,50% - [x] Design-token intent: KHÔNG hard-code color/font; dùng
primary emphasis/success/warning/negative-value/muted
B-POST.2) Cross-spec consistency check
- [x] Mọi field B0.4 xuất hiện trong PRD FR-002 (audit fields), FR-012 (settings UI fields), FR-013 (REST fields), FR-014 (outage state fields), FR-018 (FE delta fields)
- [x] Mọi field B0.4 xuất hiện trong dev-spec C4 data model (4 bảng pancake_* + ALTER account + ticket master data + customer_consent + contact_book)
- [x] Mọi action mới (Phát lại, Đồng bộ, Kiểm tra kết nối, Tạo lại token, Kích hoạt lại) sẽ có TC trong qa-test-plan D4
- [x] Mọi state lifecycle B6.1 (12 state) + B6.2 (3 state) sẽ có TC trong qa-test-plan
- [x] Mọi variant B2.x.2 (A-F) có PRD AC hoặc UI-only state đã ghi rõ
- [x] Mọi variant có QA TC ref hoặc N/A có lý do
- [x] Role/permission B5 + B5.4 EXT-4 khớp PRD Z DEC-016 + Permission Matrix
- [x] FE delta SCR-06 (3 file) khớp PRD FR-018 + dev-spec C2 Impact Map
B-POST.3) Lint nhanh
bash
SLUG=pancake-crm-integration
# 1. Không có style hard-code / English UI copy
rg -n "cam đỏ|màu đỏ|font-size|font-weight|Submit|Confirm|Loading|No data|Forbidden|Save Now|Delete|Edit Now|View Now" docs/features/$SLUG/ui-spec.md || true
# 2. Không TBD/Lorem
rg -n "TBD|TODO|Lorem|\{placeholder\}|XXX|FIXME" docs/features/$SLUG/ui-spec.md || true
# 3. Không dịch enum vào diagram
rg -n "stateDiagram.*đã xác thực|stateDiagram.*đã xử lý" docs/features/$SLUG/ui-spec.md || true
# 4. Role/permission flow nhất quán
rg -n "Manager.*Pancake|Telesale.*Settings" docs/features/$SLUG/ui-spec.md | grep -v "hidden" || true
# 5. "Click" verb chưa Việt hóa
rg -n '"Click"' docs/features/$SLUG/ui-spec.md || true
# 6. Format VN sai
rg -n '\$[0-9]|MM/DD/YYYY|hh:mm A|[0-9],[0-9]{3}đ' docs/features/$SLUG/ui-spec.md || true
# 7. Schema NOT NULL field mà UI cho phép null
rg -n "_id=NULL|=NULL OK" docs/features/$SLUG/ui-spec.md
# → chỉ B0.8 có `branch_id NULL` (Loose mode hợp lệ — schema allow NULL)B-POST.4) Cross-spec diff bot
bash
SLUG=pancake-crm-integration
# Diff PRD FR vs UI SCR
PRD_FR=$(rg -o '^### FR-[0-9]+' docs/features/$SLUG/prd.md | sort)
UI_SCR=$(rg -o 'SCR-0[1-6]' docs/features/$SLUG/ui-spec.md | sort -u)
# Expected: 18 FR, 6 SCR. FR-012 → SCR-01..05. FR-018 → SCR-06. FR-001..011/013..017 = no UI.
# Mỗi DEC trong SOURCE_OF_TRUTH phải ref ở ui-spec hoặc note "N/A backend only"
DECs=$(rg -o 'DEC-0[0-2][0-9]' docs/features/$SLUG/SOURCE_OF_TRUTH.md | sort -u)
for dec in $DECs; do
rg -q "$dec" docs/features/$SLUG/ui-spec.md && echo "$dec: OK" || echo "$dec: MISSING (verify N/A)"
done
# Mỗi permission action trong UI Spec phải có ở dev-spec C8 Security
rg -o "pancake_crm_integration\.[a-z_]+" docs/features/$SLUG/ui-spec.md | sort -u
# Expected: access, create, update, delete, view_all, replay_dlq (6 actions)DEC coverage ui-spec: DEC-016 (Tab structure ✓), DEC-021 (3-mức flag ✓ B5/SCR-01/SCR-02), DEC-017 (slot 9 ✓ SCR-06), DEC-008 (VIP tag NAME ✓ B9 row 6), DEC-009 (test event ✓ SCR-03), DEC-010 (notification ✓ B4), DEC-014 (opt-out ✓ B6.1 status), DEC-024 (3 template ✓ B4), DEC-015 (4-layer recovery ✓ SCR-04 + SCR-03 DLQ replay), DEC-025 (reconciliation ✓ B6.1 event_type), DEC-013 (Loose mode ✓ SCR-02). DEC backend-only (002/003/004/005/006/011/012/018/019/020) đã note "N/A — backend only".
B-QUALITY) Rà soát rủi ro thiếu sót (≥30 rủi ro UI — BẮT BUỘC L)
Rủi ro QA thường gặp
| # | Rủi ro | Đã cover ở section |
|---|---|---|
| 1 | Thiếu test cho field mới | B0.4 (Field × Surface 30+ field với Required/Format/Business rule) |
| 2 | Thiếu state empty/loading/error | B0.5 (6 state × 6 SCR × variant) + B2.x.2 Variant Matrix |
| 3 | Thiếu permission denied test | B5.1 (mọi action có denied feedback hidden cho non-admin) + B5.4 EXT-4 field-level |
| 4 | Thiếu confirm modal | B2.SCR-01.10 (Tạo lại token, kill switch), SCR-02.10 (Loose mode, disable with traffic), SCR-03.10 (replay, bulk) |
| 5 | Thiếu format VN | B-i18n + B2.SCR-04 metric cards (99,950% / 12,5s / 92,00%) |
| 6 | Boundary value không cụ thể | B0.4 max 100 dòng VIP tag, ≥16 ký tự api_key, ≤64 ký tự workspace_id, date range ≤90 ngày |
Rủi ro UI/UX thường gặp
| # | Rủi ro | Đã cover ở section |
|---|---|---|
| 7 | Thêm field không nói nằm đâu | B0.4 cột "List/Detail/Export/Search" + B2.x.6 bảng cấu hình cột với Vị trí |
| 8 | Wireframe không vẽ vùng KEEP | B2.SCR-06.2 gắn vào layout Tickets/TicketMultipleAdd/CustomerTicketManager existing |
| 9 | Field không có default | B0.4 cột Required + Format; mọi field có default rõ ràng (date range = 7 ngày, status filter = "Tất cả", include_test = unchecked) |
| 10 | Empty state không có CTA | B2.SCR-02 Variant B + B2.SCR-03 Variant B đều có CTA "Đồng bộ" / "Mở rộng khoảng thời gian" |
| 11 | Modal không nói trigger | B2.SCR-01.10 + SCR-02.10 + SCR-03.10 mọi confirm modal có trigger event rõ |
| 12 | Filter không nói default | B2.SCR-03.5 default 7 ngày, status "Tất cả", source "Tất cả", include_test = false |
| 13 | Chỉ có happy-path wireframe | B2.x.3 wireframes có Variant A (default) + B (empty) + D (loading) + E (error) + F (special state) |
| 14 | Role/permission flow lệch nhau | B5.1 + B-POST.2 + B-POST.4 cross-spec diff |
| 15 | UI spec hard-code style | B2.x.1A nguyên tắc dùng intent token (primary/success/warning/negative-value/muted) |
Rủi ro PO/BA thường gặp
| # | Rủi ro | Đã cover |
|---|---|---|
| 16 | Spec mới ghi đè behavior cũ mà không khai báo | B0.1 Delta Status KEEP/UPDATE/NEW/HIDE/REMOVE cho 100% UI hiện hữu |
| 17 | Out-of-scope không rõ | B-Versioning (mobile out scope, export defer MVP-2), G10 mobile out, G11 search payload defer |
| 18 | Decision không có ≥2 phương án | Ref PRD Z 25 DEC (mỗi DEC có alternatives) — UI Spec không re-list |
Rủi ro chuyên sâu
| # | Rủi ro | Đã cover |
|---|---|---|
| 19 | Wireframe ASCII vỡ alignment | B0.6 Wireframe Quality Contract |
| 20 | Search-replace lỗi enum vào diagram | B0.7 Code/Display Bilingual Pairing (12+3+4+3 = 22 cặp) |
| 21 | UI cho phép null field schema NOT NULL | B0.8 Schema Cross-Check — chỉ Loose mode branch_id NULL hợp lệ |
| 22 | Stepper số bước inconsistency | N/A — feature không có wizard nhiều bước |
| 23 | Form thiếu autosave/paste/IME contract | B2.SCR-01.7B Form Interaction Deep |
| 24 | Concurrency lờ đi | B2.SCR-01.7C + SCR-02.7C + SCR-03.7C + EXT-12 reference |
| 25 | Mạng yếu / offline không ghi | B2.SCR-01.7D + B-EdgeCases G2 |
| 26 | Lỗi gộp chung "Có lỗi xảy ra" | B2.SCR-01.10A Error Taxonomy + SCR-03.10A |
| 27 | Print/PDF thiếu page break / version stamp | N/A — feature không có in pháp lý |
| 28 | Token UI thiếu countdown / revoke | N/A — không phải token portal |
| 29 | File upload thiếu chunk/retry | N/A — không có upload |
| 30 | Search debounce ad-hoc | B2.SCR-02.7E rule 5 + SCR-03.7E rule 5 (debounce 300ms search, 500ms date range) |
| 31 | Bulk action thiếu undo / partial | B2.SCR-03 bulk replay có progress + partial success + per-row failure detail (EXT-13) |
| 32 | Export thiếu masking theo quyền | Defer MVP-2 — đã note ở G8 + B5.4 EXT-4 |
| 33 | Copy length vỡ layout | B6A reference; copy đã ngắn gọn cho buttons (≤24 ký tự VI), tabs (≤16 ký tự VI) |
| 34 | Format VN sai | B-i18n + lint B-POST.3 |
| 35 | Brand voice không nhất quán | B-Voice tone formal-professional toàn bộ |
| 36 | Microcopy form lộn xộn | B-Microcopy |
| 37 | A11y bỏ sót | G9 (5 rule keyboard, screen reader, contrast, aria-live, role=alert) |
| 38 | Performance không nói | B0.4 (audit log capacity 100k/tháng), G3.2 (1M+ event server-side pagination), G3.10 (50+ branches searchable) |
| 39 | Edge case không phân nhóm | B-EdgeCases 12 nhóm G1-G12 với ≥5 case mỗi nhóm |
| 40 | Feature flag / staged rollout không có UI | B-Versioning + DEC-021 3-mức (kill switch SCR-01, per-source SCR-02) |
| 41 | Help touchpoint thiếu | B-Help (deeplink docs.diva-group + runbook) |
| 42 | Lifecycle ≥4 trạng thái không có EXT-3 | EXT-3 đã load — B6.1 cover 12 trạng thái event + B6.2 cover 3 trạng thái outage |
| 43 | RBAC field-level không có EXT-4 | EXT-4 đã load — B5.4 (api_key/token/raw_payload masked qua Hasura permission) |
| 44 | Audit/compliance UI thiếu | EXT-9 reference — SCR-03 audit log cover (event_id, record_id, status, source_ip, correlation_id, retry_count) |
| 45 | Real-time update không nói | SCR-04 polling 60s; outage banner sticky; defer WebSocket MVP-2 |
| 46 | Notification template không thống nhất | B4 + DEC-024 3 template với placeholder rõ ràng |
| 47 | Wireframe data placeholder thay vì realistic | B0.6 + wireframes thực tế: "Nguyễn Thị Mai 0912345678", "FB Diva HCM Q1", "Diva HN Cầu Giấy" |
| 48 | Loose mode UI không rõ vs Strict mode | SCR-02 dropdown branch có option "Để trống (Loose mode)" + tooltip + confirm modal |
| 49 | Kill switch vs per-source toggle confusion | B6.3 connection_status 4 state + B5.1 + B9 rows 5, 11 phân biệt rõ |
| 50 | DLQ replay có thể tạo duplicate ticket | B6.1 status processed KHÔNG cho replay (idempotency); chỉ dead_letter/auth_failed/parse_error |
B-Đối soát DEC (DEC Cross-Reference)
| DEC | Status trong UI Spec | Section ref |
|---|---|---|
| DEC-001 (Inbound 1 chiều) | N/A — backend only, UI assume inbound | (no UI) |
| DEC-002 (Split webhook + crm-api) | N/A — backend architecture | (no UI) |
| DEC-003 (Fail-open 200) | Cover qua B6.1 lifecycle 12 status (auth_failed, ip_blocked, parse_error, ... đều terminal trả 200) | B6.1 |
| DEC-004 (Idempotency 3-tuple) | Cover qua B6.1 skipped_duplicate | B6.1 |
| DEC-005 (Phone E.164) | N/A — backend only | (no UI) |
| DEC-006 (contact_book ALTER) | N/A — backend only | (no UI) |
| DEC-007 (4 bảng pancake_* dùng disabled) | Cover gián tiếp qua SCR-02 inline-toggle (is_active) thay vì delete | SCR-02 |
| DEC-008 (VIP tag NAME case-insensitive) | Cover B0.4.A row vip_tag_names + B9 row 6 + SCR-01 textarea helper text | SCR-01 + B9 |
| DEC-009 (Test event is_test) | Cover SCR-03 filter checkbox + B9 row 28 | SCR-03 + B9 |
| DEC-010 (Notification in-app push admin) | Cover B4 | B4 |
| DEC-011 (Smart update Q2.b) | Cover qua B7.8 mapping thuật ngữ "Chỉ tạo ticket khi thay đổi quan trọng" | B7.8 |
| DEC-012 (Reuse GetTicketUpdates) | N/A — backend only | (no UI) |
| DEC-013 (Auto-assign 1-tier + Loose mode) | Cover SCR-02 Loose mode dropdown + B5.4 modal | SCR-02 |
| DEC-014 (Opt-out marketing) | Cover B6.1 status skipped_opt_out + B7.8 | B6.1 + B7.8 |
| DEC-015 (4-layer outage recovery) | Cover SCR-03 DLQ replay + SCR-04 outage banner + B6.2 lifecycle | SCR-03 + SCR-04 + B6.2 |
| DEC-016 (Settings 4 tabs Admin only) | Cover B5.1 + SCR-05 + SCR-01..04 | B5 + B2 |
| DEC-017 (Ticket source slot 9) | Cover SCR-06 + B7.6 + B-i18n | SCR-06 |
| DEC-018 (REST /sources only) | Cover SCR-01 button "Kiểm tra kết nối" + SCR-02 button "Đồng bộ" | SCR-01 + SCR-02 |
| DEC-019 (Circuit breaker gobreaker) | N/A — backend only | (no UI) |
| DEC-020 (Advisory lock phone) | N/A — backend only | (no UI) |
| DEC-021 (3-mức feature flag) | Cover SCR-01 kill switch (mức 1) + SCR-02 is_active (mức 3) + B6.3 connection.status (mức 2) | SCR-01 + SCR-02 + B6.3 |
| DEC-022 (Pilot 4 tuần) | Cover B-Versioning + SCR-02 toggle pilot rollout flow | B-Versioning |
| DEC-023 (5 KPI metric) | Cover SCR-04 4 metric cards + B9 calculated rows 36, 39, 46 + 2 chart | SCR-04 + B9 |
| DEC-024 (3 notification template) | Cover B4 | B4 |
| DEC-025 (25h overlap reconciliation) | Cover B6.1 event_type reconciled + SCR-04 outage history rescue label | B6.1 + SCR-04 |
DEC backend-only: DEC-001/002/005/006/012/019/020 không có UI surface — note ở ui-spec OK, dev-spec là canonical.
Hết UI Spec.
Sẵn sàng cho
dev-spec.md(C1-C12) +qa-test-plan.md(D1-D5) +handoff.md+go-live-checklist.md(E1-E4). Cross-spec consistency check (_CONSISTENCY_MATRIX.md) sẽ chạy sau khi đủ 4 spec.
B-Resolutions) Pass 1 (Phase 5.2 Multi-Perspective Review fixes)
Date: 15/05/2026 | Trigger: FE Lead + UI/UX Designer review.
B-R.1) F18 delta CORRECTED — 4 file (không phải 3)
Verified diva-admin/src/modules/user/components/customer/CustomerTicketManager.tsx:111-158 có bản sao sourceDescriptions (không import từ Tickets.tsx).
F18 thật:
| # | File | Delta |
|---|---|---|
| 1 | crm/types.ts:146-164 | ADD TICKET_SOURCE_PANCAKE + append TicketSources |
| 2 | crm/i18n/vi.ts:129-138 | ADD nested key crm.label.ticket.ticket_source.ticket_source_pancake: "Pancake CRM" |
| 3 | crm/pages/Tickets.tsx:128-158 | ADD entry sourceDescriptions.ticket_source_pancake |
| 4 | user/components/customer/CustomerTicketManager.tsx:111-158 (NEW) | ADD entry sourceDescriptions.ticket_source_pancake — duplicate source-of-truth tạm thời, M2 refactor extract single-source |
B-R.2) Component library audit (FE P0-FE-3)
| Spec cũ | Real | Action |
|---|---|---|
XPassword | XInputPassword | Rename trong B2.SCR-01 + B-i18n |
XTextarea | (no) QInput type="textarea" | Use Quasar |
XBadge | (no) QBadge | Use Quasar |
XDrawer | (no) QDrawer / QDialog seamless | Verify pattern Settings |
XSearchInput | (no) XInput + clear icon | Use existing |
XDateRangePicker | XInputDateRange | Rename |
XJsonViewer | BUILD NEW (~1d FE) | New components/core/display/XJsonViewer.tsx |
XMetricCard | BUILD NEW (~0.5d FE) | New components/core/display/XMetricCard.tsx |
XChart | REUSE modules/dashboard/components/{BarChart,LineChart} | Update ref |
B-R.3) DEC-026 — Slot 9 trong báo cáo (PO/BA + FE P1-FE-1)
Verified TelesaleReportFilter.tsx:140 + CustomerServiceReportFilter.tsx:195 auto-render slot 9 qua TicketSources.map. RSK-005 PRD A7 giảm severity từ TB → Thấp (auto-render confirm).
B-R.4) Banner outage auto-fade (UX P0-UX-1)
Spec cũ: Banner đỏ persistent đến khi outage_recovered → banner xanh sustain 5 phút.
Revised B6.2:
outage_started→ banner đỏ persistent (đang đau)outage_recovered→ banner xanh auto-fade 30s (báo OK rồi)healthysau 5 phút sustained → không banner
B-R.5) Replay copy clarity (UX P0-UX-2)
Giữ "Phát lại" canonical (B7.8). Tooltip B9-18 đã có rõ ràng. KHÔNG đổi sang "Xử lý lại" (avoid breaking change downstream).
B-R.6) Banner emoji removal (UX I12)
B-Voice rule: KHÔNG emoji trong copy hành chính. Banner outage bỏ emoji 🚨, dùng intent token + icon SVG.
B-R.7) Status badge icon disambiguation (UX P0-UX-4)
5 status skipped_* icon disambiguation (G9.5 color blind support):
skipped_duplicate→ 🔁skipped_source_disabled→ 🚧skipped_kill_switch→ 🛑skipped_opt_out→ 🚫skipped_parse_error→ ⚠
Hoặc giữ icon đơn, dùng text VN rõ trong B6.1 (đã có).
B-R.8) i18n vi.ts canonical path fix (FE I1)
Spec dev-spec C6 line đơn giản hóa ticket_source_pancake: 'Pancake CRM' thực ra phải nest đầy đủ:
ts
// crm/i18n/vi.ts
{
crm: {
label: {
ticket: {
ticket_source: {
ticket_source_1: "Nguồn 1",
// ...
ticket_source_pancake: "Pancake CRM" // CORRECT path
}
}
}
}
}B-R.9) URL tab deeplink (FE I3)
Bỏ sessionStorage pancake-crm-last-tab. Dùng URL route ?tab=<name> only — đơn giản, không stale, multi-tab browser support.
B-R.10) Pinia vs composition pattern (FE P1-FE-2)
Pancake settings dùng composition pattern theo compositions/useSettings.ts (cùng module settings) — consistent. KHÔNG mix Pinia store.
B-R.11) Permission check refactor (DEC-027)
UI Spec B5.1 replay_dlq action → REPLACE bằng pancake_crm_integration.update:
tsx
v-if="globalStore.hasPermission('pancake_crm_integration', 'update')"B-R.12) Polling guard tab visibility (FE P1-FE-3)
SCR-04 polling 60s phải check document.visibilityState === 'visible' trước mỗi refetch. Tránh waste khi 10-20 admin tab mở cùng.
B-R.13) Concurrent inline-edit conflict strategy (FE P1-FE-4)
pancake_source_routing UPDATE với where: { updated_at: { _eq: $oldUpdatedAt } } — nếu 0 affected rows → conflict modal "Bản ghi đã thay đổi. Refetch?".
KHÔNG add version column riêng.
B-R.14) Telesale survey n≥3 (W1 exit — QA D7)
W1 source phải có ≥3 telesale active để telesale survey statistically meaningful. PO/Marketing chọn source W1 theo criteria này.
Hết Pass 1 Resolutions UI Spec. 13 fix consolidated từ FE/UX review.