Appearance
Đặc tả giao diện (UI Spec) — Chấm công đa đơn vị trên shared tenant
v3.1.5 — 21/04/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
Bổ sung product execution contract: mục tiêu màn, CTA hierarchy, interaction behavior, action feedback cho các màn xương sống | SCR-00, SCR-00C, SCR-01, SCR-02, SCR-03, SCR-05, SCR-06 | FE Admin, FE Mobile, UI/UX, QA |
Bổ sung Copy Text Dictionary, Responsive + Accessibility Rules, Tooltip Dictionary để giảm phần FE/UI phải tự quyết | B6, B7, B9 | FE, UI/UX, QA |
Chuẩn hóa SCR-00 thành setup center: checklist deeplink được cả màn ngoài SCR-00, bổ sung Setup Navigator và wizard-style flow | B2) SCR-00 | FE Admin, QA, Ops |
Tách Tab Quy định theo tư duy SaaS self-serve: Cơ bản / Nâng cao, preset, copy config, draft/review/publish | Tab Quy định | FE Admin, UX, QA |
Bổ sung bulk actions cho tenant lớn ở Nhân viên áp dụng: gán theo scope, đồng bộ theo cơ cấu, review delta | Tab Nhân viên áp dụng | FE Admin, Ops |
Bổ sung preview runtime trong form ca để nhìn trước CTA mobile, day detail và export contract | SCR-00C | FE Admin, FE Mobile, QA |
| Bổ sung cơ chế an toàn khi đổi config đang live: draft, impact preview, effective date và publish từ ngày mai | Tab Quy định | FE Admin, BE, Ops |
Chuyển cách tính công xuống Ca làm việc, bỏ config mode ở Tab Quy định, và thêm screen contract mới cho Work Shift | SCR-00C, Tab Quy định | FE Admin, BE, QA |
Chốt GPS UX theo 2 lớp: tọa độ chi nhánh dùng branch master data hiện có, bán kính/rule GPS nằm ở SCR-00 > Quy định | B2) SCR-00 | FE Admin, UI/UX, QA |
Chốt SCR-00 là màn mới trong settings, có route/menu rõ ràng và Readiness Checklist | B2) SCR-00 | FE Admin, QA |
Chuẩn hóa Chi nhánh áp dụng / Phòng ban áp dụng / Nhân viên áp dụng / Triển khai, thêm STT và nhãn tiếng Việt cho field code | B2) SCR-00 | FE Admin, UI/UX |
Bổ sung flow bulk assign/reassign/unassign, import/paste mã NV và gating Bật áp dụng | B2) SCR-00 | FE Admin, QA |
Tham chiếu (Ref): PRD v3.1.4 | Ngày (Date): 21/04/2026
Changelog
| Version | Date | Author | Thay đổi |
|---|---|---|---|
| 3.1.5 | 21/04/2026 | PO/BA | Nâng ui-spec lên mức product execution-ready: thêm objective/CTA hierarchy, interaction behavior, action feedback, responsive/a11y, copy text và tooltip contracts |
| 3.1.4 | 21/04/2026 | PO/BA | Điều chỉnh ui-spec theo hướng SaaS-ready: setup center, config basic/advanced, bulk scope actions, preview runtime và publish-safe flow |
| 3.1.3 | 15/04/2026 | PO/BA | Đổi UI Công chuẩn sang nhóm áp dụng công chuẩn, hiển thị phòng ban áp dụng, bỏ hoàn toàn cách hiểu cũ theo branch_label_id |
| 3.0 | 07/04/2026 | PO/BA | Initial UI spec cho settings/timekeeping/mobile/export multi-unit |
| 3.0.1 | 07/04/2026 | PO/BA | Đồng bộ contract nghỉ giữa ca fixed/flex, annual leave Day-1 và remote nhóm nhỏ |
| 3.1 | 13/04/2026 | PO/BA | Chuẩn hóa UX vận hành: route/menu SCR-00, readiness checklist, tab Triển khai, vocabulary tiếng Việt, STT/cột hiển thị chuẩn, bulk assignment và soft-disable mapping |
| 3.1.1 | 13/04/2026 | PO/BA | Làm rõ GPS UX: không tạo màn GPS master riêng; dùng branch master data cho tọa độ chi nhánh và đặt gps_radius_meters làm override per-unit trong SCR-00, có deeplink Xem/Sửa tọa độ chi nhánh gốc |
| 3.1.2 | 14/04/2026 | PO/BA | Chuyển cách tính công từ Đơn vị chấm công xuống Ca làm việc, đổi Số ngày công thành Công tối đa của ca, thêm Giờ chuẩn của ca và gps_required ở shift để quyết định Bắt buộc / Không bắt buộc GPS theo từng ca |
B1) Bản đồ màn hình (Screen Map)
| SCR | Tên | Reuse shell hiện có | Mô tả delta |
|---|---|---|---|
| SCR-00 | Cấu hình đơn vị chấm công | New page in settings module | Cấu hình timekeeping_unit, map branch/department/user trong phạm vi chấm công |
| SCR-00C | Work Shift / Shift Group Settings | ShiftSetting.tsx | Thêm filter timekeeping_unit, reuse field giờ nghỉ trưa hiện có, chọn mode nghỉ fixed/flex, cấu hình 2 mốc/4 mốc |
| SCR-00D | Timekeeping Approver Settings | ApproverPage.tsx | Chỉ mở delta cho request type thuộc timekeeping |
| SCR-01 | Working Schedule | WorkingSchedule.tsx | Thêm filter timekeeping_unit, ca 2 mốc/4 mốc |
| SCR-02 | Working Time Sheet | WorkingTimeSheet.tsx | Thêm filter timekeeping_unit, multi-record day |
| SCR-03 | Working Time Sheet Day Detail | Popup hiện có | Chi tiết 2 mốc/4 mốc và request overlay |
| SCR-04 | Employee Timekeeping History | EmployeeProfileTimekeepingHistory.tsx | Hiển thị summary theo ngày đúng với timekeeping runtime mới |
| SCR-05 | Staff Mobile Attendance | attendance_screen.dart | PN 2 mốc + 4 mốc, Daisy 2 mốc + 4 mốc theo từng ca |
| SCR-06 | Timekeeping Export | Export modal hiện có | Chỉ export vận hành chấm công, không có payroll bridge |
| SCR-07 | Request List | RequestApproval / danh sách đơn hiện có | Scope lại request theo timekeeping_unit, giữ shell hiện tại |
| SCR-08 | Employee Profile Badge | EmployeeProfile hiện có | Hiện badge readonly Đơn vị chấm công trên hồ sơ NV |
UI Principles
- Không tạo module quản trị org mới ngoài scope chấm công.
- Không sửa UI dashboard/report legacy.
- Reuse shell hiện có của
settings,timekeeping,user,mobile attendance. - Mọi filter mới chỉ là
timekeeping_unit, không thay company scope toàn hệ thống. - Day-1 chỉ mở
on-site attendance; feature này không tạo CTA/request entryremotemới cho rollout unit. Flowremotelegacy vẫn tồn tại ngoài phạm vi UI change nếu tenant đang dùng. - Mọi bảng list/table trong admin mặc định có cột
STTở vị trí đầu tiên. - Mọi field contract có
field codetiếng Anh phải đi kèmTên hiển thị (VI)đúng với nhãn/cột dùng trên UI.
B1.1) Hành trình vận hành thật (End-to-End User Journey)
Journey A — System Admin chuẩn bị rollout
| Bước | Màn hình | Người thao tác | Việc làm chính | Output cho màn sau |
|---|---|---|---|---|
| 1 | SCR-00 > Đơn vị | System Admin | Tạo timekeeping_unit, đặt tên, phase, cờ admin/mobile | Có unit để cấu hình |
| 2 | SCR-00 > Quy định | System Admin | Khai báo quota đơn từ, GPS radius, OT threshold, penalty, công chuẩn | Có rule nền cho unit |
| 3 | SCR-00 > Chi nhánh áp dụng | System Admin | Map branch thật vào unit | Mở khóa scope branch |
| 4 | SCR-00 > Phòng ban áp dụng | System Admin | Map department theo branch đã chọn | Mở khóa scope department |
| 5 | SCR-00 > Nhân viên áp dụng | System Admin / HR | Gán user vào unit với primary_branch_id, primary_department_id, hiệu lực | Có headcount thật để xếp lịch |
| 6 | SCR-00C | HR / Admin timekeeping | Tạo ca, chọn 2 mốc hay 4 mốc, GPS bắt buộc hay không, mode tính công | Có shift template đúng nghiệp vụ |
| 7 | SCR-00D | HR / Admin timekeeping | Cấu hình approver theo unit/branch/department | Có luồng duyệt đúng scope |
| 8 | SCR-01 | HR / Admin timekeeping | Import hoặc tạo lịch làm việc theo tuần/ngày | Có time_slot_user cho runtime |
| 9 | SCR-00 > Triển khai | System Admin / QA / Ops | Kiểm readiness, lưu snapshot, bật rollout | Admin/mobile bắt đầu dùng thật |
Journey B — HR vận hành hàng ngày
| Bước | Màn hình | Việc làm chính | Kết quả mong đợi |
|---|---|---|---|
| 1 | SCR-02 | Chọn unit, tháng, branch/department để xem bảng công | Có bức tranh vận hành theo unit |
| 2 | SCR-03 | Mở chi tiết một ngày khi thấy Thiếu mốc, Đi trễ, Về sớm | Xem đủ 2 mốc/4 mốc + request overlay |
| 3 | SCR-07 | Tra đơn quên chấm, trễ/sớm, OT, đổi ca | Biết đơn nào đã/đang chờ duyệt |
| 4 | SCR-00D | Nếu thấy duyệt sai scope, quay lại chỉnh approver | Sửa được luồng duyệt gốc |
| 5 | SCR-06 | Xuất bảng công tháng / chi tiết ngày / trễ-sớm / OT / quên chấm | Chốt vận hành, không cần Excel ngoài |
Journey C — Nhân viên dùng mobile
| Bước | Màn hình | Việc làm chính | Hệ thống phản hồi |
|---|---|---|---|
| 1 | SCR-05 | Mở app chấm công hôm nay | CTA quyết định theo ca được xếp trong SCR-01 |
| 2 | SCR-05 | Bấm Vào ca / Ra nghỉ / Vào lại / Ra về | Ghi event theo state machine 2 mốc hoặc 4 mốc |
| 3 | Request Center legacy | Tạo đơn quên chấm / trễ-sớm / OT khi có sự cố | Đơn đi vào approver cùng unit |
| 4 | SCR-05 history | Xem lịch sử tháng, running total, request overlay | Tự đối soát tình trạng chấm công |
B1.2) Sơ đồ phụ thuộc màn hình (Screen Dependency Map)
Ý nghĩa vận hành:
SCR-00là nơi khóa scope dữ liệu, không phải màn cấu hình đứng một mình.SCR-00Cquyết định contract runtime của mobile và bảng công.SCR-01là cầu nối giữa cấu hình và ngày công thực tế.SCR-02là cockpit vận hành;SCR-03chỉ là màn drill-down củaSCR-02.SCR-06không phải nguồn sự thật; nó chỉ tổng hợp từ dữ liệu đã được setup và punch ở các màn trước.
B1.3) Ma trận liên kết màn hình (Screen Linking Matrix)
| Màn hình | Vào từ đâu | Hành động chính | Đi tiếp đâu | Nếu thiếu điều kiện |
|---|---|---|---|---|
SCR-00 | Menu Internal Settings | Tạo unit, map scope, readiness | SCR-00C, SCR-00D, SCR-01, tab Triển khai | Hiện checklist ❌, deeplink sang tab còn thiếu |
SCR-00C | Deeplink từ SCR-00 > Quy định, menu shift | Tạo shift, chốt 2 mốc/4 mốc, mode tính công | SCR-01, SCR-02, SCR-05 | Nếu chưa có unit active thì không cho tạo shift |
SCR-00D | Deeplink từ SCR-00, menu approver | Chốt approver theo unit | SCR-07, SCR-02 | Nếu chưa map user/department thì không cho save scope |
SCR-01 | Menu timekeeping, deeplink từ readiness | Xếp lịch/import lịch | SCR-05, SCR-02 | Nếu chưa có shift hoặc user assignment thì empty state hướng dẫn |
SCR-05 | App mobile | Punch theo state machine, xem history | SCR-07 legacy | Nếu chưa có lịch hoặc rollout chưa bật thì giữ behavior legacy / báo không có ca |
SCR-02 | Menu timekeeping | Quan sát vận hành theo tháng | SCR-03, SCR-06, SCR-07 | Nếu thiếu unit/filter scope thì empty state |
SCR-03 | Click từ SCR-02 | Xem chi tiết ngày, request overlay | Quay lại SCR-02, mở SCR-07 | Không mở độc lập từ menu |
SCR-06 | Nút export từ SCR-02 | Chốt báo cáo vận hành | File export | Nếu dữ liệu chưa đủ scope thì báo theo filter hiện tại |
SCR-07 | Menu request, link từ popup/detail | Theo dõi và xử lý đơn | SCR-02, SCR-03 | Nếu approver ngoài unit thì không thấy đơn |
B1.4) Nguyên tắc liên kết dữ liệu (Single-Flow Rules)
| Rule ID | Rule |
|---|---|
| FLOW-001 | SCR-00 là nguồn sự thật cho unit scope; các màn sau không tự map thêm branch/department ngoài SCR-00 |
| FLOW-002 | SCR-00C là nguồn sự thật cho contract 2 mốc/4 mốc, gps_required, workday_calculation_mode; mobile và working sheet chỉ render theo cấu hình đã khóa ở đây |
| FLOW-003 | SCR-01 là nguồn sự thật cho "hôm nay user thuộc ca nào"; mobile không tự suy diễn ca từ unit |
| FLOW-004 | SCR-05 chỉ ghi nhận event chấm công; không tự chốt payroll hay thay rule công |
| FLOW-005 | SCR-02 và SCR-03 là lớp quan sát/đối soát; không phải nơi sửa cấu hình gốc |
| FLOW-006 | SCR-06 luôn xuất theo filter thật mà SCR-02 đang đứng trên đó, để HR không bị nhảy context khi chốt công |
| FLOW-007 | Nếu checklist/readiness phát hiện thiếu dữ liệu, hệ thống phải ưu tiên deeplink về đúng màn gốc cần sửa thay vì mở modal hướng dẫn chung chung |
B2) SCR-00: Cấu hình đơn vị chấm công
Mục tiêu
Đây là màn mới trong settings module, dùng để quản trị timekeeping_unit cho feature này. Nó không thay vai trò của module quản trị tổ chức toàn hệ thống.
Vai trò trong flow: SCR-00 là "trạm điều phối rollout", nơi gom toàn bộ điều kiện để các màn còn lại dùng được thật. HR/System Admin không nên phải nhớ thứ tự setup bằng đầu; màn này phải dẫn đường bằng checklist, deeplink và trạng thái readiness.
Input / Output của màn
| Input | Output |
|---|---|
| Master data branch/department/user hiện có | timekeeping_unit, mapping scope, assignment user, rollout snapshot |
| Quyết định nghiệp vụ đã chốt ở PRD/dev-spec | Điều kiện để SCR-00C, SCR-00D, SCR-01 và mobile dùng đúng scope |
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Tạo hoặc chỉnh 1 timekeeping_unit, biết còn thiếu bước nào, và điều phối rollout từ cùng một setup center |
| Primary CTA | Bật áp dụng ở tab Triển khai; chỉ enable khi checklist không còn ❌ |
| Secondary CTA | Thêm đơn vị, Mở bước còn thiếu, Lưu nháp, Xem ảnh hưởng, Lưu triển khai |
| Khối thông tin phải nổi bật | Readiness Checklist, Setup Navigator, tên unit hiện tại và phạm vi ảnh hưởng của draft |
| Điều không được gây hiểu nhầm | Badge checklist không phải text tĩnh; mọi badge phải click được hoặc nói rõ vì sao chưa click được |
As-Is Audit
- Hiện tại admin đã có các màn settings liên quan nhưng đang tách rời:
ShiftSetting.tsxquản lýWork Shift/Shift GroupApproverPage.tsxquản lý approver theo từng request typeAnnualLeave.tsxquản lýapp_setting.request_leaveở cấp global
- Chưa có màn riêng để CRUD
timekeeping_unit, map branch/department/user, hoặc cấu hình timekeeping per unit trên cùng một screen. - Vì vậy SCR-00 là new page, nhưng vẫn phải reuse shell/navigational pattern của
settingsmodule để tránh tạo trải nghiệm lạ.
Menu & Route Contract
| Item | Giá trị đề xuất |
|---|---|
| Module | settings |
| Menu group | Internal Settings |
| Menu label | Đơn vị chấm công |
| Route name | ROUTE_SETTING_TIMEKEEPING_UNIT |
| Path | /s/internal-settings/timekeeping-unit |
| Container | InternalSetting.tsx |
Rule: Tab Nhân viên áp dụng để gán user vào timekeeping_unit nằm bên trong SCR-00, không nằm trong màn UserCreate / EmployeeProfileCreate.
Layout
text
+------------------------------------------------------------------------------------------------------------------+
| CẤU HÌNH ĐƠN VỊ CHẤM CÔNG |
| [Tab Đơn vị] [Tab Quy định] [Tab Chi nhánh áp dụng] [Tab Phòng ban áp dụng] [Tab Nhân viên áp dụng] [Tab Triển khai] |
|------------------------------------------------------------------------------------------------------------------|
| [Tìm theo mã/tên...] [Trạng thái ▼] [Thêm đơn vị] |
| STT | Mã | Tên đơn vị | Trạng thái | Số nhân viên | Số chi nhánh áp dụng | Số phòng ban áp dụng | Giai đoạn | Thao tác |
| 1 | PN | Phương Nam | Đang áp dụng | 247 | 4 | 5 | R1A | [Chi tiết] |
| 2 | DS | Daisy | Đang áp dụng | 150 | 6 | 6 | R1B | [Chi tiết] |
+------------------------------------------------------------------------------------------------------------------+Readiness Checklist
text
+------------------------------------------------------------------------------------------------------------------+
| ĐIỀU KIỆN BẬT ÁP DỤNG — DAISY |
| [Chi nhánh đã map: 6/6 ✅] [Phòng ban đã map: 6/6 ✅] [Nhân viên đã gán: 148/150 ⚠️] [Luồng duyệt đã đủ: 5/6 ⚠️] |
| [Ca làm việc đã cấu hình: 17/17 ✅] [Quy định nền: Đủ ✅] [Xuất báo cáo thử: ⚠️] [Mở bước còn thiếu] |
+------------------------------------------------------------------------------------------------------------------+| Trạng thái (Status) | Ý nghĩa |
|---|---|
✅ | Đủ điều kiện để bật áp dụng |
⚠️ | Đã cấu hình một phần, còn việc cần xử lý |
❌ | Chưa đủ điều kiện hoặc chưa có bằng chứng |
Điều hướng nhanh: click từng badge trong checklist để nhảy thẳng sang đúng nơi cần xử lý. Nếu hạng mục nằm ngoài SCR-00 như Ca làm việc hoặc Người duyệt, hệ thống deeplink sang SCR-00C / SCR-00D, đồng thời giữ breadcrumb để quay lại SCR-00 > Triển khai.
Quy tắc đặt tên badge: ưu tiên động từ hoặc trạng thái dễ hiểu cho vận hành như đã map, đã gán, đã cấu hình, đã đủ, đã chạy thử; tránh nhãn quá nén như GPS/Quy định hoặc Kiểm tra xuất báo cáo vì khó hiểu trạng thái thực sự đang được đo là gì.
Quy ước trạng thái vận hành: Chờ hiệu lực chỉ dùng cho entity có effective_from/effective_to thực sự ở Day-1, tức là timekeeping_unit_user trong tab Nhân viên áp dụng. Các màn Đơn vị, Chi nhánh áp dụng, Phòng ban áp dụng chỉ dùng Đang áp dụng / Ngưng áp dụng.
Setup Navigator (wizard-style)
| Bước setup | Badge readiness tương ứng | Điểm đến khi click | Điều hệ thống kiểm tra |
|---|---|---|---|
| 1. Chọn scope chi nhánh | Chi nhánh đã map | SCR-00 > Chi nhánh áp dụng | Có đủ branch active cần rollout |
| 2. Chọn scope phòng ban | Phòng ban đã map | SCR-00 > Phòng ban áp dụng | Department đã map theo branch đúng |
| 3. Gán nhân sự | Nhân viên đã gán | SCR-00 > Nhân viên áp dụng | Headcount active/future đủ theo target |
| 4. Cấu hình rule nền | Quy định nền và các badge rule | SCR-00 > Quy định | Rule cơ bản đã publish cho unit |
| 5. Cấu hình ca làm việc | Ca làm việc đã cấu hình | SCR-00C | Có đủ shift template/shift group cho scope rollout |
| 6. Cấu hình người duyệt | Luồng duyệt đã đủ | SCR-00D | Request type Day-1 có approver hợp lệ cùng unit |
| 7. Xếp lịch và chạy thử | Xuất báo cáo thử / Đã chạy thử | SCR-01 rồi SCR-02 | Có lịch, có dữ liệu kiểm tra và export pass |
| 8. Bật rollout | Không còn badge ❌ | SCR-00 > Triển khai | Cho phép lưu snapshot và bật rollout |
Nguyên tắc SaaS: SCR-00 là setup hub, nhưng không ép tất cả cấu hình nằm trong một màn. Checklist được phép điều hướng sang màn khác miễn là luôn có đường quay về SCR-00 > Triển khai để hoàn tất rollout.
Interaction Behavior Contract
| Tình huống | Behavior mong muốn |
|---|---|
| Save mode theo tab | Đơn vị, Chi nhánh, Phòng ban, Nhân viên áp dụng, Triển khai dùng save tay theo tab; Quy định dùng flow Lưu nháp -> Xem ảnh hưởng -> Áp dụng từ ngày mai |
| Dirty state | Tab nào có thay đổi chưa lưu phải hiện chấm vàng Có thay đổi chưa lưu ở nhãn tab |
| Unsaved changes | Khi rời tab hoặc deeplink sang màn khác mà còn draft chưa lưu, hiện confirm modal Lưu nháp / Bỏ thay đổi / Ở lại |
| Search / filter apply | Ô tìm kiếm debounce 300ms; dropdown filter apply ngay; đổi Đơn vị chấm công reset toàn bộ filter con đang phụ thuộc |
| Cascade reset | Gỡ branch phải chặn silent remove nếu còn department/user/schedule/approver phụ thuộc; bắt buộc mở dialog dependency |
| Bulk action | Gán toàn bộ theo scope đang lọc và Đồng bộ theo cơ cấu hiện tại phải có preview delta trước khi lưu |
| Long-running job | Import mã NV hoặc sync lớn chạy background, hiển thị progress + số bản ghi thành công/thất bại |
Action Feedback Contract
| Action / Situation | Feedback UI | Hành động tiếp theo |
|---|---|---|
| Lưu tab thành công | Toast xanh + badge readiness refresh | Giữ nguyên context unit hiện tại |
| Validation lỗi | Inline error tại field + banner tổng hợp ở đầu popup/tab | Focus về field lỗi đầu tiên |
| Mở bước còn thiếu | Deeplink + breadcrumb Quay lại Triển khai | User quay về đúng unit và đúng tab trước đó |
| Bulk sync / import xong | Banner kết quả Tạo mới / Chuyển đơn vị / Thất bại + nút export danh sách affected users | Cho phép retry phần lỗi |
| Bật áp dụng | Confirm modal cuối cùng hiển thị checklist snapshot | Sau khi xác nhận, chuyển unit sang trạng thái Đang áp dụng |
| Lỗi publish / save | Toast đỏ + giữ nguyên draft | Có nút Thử lại hoặc Quay về draft |
Tab contract
| Tab | Chức năng | Ghi chú |
|---|---|---|
| Đơn vị | Tạo/sửa/ngưng áp dụng đơn vị chấm công | Thông tin cơ bản |
| Quy định | Cấu hình quy định chấm công per đơn vị | Mới — settings module |
| Chi nhánh áp dụng | Thêm chi nhánh hiện có vào timekeeping_unit | Không đổi branch master data |
| Phòng ban áp dụng | Thêm phòng ban hiện có vào timekeeping_unit | Không đổi department master data |
| Nhân viên áp dụng | Thêm nhân viên vào timekeeping_unit | Chỉ phục vụ scope chấm công |
| Triển khai | Bật/tắt admin/mobile theo unit | PN + Daisy cùng 1B |
Linked setup surfaces
| Hạng mục rollout | Nằm ở đâu | Vì sao không nhét vào SCR-00 |
|---|---|---|
| Ca làm việc | SCR-00C | Reuse màn shift hiện có, tránh duplicate form ca |
| Người duyệt | SCR-00D | Reuse shell approver hiện có, giữ consistency với flow request |
| Lịch làm việc | SCR-01 | Đây là nghiệp vụ vận hành riêng sau khi scope và shift đã sẵn sàng |
SCR-00 phải hiển thị các hạng mục này như các bước thuộc cùng một setup flow, không coi chúng là phần tách rời về mặt trải nghiệm.
Tab "Đơn vị" — Field contract
| Trường (Field code) | Tên hiển thị (VI) | Kiểu (Type) | Bắt buộc (Required) | Quy tắc (Rule) |
|---|---|---|---|---|
code | Mã đơn vị | QInput | Yes | Unique trong bảng timekeeping_unit |
name | Tên đơn vị | QInput | Yes | Tên hiển thị |
rollout_phase | Giai đoạn triển khai | QSelect | Yes | R1A / R1B / R1C / OFF |
allow_admin_timekeeping | Bật màn admin chấm công | QToggle | Yes | Bật màn admin trong scope này |
allow_mobile_self_service | Bật chấm công trên mobile | QToggle | Yes | Bật mobile attendance trong scope này |
Tab "Quy định" — Cấu hình quy định chấm công (Settings Module)
Mỗi đơn vị có bộ quy định riêng. System Admin cấu hình trên UI, backend đọc config từ DB — không hardcode trong code.
Lưu ý về as-is: phần rule phép năm global (
min_working_day,reset_date,leave_expiry_date) vẫn tiếp tục nằm ởAnnualLeave.tsx. Tab này chỉ thêm config thuộc scope timekeeping-unit như quota, OT threshold và GPS radius mặc định; không thêm toggle riêng cho báo cáo phép năm.GPS cũng tách thành 2 lớp:
tọa độ chi nhánhtiếp tục lấy từ branch master data hiện cóbán kính GPSlà rule override theotimekeeping_unittrong tab này
Cách tiếp cận cho SaaS self-serve
Tab Quy định không được ép tenant mới phải hiểu toàn bộ domain ngay ở lần cấu hình đầu tiên. Vì vậy màn này chia thành 2 lớp:
| Chế độ | Dùng khi nào | Hiện gì |
|---|---|---|
Cơ bản | Onboarding tenant mới, rollout nhanh theo preset | Quota đơn từ, OT threshold, GPS radius, preset config, copy config |
Nâng cao | Tenant có ngoại lệ nghiệp vụ hoặc đã qua pilot | Công chuẩn theo scope, penalty engine chi tiết, OT rate chi tiết, audit/publish controls |
Nguyên tắc: tenant mới bắt đầu từ Preset hoặc Sao chép cấu hình, chỉ vào Nâng cao khi thật sự cần lệch so với mẫu chuẩn.
Layout
text
+------------------------------------------------------------------------------------------------------------------+
| CẤU HÌNH ĐƠN VỊ > Phương Nam > QUY ĐỊNH CHẤM CÔNG |
|------------------------------------------------------------------------------------------------------------------|
| [Mức cấu hình: Cơ bản ▼] [Preset cấu hình ▼] [Sao chép từ đơn vị khác] [Xem lịch sử publish] |
|------------------------------------------------------------------------------------------------------------------|
| |
| ┌─ THIẾT LẬP CƠ BẢN ─────────────────────────────────────────────────────────────────────────┐ |
| │ │ |
| │ Max đơn đi trễ/về sớm [3 ] lần/tháng ⓘ Vượt vẫn cho tạo, HR quyết approve │ |
| │ Max phút per đơn trễ/sớm [Không giới hạn ▼] ⓘ Daisy = 60 phút. Null = không giới hạn│ |
| │ Max đơn quên chấm [3 ] lần/tháng │ |
| │ Tự động tạo lịch tuần [Bật ▼] ⓘ Tắt nếu đơn vị có ca xoay │ |
| │ OT tối thiểu [30 ] phút ⓘ Request OT < threshold sẽ bị bỏ qua │ |
| │ Bán kính GPS mặc định [200] mét ⓘ Trống = dùng global │ |
| │ │ |
| └───────────────────────────────────────────────────────────────────────────────────────────────┘ |
| |
| ┌─ THIẾT LẬP NÂNG CAO (collapse mặc định ở mode Cơ bản) ────────────────────────────────────┐ |
| │ │ |
| │ Công chuẩn theo scope [2 nhóm áp dụng] [Mở chi tiết] │ |
| │ Penalty engine [4 rules đang active] [Mở chi tiết] │ |
| │ Đơn giá OT [2 role rates] [Mở chi tiết] │ |
| │ │ |
| └───────────────────────────────────────────────────────────────────────────────────────────────┘ |
| |
| ┌─ TỌA ĐỘ CHI NHÁNH GỐC ─────────────────────────────────────────────────────────────────────┐ |
| │ │ |
| │ Tọa độ chi nhánh [Xem/Sửa tọa độ chi nhánh gốc] │ |
| │ │ |
| └───────────────────────────────────────────────────────────────────────────────────────────────┘ |
| |
| Draft hiện tại: chưa publish | Ảnh hưởng dự kiến: 4 CN, 6 PB, 148 NV, 17 ca |
| [Lưu nháp] [Xem ảnh hưởng] [Áp dụng từ ngày mai] |
+------------------------------------------------------------------------------------------------------------------+Chốt hiển thị: trong wireframe của Tab Quy định, block Tọa độ chi nhánh gốc phải luôn nằm cùng cấp với phần rule cấu hình; đồng thời Công chuẩn và Penalty engine không nên ném hết field ra mặt đầu tiên khi tenant đang ở mode Cơ bản.
Field contract — Tab Quy định
Readonly summary — Cách tính công ở cấp ca làm việc:
text
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ CÁCH TÍNH CÔNG ĐƯỢC CẤU HÌNH Ở CA LÀM VIỆC │
│ Day-1 không chọn "Theo ngày công / Theo giờ" ở cấp Đơn vị chấm công. │
│ Mỗi ca làm việc tự quyết định mode tính công để hỗ trợ mixed-mode trong cùng một đơn vị. │
│ [Mở Ca làm việc] │
└──────────────────────────────────────────────────────────────────────────────────────────────┘Control bar — SaaS setup helpers:
| Control | Mục đích | Hành vi |
|---|---|---|
config_mode | Chuyển Cơ bản / Nâng cao | Cơ bản là mặc định cho tenant mới |
preset_key | Chọn bộ cấu hình mẫu | Diva mặc định / Phương Nam chuẩn / Daisy chuẩn / Tự cấu hình |
copy_from_unit_id | Sao chép cấu hình từ đơn vị khác | Chỉ copy vào draft, chưa publish ngay |
draft_effective_date | Ngày bắt đầu áp dụng | Mặc định = ngày mai |
review_impact | Xem phạm vi bị ảnh hưởng | Hiện CN/PB/NV/ca bị tác động trước khi publish |
Nhóm Đơn từ:
| Trường (Field code) | Tên hiển thị (VI) | Kiểu (Type) | Bắt buộc (Required) | Mặc định (Default) | Quy tắc (Rule) | Giải thích (Tooltip) |
|---|---|---|---|---|---|---|
max_late_early_requests_per_month | Số đơn đi trễ/về sớm tối đa trong tháng | QInput number | Yes | 3 | Min 0, max 31 | Số đơn trễ/sớm tối đa mỗi tháng. Vượt vẫn cho tạo đơn, hiện cảnh báo |
late_early_max_duration_minutes | Số phút tối đa mỗi đơn đi trễ/về sớm | QInput number | No | null | Null = không giới hạn. Min 1, max 480 | Max phút per lần xin trễ/sớm. Daisy = 60. PN = null |
max_forget_clock_requests_per_month | Số đơn quên chấm tối đa trong tháng | QInput number | Yes | 3 | Min 0, max 31 | Số đơn quên chấm tối đa mỗi tháng |
Nhóm Ca làm việc:
| Trường (Field code) | Tên hiển thị (VI) | Kiểu (Type) | Bắt buộc (Required) | Mặc định (Default) | Quy tắc (Rule) | Giải thích (Tooltip) |
|---|---|---|---|---|---|---|
auto_schedule_disabled | Tắt tự động tạo lịch tuần | QToggle | Yes | false | — | Tắt = cron KHÔNG tạo lịch tuần tự động cho đơn vị này |
ot_min_threshold_minutes | OT tối thiểu để tính | QInput number | Yes | 0 | Min 0, max 480 | Request OT có duration < threshold sẽ bị bỏ qua khi export |
Nhóm Công chuẩn (v4 — theo nhóm áp dụng công chuẩn):
Công chuẩn được cấu hình theo nhóm áp dụng công chuẩn, không theo branch. Mỗi phòng ban trong unit sẽ được map vào đúng 1 nhóm áp dụng để resolve công chuẩn tháng.
Hiển thị dạng bảng — mỗi row = 1 nhóm áp dụng công chuẩn trong unit:
text
┌──────────────────────────────────────────────────────────────────┐
│ CÔNG CHUẨN │
│ │
│ Nhóm áp dụng │ Công thức │ Phòng ban áp dụng │
│ ─────────────────────┼───────────────────────────────┼───────────────────────────│
│ PN - Khối Dịch vụ │ [Ngày − CN ▼] │ Bác sĩ, Điều dưỡng... │
│ PN - Khối Văn phòng │ [Ngày − CN − 0.5×T7 ▼] │ TC-HC, Kế toán, MKT... │
│ │
│ ⓘ Mỗi phòng ban chỉ thuộc 1 nhóm áp dụng công chuẩn trong cùng │
│ đơn vị. HR có thể thêm nhóm mới khi có ngoại lệ nghiệp vụ. │
└──────────────────────────────────────────────────────────────────┘| Trường (Field code) | Tên hiển thị (VI) | Kiểu (Type) | Ý nghĩa |
|---|---|---|---|
standard_workday_scope_key | Nhóm áp dụng | QInput / readonly code | Key nghiệp vụ của nhóm áp dụng công chuẩn. VD: PN_OFFICE, DAISY_SERVICE |
scope_name | Tên nhóm áp dụng | QInput | Tên hiển thị cho HR. VD: PN - Khối Văn phòng, Daisy - VP Kế toán |
formula | Công thức công chuẩn | QSelect | Ngày − CN / Ngày − CN − 0.5×T7 / 26 ngày cố định / Tuỳ chỉnh |
fixed_value | Giá trị cố định | QInput number | Chỉ hiện khi formula = "Tuỳ chỉnh" |
department_ids | Phòng ban áp dụng | QSelect multiple | Chọn các phòng ban dùng chung công thức công chuẩn này |
Action: Day-1 cho phép:
Thêm nhóm áp dụngGán phòng banSửa công thức
Rule: Mỗi department_id chỉ được thuộc 1 standard_workday_scope_key active trong cùng unit.
Nhóm Vi phạm & Phạt (v3 — NEW, bảng con timekeeping_penalty_rule):
Mỗi đơn vị có bảng rule riêng. Admin CRUD rows trong bảng này.
| Trường (Field code) | Tên hiển thị (VI) | Kiểu (Type) | Ý nghĩa |
|---|---|---|---|
violation_type | Loại vi phạm | QSelect | late_early / forget_start / forget_end / forget_break |
penalty_mode | Cách tính phạt | QSelect | per_minute (đ/phút) / fixed_amount (đ/lần) / deduct_workday (trừ công) |
penalty_amount | Số tiền phạt | QInput number | Số tiền (VD: 10000, 30000, 50000) |
penalty_workday | Số công bị trừ | QInput number | Số công trừ (VD: 0.5) — chỉ hiện khi mode = deduct_workday |
exempt_count | Số lần miễn | QInput number | Số lần miễn/tháng (0 = không miễn) |
exempt_pool | Nhóm dùng chung số lần miễn | QSelect | individual (đếm riêng — PN) / shared (gộp chung — Daisy) |
Nhóm Tăng ca (v3 — thêm rate):
| Trường (Field code) | Tên hiển thị (VI) | Kiểu (Type) | Bắt buộc (Required) | Mặc định (Default) | Quy tắc (Rule) | Giải thích (Tooltip) |
|---|---|---|---|---|---|---|
ot_rate_default | Đơn giá OT mặc định | QInput number | Yes | 0 | Min 0 | Đơn giá OT mặc định (đ/giờ). PN = 50.000, Daisy = 35.000 |
ot_rate_doctor | Đơn giá OT bác sĩ | QInput number | Yes | 0 | Min 0 | Đơn giá OT Bác sĩ (đ/giờ). Cả PN + Daisy = 150.000 |
Nhóm GPS (v3 — NEW):
| Trường (Field code) | Tên hiển thị (VI) | Kiểu (Type) | Bắt buộc (Required) | Mặc định (Default) | Quy tắc (Rule) | Giải thích (Tooltip) |
|---|---|---|---|---|---|---|
gps_radius_meters | Bán kính GPS mặc định | QInput number | No | Trống = dùng global | Min 50, max 5000 khi nhập | Bán kính GPS mặc định của đơn vị (mét). Dùng làm radius mặc định cho các ca thuộc unit |
Readonly helper:
| Item | Nội dung | Action |
|---|---|---|
| Tọa độ chi nhánh | Tọa độ GPS của từng chi nhánh không nhập ở đây. Hệ thống dùng latitude / longitude trong branch master data để check khoảng cách | Xem/Sửa tọa độ chi nhánh gốc |
Day-1 chưa mở config cộng phép/reset/hết hạn theo từng đơn vị. Các rule này vẫn đọc từ
app_setting.request_leaveglobal và chỉ verify không ảnh hưởng PN/Daisy. Báo cáo phép năm PN được mở trực tiếp ở màn report hiện có, không cấu hình qua toggle trong tab này.
Seed data mặc định
| Đơn vị | Max trễ/sớm | Max phút | Max quên | Auto schedule | OT min | Phép năm |
|---|---|---|---|---|---|---|
| Diva | 3 | null | 3 | true | 0 | true |
| Phương Nam | 3 | null | 3 | true | 30 | true |
| Daisy | 3 | 60 | 3 | false | 0 | false |
Rules
| Rule ID | Rule |
|---|---|
| CFG-001 | Backend đọc config từ timekeeping_unit row, KHÔNG hardcode trong Go/TS code |
| CFG-002 | Cronjob log_time_keeping_flag đọc config snapshot đầu ngày (cache at batch start), dùng cho cả batch |
| CFG-003 | logTimeKeeping action đọc config snapshot đầu ngày (cache per day, invalidate at midnight). Đảm bảo NV punch sáng và chiều dùng cùng config |
| CFG-004 | Đổi config chỉ ảnh hưởng record từ ngày hôm sau (vì cả cronjob + action dùng snapshot đầu ngày) |
| CFG-005 | Chỉ System Admin (ITLeader, ITStaff) được sửa tab Quy định |
| CFG-006 | Mỗi lần lưu config, ghi audit log (ai đổi, đổi gì, lúc nào) |
| CFG-007 | Tab Quy định không còn field Cách tính công; nếu admin cần đổi mode thì phải vào Ca làm việc để chỉnh trên từng shift |
| CFG-008 | Preset chỉ là điểm khởi tạo draft; sau khi publish, unit được coi là Tự cấu hình nếu có chỉnh tay |
| CFG-009 | Sao chép từ đơn vị khác phải hiện diff trước khi lưu nháp, tránh ghi đè mù |
| CFG-010 | Mode Cơ bản mặc định ẩn các block Công chuẩn, Penalty engine, OT rate; chỉ hiện summary + CTA Mở chi tiết |
| CFG-011 | Nút chính không phải Lưu ngay mà là Lưu nháp -> Xem ảnh hưởng -> Áp dụng từ ngày mai |
| CFG-012 | Trước khi publish, UI phải hiển thị ít nhất số chi nhánh, phòng ban, nhân viên, ca làm việc sẽ bị ảnh hưởng |
Dialog — Review impact trước khi publish
text
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ XEM ẢNH HƯỞNG CẤU HÌNH │
│----------------------------------------------------------------------------------------------│
│ Đơn vị: Daisy │
│ Hiệu lực từ: 22/04/2026 │
│ │
│ Phạm vi bị ảnh hưởng: │
│ - 6 chi nhánh │
│ - 9 phòng ban │
│ - 148 nhân viên │
│ - 17 ca làm việc │
│ │
│ Thay đổi chính: │
│ - Max đơn trễ/sớm: 3 -> 4 │
│ - GPS radius: 200m -> 250m │
│ - Preset: Daisy chuẩn -> Tự cấu hình │
│ │
│ Vì hệ thống snapshot đầu ngày, cấu hình mới chỉ áp dụng từ 00:00 ngày hiệu lực. │
│ │
│ [Quay lại draft] [Xác nhận áp dụng] │
└──────────────────────────────────────────────────────────────────────────────────────────────┘Tab Chi nhánh áp dụng
As-Is relation với branch master data:
- Reuse data source / filter pattern / table pattern từ màn branch hiện có
/s/internal-settings/branch?tab=branch. Tab Chi nhánh áp dụngtrongSCR-00không thay thế màn branch master data và không dùng thẳng route đó làm UI mapping.- Mục tiêu của tab này chỉ là map branch global vào
timekeeping_unit. - Nếu branch thiếu metadata gốc (
địa chỉ,branch_label_id, trạng thái...), user dùng action deeplinkXem chi nhánh gốcđể sang màn branch master data và quay lại.
text
+------------------------------------------------------------------------------------------------------------------+
| CẤU HÌNH ĐƠN VỊ CHẤM CÔNG > DAISY > CHI NHÁNH ÁP DỤNG |
| [Tab Đơn vị] [Tab Quy định] [Tab Chi nhánh áp dụng] [Tab Phòng ban áp dụng] [Tab Nhân viên áp dụng] [Tab Triển khai] |
|------------------------------------------------------------------------------------------------------------------|
| [Đơn vị chấm công: Daisy ▼] [Trạng thái ▼] [Tìm chi nhánh...] [Thêm chi nhánh vào đơn vị] |
|------------------------------------------------------------------------------------------------------------------|
| STT | Mã CN | Tên chi nhánh | Loại chi nhánh | Địa chỉ ngắn | Đã thêm vào đơn vị | Loại áp dụng | Thao tác |
| 1 | DA01 | Daisy Q10 | office | 285 CMT8, Q10 | Đang áp dụng | office | [Xem chi nhánh gốc] [Xóa khỏi đơn vị] |
| 2 | DA02 | Daisy Nha Trang | general | 12 LTK, Nha Trang | Đang áp dụng | general | [Xem chi nhánh gốc] [Xóa khỏi đơn vị] |
|------------------------------------------------------------------------------------------------------------------|
| [<] 1 2 [>] |
+------------------------------------------------------------------------------------------------------------------+Popup — Thêm chi nhánh vào đơn vị
text
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ THÊM CHI NHÁNH VÀO ĐƠN VỊ CHẤM CÔNG │
│ Đơn vị chấm công: [Daisy ▼] │
│------------------------------------------------------------------------------------------------------------------│
│ [Tìm theo mã/tên chi nhánh...] [Loại CN ▼] │
│------------------------------------------------------------------------------------------------------------------│
│ [ ] Daisy Q10 office 285 CMT8, Q10 │
│ [ ] Daisy Nha Trang general 12 LTK, Nha Trang │
│ [ ] Daisy Q3 branch 18A 3/2, Q3 │
│------------------------------------------------------------------------------------------------------------------│
│ Rule hiển thị: chỉ hiện branch active, chưa bị disable ở master data │
│ │
│ [Hủy] [Lưu chi nhánh áp dụng] │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘Rules — Tab Chi nhánh áp dụng
| Rule ID | Rule |
|---|---|
| BR-001 | timekeeping_unit_branch chỉ là bảng map cho scope chấm công; không đổi ownership của branch trong org global |
| BR-002 | Một branch có thể được map vào nhiều timekeeping_unit nếu nghiệp vụ cần, nhưng Day-1 UI chỉ cho map khi đã được PO/TL chốt |
| BR-003 | Mapping branch không còn là input trực tiếp của công chuẩn; công chuẩn resolve theo department -> standard_workday_scope_key |
| BR-004 | Khi gỡ map branch, hệ thống cảnh báo nếu branch đó đang được dùng bởi department map, user assignment, working schedule hoặc GPS scope |
| BR-005 | Nút Xem chi nhánh gốc deeplink sang /s/internal-settings/branch?tab=branch hoặc branch detail để sửa master data; quay lại SCR-00 không mất context đơn vị hiện tại |
| BR-006 | Action Xóa khỏi đơn vị ở Day-1 thực hiện soft-disable mapping (disabled = true), không hard delete row |
| BR-007 | List mặc định chỉ hiện branch Đang áp dụng; dùng filter Trạng thái để xem Ngưng áp dụng |
Dialog — Xóa chi nhánh khỏi đơn vị
text
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ XÁC NHẬN XÓA CHI NHÁNH KHỎI ĐƠN VỊ │
│----------------------------------------------------------------------------------------------│
│ Chi nhánh: Daisy Q10 │
│ │
│ Ảnh hưởng hiện tại: │
│ - 3 phòng ban đang map │
│ - 24 nhân viên đang active │
│ - 12 lịch làm việc trong tương lai │
│ - 2 approver scope theo chi nhánh │
│ │
│ Hành động đề xuất: xử lý hết dependency trước khi xóa khỏi đơn vị. │
│ │
│ [Quay lại] [Xem dependency] │
└──────────────────────────────────────────────────────────────────────────────────────────────┘Tab Phòng ban áp dụng
text
+------------------------------------------------------------------------------------------------------------------+
| CẤU HÌNH ĐƠN VỊ CHẤM CÔNG > DAISY > PHÒNG BAN ÁP DỤNG |
| [Tab Đơn vị] [Tab Quy định] [Tab Chi nhánh áp dụng] [Tab Phòng ban áp dụng] [Tab Nhân viên áp dụng] [Tab Triển khai] |
|------------------------------------------------------------------------------------------------------------------|
| [Đơn vị chấm công: Daisy ▼] [Chi nhánh ▼] [Trạng thái ▼] [Tìm phòng ban...] [Thêm phòng ban vào đơn vị] |
|------------------------------------------------------------------------------------------------------------------|
| STT | Mã PB | Tên phòng ban | Chi nhánh | Đã thêm vào đơn vị | Nguồn gợi ý | Số NV đang áp dụng | Thao tác |
| 1 | MKT01 | Marketing | Daisy Q10 | Đang áp dụng | Tự động | 12 | [Xóa khỏi đơn vị] |
| 2 | TV01 | Tư vấn | Daisy Nha Trang | Đang áp dụng | Thủ công | 18 | [Xóa khỏi đơn vị] |
|------------------------------------------------------------------------------------------------------------------|
| [<] 1 2 [>] |
+------------------------------------------------------------------------------------------------------------------+Popup — Thêm phòng ban vào đơn vị
text
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ THÊM PHÒNG BAN VÀO ĐƠN VỊ CHẤM CÔNG │
│ Đơn vị chấm công: [Daisy ▼] Chi nhánh: [Daisy Q10 ▼] │
│------------------------------------------------------------------------------------------------------------------│
│ [Tìm theo mã/tên phòng ban...] │
│------------------------------------------------------------------------------------------------------------------│
│ [ ] Marketing Daisy Q10 12 NV │
│ [ ] Kế toán Daisy Q10 4 NV │
│ [ ] Điều trị Daisy Q10 20 NV │
│------------------------------------------------------------------------------------------------------------------│
│ Rule hiển thị: chỉ hiện department active có `branch_id` thuộc branch đã map vào unit │
│ │
│ [Hủy] [Lưu phòng ban áp dụng] │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘Rules — Tab Phòng ban áp dụng
| Rule ID | Rule |
|---|---|
| DEPT-001 | Dropdown Chi nhánh chỉ hiện branch đã được map ở tab Chi nhánh áp dụng của đúng unit |
| DEPT-002 | Danh sách department được gợi ý từ department.branch_id thuộc tập branch đã map; align DEC-028 |
| DEPT-003 | Tên department có thể trùng giữa nhiều unit; phân biệt bằng branch_id và assignment active, không đổi tên master data |
| DEPT-004 | Khi gỡ map department, hệ thống cảnh báo nếu department đó đang được dùng trong approver scope, working schedule hoặc user assignment |
| DEPT-005 | Tab Phòng ban áp dụng không dùng trạng thái Chờ hiệu lực; Day-1 chỉ có Đang áp dụng / Ngưng áp dụng |
| DEPT-006 | Action Xóa khỏi đơn vị ở Day-1 thực hiện soft-disable mapping (disabled = true), không hard delete row |
| DEPT-007 | List mặc định chỉ hiện department Đang áp dụng; dùng filter Trạng thái để xem Ngưng áp dụng |
Dialog — Xóa phòng ban khỏi đơn vị
text
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ XÁC NHẬN XÓA PHÒNG BAN KHỎI ĐƠN VỊ │
│----------------------------------------------------------------------------------------------│
│ Phòng ban: Marketing | Chi nhánh: Daisy Q10 │
│ │
│ Ảnh hưởng hiện tại: │
│ - 12 nhân viên đang active │
│ - 1 approver scope theo phòng ban │
│ - 4 lịch làm việc trong tương lai │
│ │
│ Hành động đề xuất: chuyển user sang phòng ban khác hoặc gỡ assignment trước khi unmap. │
│ │
│ [Quay lại] [Xem dependency] │
└──────────────────────────────────────────────────────────────────────────────────────────────┘Tab Nhân viên áp dụng
text
+------------------------------------------------------------------------------------------------------------------+
| CẤU HÌNH ĐƠN VỊ CHẤM CÔNG > DAISY > NHÂN VIÊN ÁP DỤNG |
| [Tab Đơn vị] [Tab Quy định] [Tab Chi nhánh áp dụng] [Tab Phòng ban áp dụng] [Tab Nhân viên áp dụng] [Tab Triển khai] |
|------------------------------------------------------------------------------------------------------------------|
| [Đơn vị chấm công: Daisy ▼] [Chi nhánh ▼] [Phòng ban ▼] [Tìm nhân viên...] [Thêm nhân viên vào đơn vị] |
|------------------------------------------------------------------------------------------------------------------|
| STT | Mã NV | Họ tên | Đơn vị chấm công | Chi nhánh | Phòng ban | Từ ngày | Trạng thái | Thao tác |
| 1 | DS001 | Nguyễn A | Daisy | Q10 | Marketing | 01/05/2026 | Đang áp dụng | [Sửa] |
| 2 | DS002 | Trần B | Daisy | Q10 | Marketing | 01/05/2026 | Đang áp dụng | [Sửa] |
| 3 | DS003 | Lê C | Daisy | Nha Trang | Tư vấn | 15/05/2026 | Chờ hiệu lực | [Sửa] |
+------------------------------------------------------------------------------------------------------------------+
| [<] 1 2 3 [>] |
+------------------------------------------------------------------------------------------------------------------+Entry point:
Settings→Internal Settings→Đơn vị chấm công→ tabNhân viên áp dụng. Không thêm fieldtimekeeping_unitvào form tạo/sửa nhân viên hiện tại.
Popup — Thêm nhân viên vào đơn vị
text
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ THÊM NHÂN VIÊN VÀO ĐƠN VỊ CHẤM CÔNG │
│ Đơn vị chấm công: [Daisy ▼] │
│------------------------------------------------------------------------------------------------------------------│
│ [Chi nhánh ▼] [Phòng ban ▼] [Trạng thái áp dụng ▼] [Tìm theo mã/tên...] │
│ [ ] Chỉ hiện nhân viên chưa gán [ ] Chỉ hiện nhân viên đang thuộc unit khác │
│ [Gán toàn bộ theo scope đang lọc] [Đồng bộ theo cơ cấu hiện tại] [Nhập file mã NV] [Dán danh sách mã NV] │
│ [Xóa nhân viên khỏi đơn vị] [Xem nhân sự mới chưa được gán] │
│------------------------------------------------------------------------------------------------------------------│
│ [✓] DS001 Nguyễn A Q10 Marketing Chưa gán — │
│ [✓] DS002 Trần B Q10 Marketing Phương Nam 01/03/2026 → nay │
│ [ ] DS010 Phạm D Q3 Điều trị Daisy 01/05/2026 → nay │
│------------------------------------------------------------------------------------------------------------------│
│ Đã chọn: 2 nhân viên │
│ [Chọn tất cả theo bộ lọc] [Bỏ chọn tất cả] │
│------------------------------------------------------------------------------------------------------------------│
│ Hiệu lực từ [01/05/2026 📅] Hiệu lực đến [ ] Lý do thay đổi [............................] │
│ │
│ Rule hiển thị: chỉ hiện user thuộc branch/department đã map vào unit đang chọn │
│ Nếu NV đang active ở unit khác, hệ thống sẽ đóng assignment cũ và tạo assignment mới theo ngày hiệu lực │
│ │
│ [Hủy] [Lưu nhân viên áp dụng] │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘Hành vi lọc
- Dropdown
Phòng banchỉ hiện department đã được map trong tabPhòng ban áp dụngcủa đúng unit đang mở. - Danh sách nhân viên được resolve theo giao giữa:
timekeeping_unitđang chọndepartment_userbranch_user
- Cùng tên phòng ban như
Marketingcó thể xuất hiện ở nhiều unit; danh sách nhân viên được phân biệt bằng assignment active củatimekeeping_unit_user, không phân biệt bằng tên phòng ban. - Filter
Trạng thái áp dụngcó tối thiểu các giá trị:Tất cảChưa gánĐang thuộc đơn vị nàyĐang thuộc đơn vị khác
- Bảng luôn hiển thị thêm 2 cột thông tin vận hành:
Đơn vị chấm công hiện tạiThời gian áp dụng hiện tại
- Nếu user đã thuộc unit khác, row vẫn được chọn để chuyển unit; UI phải hiển thị rõ đây là hành động re-assignment, không phải create mới thuần túy.
Gán toàn bộ theo scope đang lọcáp dụng cho toàn bộ user đang thuộc giao củaChi nhánh + Phòng ban + trạng tháihiện tại, không cần tick từng dòng.Đồng bộ theo cơ cấu hiện tạitạo preview delta gồm:Nhân sự mới trong scope nhưng chưa gánNhân sự đã rời scope nhưng còn assignment activeNhân sự đổi branch/department chính
Xem nhân sự mới chưa được gánlà shortcut cho tenant lớn để xử lý biến động hàng ngày mà không phải lọc lại từ đầu.Nhập file mã NVcho phép upload file Excel/CSV chỉ gồmstaff_code.Dán danh sách mã NVcho phép dán nhiều mã cách nhau bởi dấu phẩy/xuống dòng để lọc nhanh batch lớn.Xóa nhân viên khỏi đơn vịchỉ khả dụng khi đang filterĐang thuộc đơn vị này.
Bulk actions cho tenant lớn
| Action | Dùng khi nào | Kết quả |
|---|---|---|
Gán toàn bộ theo scope đang lọc | Setup mới cho 1 branch/department lớn | Tạo bulk assignment theo filter hiện tại |
Đồng bộ theo cơ cấu hiện tại | Tenant có biến động nhân sự thường xuyên | Hiện delta add/remove/change trước khi apply |
Xem nhân sự mới chưa được gán | Vận hành hàng ngày sau onboarding | Chỉ hiện danh sách cần xử lý mới |
Dán danh sách mã NV | Có danh sách do HR cung cấp ngoài hệ thống | Chọn nhanh theo batch |
Trạng thái assignment
| Trạng thái | Điều kiện | Hiển thị ở đâu |
|---|---|---|
Chưa gán | User chưa có row timekeeping_unit_user nào active hoặc future | Popup Thêm nhân viên vào đơn vị |
Chờ hiệu lực | effective_from > today | Tab Nhân viên áp dụng, popup confirm/review |
Đang áp dụng | effective_from <= today và effective_to rỗng hoặc >= today | Tab Nhân viên áp dụng, các màn admin/mobile runtime |
Đã hết hiệu lực | effective_to < today | History/audit hoặc popup tra cứu, không cần badge nổi bật ở list Day-1 |
Popup — Confirm chuyển assignment
text
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ XÁC NHẬN CHUYỂN ĐƠN VỊ CHẤM CÔNG │
│----------------------------------------------------------------------------------------------│
│ 1 nhân viên đang active ở đơn vị khác: │
│ - DS002 | Trần B | Phương Nam | 01/03/2026 → nay │
│ │
│ Hệ thống sẽ thực hiện: │
│ - kết thúc assignment hiện tại vào 30/04/2026 │
│ - tạo assignment mới cho Daisy từ 01/05/2026 │
│ │
│ [Quay lại] [Xác nhận chuyển] │
└──────────────────────────────────────────────────────────────────────────────────────────────┘Rules
| Rule ID | Rule |
|---|---|
| TKU-001 | Một user chỉ có 1 active timekeeping_unit tại một thời điểm |
| TKU-002 | Branch/department được map vào timekeeping_unit chỉ để phục vụ timekeeping |
| TKU-003 | Ngưng áp dụng timekeeping_unit chỉ chặn write mới trong timekeeping scope |
| TKU-004 | Không sửa dữ liệu branch/department global từ screen này |
| TKU-005 | Đổi timekeeping_unit ở filter admin thì reset branch, department, shift group về rỗng trước khi query lại |
| TKU-006 | UserCreate và EmployeeProfileCreate không có field timekeeping_unit; mọi assignment Day-1 thực hiện tại SCR-00 > Tab Nhân viên áp dụng |
| TKU-007 | Save bulk assignment phải hỗ trợ cả create mới và re-assignment trong cùng một thao tác |
| TKU-008 | re-assignment = đóng row active cũ (effective_to = new_effective_from - 1 day) rồi insert row mới |
| TKU-009 | Nếu effective_from chồng lấn với assignment active hiện tại và admin không xác nhận chuyển unit, hệ thống không cho lưu |
| TKU-010 | Bảng list phải hiển thị Đơn vị chấm công hiện tại để HR tránh gán nhầm hàng loạt |
| TKU-011 | Nhập file mã NV và Dán danh sách mã NV chỉ là cách chọn nhanh user; vẫn áp dụng đầy đủ validation branch/department/unit |
| TKU-012 | Xóa nhân viên khỏi đơn vị bắt buộc có confirm dialog và hiển thị số NV bị ảnh hưởng trước khi lưu |
| TKU-013 | Chờ hiệu lực chỉ áp dụng cho timekeeping_unit_user; không reuse trạng thái này cho unit/branch/department mapping Day-1 |
| TKU-014 | Gán toàn bộ theo scope đang lọc phải hiển thị preview số user tạo mới / re-assignment trước khi lưu |
| TKU-015 | Đồng bộ theo cơ cấu hiện tại không auto gỡ assignment ngay; luôn hiện delta và cần confirm |
| TKU-016 | Readiness badge Nhân viên đã gán phải đọc được cả khoảng chênh giữa headcount trong scope và headcount đã assign để gợi ý thao tác bulk phù hợp |
| TKU-017 | Sau mỗi lần sync bulk, UI phải cho phép export danh sách affected users để HR lưu đối chiếu |
Tab Triển khai
text
+------------------------------------------------------------------------------------------------------------------+
| CẤU HÌNH ĐƠN VỊ CHẤM CÔNG > DAISY > TRIỂN KHAI |
| [Tab Đơn vị] [Tab Quy định] [Tab Chi nhánh áp dụng] [Tab Phòng ban áp dụng] [Tab Nhân viên áp dụng] [Tab Triển khai] |
|------------------------------------------------------------------------------------------------------------------|
| Giai đoạn triển khai [R1B ▼] |
| Bật admin chấm công [Bật] |
| Bật chấm công mobile [Bật] |
|------------------------------------------------------------------------------------------------------------------|
| ĐIỀU KIỆN BẬT ÁP DỤNG |
| [Chi nhánh đã map: 6/6 ✅] [Phòng ban đã map: 6/6 ✅] [Nhân viên đã gán: 148/150 ⚠️] [Luồng duyệt đã đủ: 5/6 ⚠️] |
| [Ca làm việc đã cấu hình: 17/17 ✅] [Quy định nền: Đủ ✅] [Xuất báo cáo thử: ⚠️] |
|------------------------------------------------------------------------------------------------------------------|
| Ghi chú triển khai [.........................................................................................] |
| |
| [Xem checklist chi tiết] [Lưu triển khai] [Bật áp dụng] |
+------------------------------------------------------------------------------------------------------------------+Rules — Tab Triển khai
| Rule ID | Rule |
|---|---|
| ROL-001 | Bật áp dụng chỉ khả dụng khi checklist không còn badge ❌ |
| ROL-002 | Nhân viên áp dụng đạt ✅ khi số assignment Đang áp dụng + Chờ hiệu lực đủ headcount mục tiêu của unit |
| ROL-003 | Người duyệt đạt ✅ khi tất cả request type Day-1 có approver hợp lệ trong cùng unit |
| ROL-004 | Bật áp dụng không tự seed dữ liệu; chỉ bật cờ cho admin/mobile sau khi dữ liệu đã sẵn sàng |
| ROL-005 | Lưu triển khai lưu trạng thái checklist snapshot + ghi chú triển khai để QA/Ops đối chiếu khi go-live |
| ROL-006 | Badge Ca làm việc, Người duyệt, Xuất báo cáo thử có thể deeplink sang màn ngoài SCR-00, nhưng khi quay lại phải giữ context unit hiện tại |
B2) SCR-00C: Cấu hình ca làm việc / nhóm ca (Work Shift / Shift Group Settings)
Vai trò trong flow
SCR-00C là nơi khóa contract runtime của toàn feature. Đây là màn quyết định mobile sẽ hiện 2 CTA hay 4 CTA, bảng công render trạng thái nào, export có mấy cột mốc, và ca nào bắt buộc GPS.
Input / Output của màn
| Input | Output |
|---|---|
timekeeping_unit đã được tạo ở SCR-00, shift shell hiện có, rule nghiệp vụ từ PRD | shift_template/shift_group chuẩn hóa theo unit, preview runtime rõ ràng cho mobile/admin/export |
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Tạo hoặc chỉnh ca sao cho runtime contract đúng nghiệp vụ của từng unit |
| Primary CTA | Cập nhật / Tạo ca mới trong popup ca |
| Secondary CTA | Tạo nhóm ca, Xem trước kết quả runtime, filter theo Đơn vị chấm công |
| Khối thông tin phải nổi bật | Cách tính công, Công tối đa của ca, GPS cho ca này, Yêu cầu chấm 4 mốc, panel preview runtime |
| Điều không được gây hiểu nhầm | Công tối đa của ca là nhãn UI của field workday; preview runtime là contract hiển thị, không phải mock trang riêng |
As-Is Reuse
- Reuse
ShiftSetting.tsx - Reuse tab
Work Shift - Reuse tab
Shift Group
Delta
| Surface | Delta |
|---|---|
| Work Shift list | Thêm filter Đơn vị chấm công, cột Đơn vị, cột 4 mốc |
| Shift form | Thêm field Đơn vị chấm công, canonical_key, reuse break_time + start_break_time + end_break_time, thêm break_clocking_required, break_mode, break_flex_minutes, workday_calculation_mode, standard_hours, gps_required; đổi label Số ngày công thành Công tối đa của ca |
| Shift group | Chỉ nhận shift thuộc cùng timekeeping_unit |
Layout delta
text
+------------------------------------------------------------------------------------------------------------------+
| CÀI ĐẶT CA LÀM VIỆC |
| [Tab Work Shift] [Tab Shift Group] |
| [Đơn vị chấm công ▼] [Tìm ca...] [Trạng thái ▼] [Tạo ca mới] |
| STT | Mã ca | Tên ca | Đơn vị | Giờ làm việc | Cách tính công | 4 mốc | Nhóm ca | Thao tác |
+------------------------------------------------------------------------------------------------------------------+Popup — Tạo / Chỉnh sửa ca làm việc
text
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ CHỈNH SỬA CA LÀM VIỆC │
│------------------------------------------------------------------------------------------------------------------│
│ Mã ca làm việc * [BVPN_01................] Đơn vị chấm công * [Phương Nam ▼] │
│ Tên ca làm việc * [Bảo vệ VP Phú Lợi......] Cách tính công * [Theo giờ ▼] │
│ Thời gian làm việc * [07:00] [17:00] Công tối đa của ca * [1.0] công │
│ ⓘ Reuse field `workday` hiện có, chỉ đổi label hiển thị │
│ Giờ chuẩn của ca [8.0] giờ ⓘ Chỉ hiện khi `Cách tính công = Theo giờ` │
│ GPS cho ca này [Bắt buộc ▼] ⓘ Chọn ca này có bắt buộc GPS khi chấm công hay không │
│ Thời gian giữa ca [Bật] Yêu cầu chấm 4 mốc [Bật] │
│ Khung nghỉ mặc định [12:00] [13:00] Cách áp dụng giờ nghỉ [Linh hoạt ▼] │
│ Biên độ linh hoạt [60] phút ⓘ Chỉ hiện khi `Cách áp dụng giờ nghỉ = Linh hoạt` │
│ ⓘ VD: khung 12:00-13:00, biên độ 60p → ra nghỉ từ 11:00, │
│ vào lại đến 14:00 vẫn hợp lệ │
│ Đang hoạt động [Bật] │
│------------------------------------------------------------------------------------------------------------------│
│ ĐỐI TƯỢNG ÁP DỤNG │
│ STT | Tên phân quyền | Số lượng │
│ ... │
│------------------------------------------------------------------------------------------------------------------│
│ XEM TRƯỚC KẾT QUẢ RUNTIME │
│ Mobile CTA: [Vào ca] [Ra nghỉ] [Vào lại] [Ra về] │
│ Popup ngày công: 4 dòng mốc + trạng thái `Thiếu giữa trưa` nếu thiếu segment 2 │
│ Export chi tiết ngày: 4 cột mốc │
│ [Cập nhật] │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘Preview runtime trong form ca
Khi admin đổi các field break_clocking_required, break_mode, workday_calculation_mode, gps_required, form phải cập nhật ngay panel preview để người cấu hình thấy:
| Loại preview | Mục đích |
|---|---|
| Mobile CTA preview | Biết ca sẽ hiện 2 CTA hay 4 CTA trên app |
| Working Sheet / Day Detail preview | Biết popup ngày công sẽ hiển thị 2 mốc hay 4 mốc, status nào có thể xuất hiện |
| Export preview | Biết file chi tiết ngày sẽ có 2 cột hay 4 cột mốc |
| Policy summary | Nhắc lại Theo giờ / Theo ngày công, GPS bắt buộc / không bắt buộc, Fixed / Flex |
Interaction Behavior Contract
| Tình huống | Behavior mong muốn |
|---|---|
| Save mode | Manual save trong popup; không auto-save giữa chừng |
| Dirty state | Bất kỳ đổi field nào trong popup làm hiện trạng thái Có thay đổi chưa lưu |
| Unsaved changes | Đóng popup khi còn thay đổi chưa lưu phải hỏi Bỏ thay đổi / Tiếp tục chỉnh |
| Conditional display | standard_hours, break_mode, break_flex_minutes ẩn/hiện tức thời theo rules ở dưới |
| Preview runtime | Đổi field break_clocking_required, break_mode, workday_calculation_mode, gps_required phải refresh preview ngay, không cần bấm lưu |
| Async validation | canonical_key validate unique theo timekeeping_unit trước khi submit; submit lock CTA để tránh double-click |
| Impacted shift đang live | Nếu shift đã được dùng trong lịch tương lai, hiển thị impact banner nêu số lịch bị ảnh hưởng trước khi save |
Action Feedback Contract
| Action / Situation | Feedback UI | Hành động tiếp theo |
|---|---|---|
| Lưu ca thành công | Toast xanh + refresh list + highlight row vừa lưu | Giữ filter unit hiện tại |
| Validation lỗi | Inline error ngay dưới field + focus field lỗi đầu tiên | Không đóng popup |
| Shift đang được dùng trong lịch tương lai | Warning banner + confirm modal trước khi save | User xác nhận rồi mới cập nhật |
| Preview runtime thay đổi | Panel preview cập nhật realtime | Không cần action phụ |
| Lỗi lưu ca | Toast đỏ + giữ nguyên toàn bộ input | Có nút Thử lại |
Rules
| Rule ID | Rule |
|---|---|
| SHIFT-001 | canonical_key unique trong cùng timekeeping_unit |
| SHIFT-002 | break_clocking_required = true thì Working Schedule/Sheet/Mobile phải render contract 4 mốc |
| SHIFT-002A | break_clocking_required = false nhưng break_time = true vẫn là ca 2 mốc; hệ thống trừ break_duration theo schedule, không mở CTA Ra nghỉ / Vào lại |
| SHIFT-002B | break_mode chỉ hiện khi break_clocking_required = true; không dùng 2 checkbox độc lập để tránh chọn đồng thời fixed và flex |
| SHIFT-002C | break_mode = flex bắt buộc phải có start_break_time, end_break_time, break_flex_minutes > 0; Day-1 các field này chỉ là metadata/tham chiếu, không dùng để block/phạt mốc Ra nghỉ / Vào lại |
| SHIFT-003 | Không update shift theo name; dùng canonical_key |
| SHIFT-004 | Cách tính công được quyết định ở cấp shift, không lấy từ timekeeping_unit |
| SHIFT-005 | Công tối đa của ca là label hiển thị mới của field workday hiện có; Day-1 không đổi tên cột DB để giảm rủi ro |
| SHIFT-006 | Nếu workday_calculation_mode = 'hourly' thì bắt buộc có standard_hours > 0 |
| SHIFT-007 | gps_required được quyết định ở cấp shift; timekeeping_unit chỉ giữ bán kính GPS mặc định |
| SHIFT-008 | gps_required = false nghĩa là ca này không chặn punch bởi GPS; logic remote_onetime / remote_weekly vẫn là flow request riêng, không phải option trong dropdown |
| SHIFT-009 | Panel Xem trước kết quả runtime phải refresh tức thời khi admin đổi field quan trọng của ca |
| SHIFT-010 | Preview chỉ là contract hiển thị; không thay thế validation backend, nhưng phải phản ánh đúng behavior mà mobile / working sheet / export sẽ dùng |
Shift form — Contract chuẩn hóa
| Trường (Field code) | Tên hiển thị (VI) | Kiểu (Type) | Bắt buộc (Required) | Quy tắc (Rule) |
|---|---|---|---|---|
timekeeping_unit_id | Đơn vị chấm công | QSelect | Yes | Chỉ chọn unit active |
canonical_key | Mã ca chuẩn | QInput | Yes | Unique trong cùng unit |
workday_calculation_mode | Cách tính công | QSelect | Yes | fixed / hourly |
workday | Công tối đa của ca | QInput number | Yes | Reuse field workday hiện có; min 0.25, max 1.0, step 0.25 |
standard_hours | Giờ chuẩn của ca | QInput number | Conditional | Chỉ hiện khi workday_calculation_mode = 'hourly'; min 0.5, max 24 |
gps_required | GPS cho ca này | QSelect | Yes | Hiển thị 2 option Bắt buộc / Không bắt buộc; lưu tương ứng true / false |
break_time | Có nghỉ giữa ca | QToggle | Yes | Bật nếu ca có nghỉ giữa ca theo schedule |
start_break_time | Giờ bắt đầu khung nghỉ mặc định | QTime | Conditional | Bắt buộc khi break_time = true |
end_break_time | Giờ kết thúc khung nghỉ mặc định | QTime | Conditional | Bắt buộc khi break_time = true, phải > start_break_time |
break_clocking_required | Yêu cầu chấm 4 mốc | QToggle | Yes | Bật = contract 4 mốc |
break_mode | Cách áp dụng giờ nghỉ | QSelect | Conditional | Chỉ hiện khi break_clocking_required = true; giá trị fixed / flex. flex = Day-1 chỉ ghi nhận 4 mốc + tính actual hours, không hard-validate mốc nghỉ |
break_flex_minutes | Biên độ linh hoạt | QInput number | Conditional | Chỉ hiện khi break_mode = flex; min 1, max 120; Day-1 chỉ lưu metadata tham chiếu |
Shift form — Conditional display
| Điều kiện | Ẩn/Hiện |
|---|---|
workday_calculation_mode = 'fixed' | Ẩn standard_hours |
workday_calculation_mode = 'hourly' | Hiện standard_hours |
break_time = false | Ẩn start_break_time, end_break_time, break_clocking_required, break_mode, break_flex_minutes |
break_time = true | Hiện start_break_time, end_break_time, break_clocking_required |
break_clocking_required = false | Ẩn break_mode, break_flex_minutes |
break_clocking_required = true | Hiện break_mode |
break_mode = fixed | Ẩn break_flex_minutes |
break_mode = flex | Hiện break_flex_minutes |
B2) SCR-00D: Cấu hình người duyệt chấm công (Timekeeping Approver Settings)
Vai trò trong flow
SCR-00D đảm bảo các request timekeeping sau rollout luôn đi đúng người duyệt trong cùng timekeeping_unit. Đây là mắt xích chặn việc setup xong dữ liệu nhưng luồng vận hành vẫn đổ sai approver.
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Khóa routing approver theo unit/branch/department cho các request Day-1 |
| Primary CTA | Lưu cấu hình người duyệt |
| Secondary CTA | Thêm dòng cấu hình, filter Đơn vị chấm công, deeplink quay lại SCR-00 |
| Khối thông tin phải nổi bật | Request type, loại phạm vi, phạm vi áp dụng, approver đang active |
| Điều không được gây hiểu nhầm | Tab remote là legacy, feature này không redesign và không tạo request type mới |
Interaction Behavior Contract
| Tình huống | Behavior mong muốn |
|---|---|
| Save mode | Manual save theo form approver |
| Dirty state | Rời tab khi còn thay đổi chưa lưu phải hỏi confirm |
| Filter unit | Nếu vào từ readiness deeplink thì auto-select đúng unit và khóa đến khi quay lại |
| Cascade reset | Đổi Loại phạm vi phải reset field Phạm vi áp dụng để tránh save sai scope |
| Scope validation | Không cho chọn approver hoặc scope ngoài timekeeping_unit đang mở |
Action Feedback Contract
| Action / Situation | Feedback UI | Hành động tiếp theo |
|---|---|---|
| Lưu cấu hình thành công | Toast xanh + row status refresh | Cho phép quay lại SCR-00 > Triển khai |
| Scope không hợp lệ | Inline error + helper text giải thích lý do | Focus về field lỗi |
| Gỡ rule đang active | Warning confirm modal hiển thị request type/phạm vi bị ảnh hưởng | User xác nhận rồi mới gỡ |
As-Is Reuse
- Reuse
ApproverPage.tsx - Reuse layout tab hiện có của approver settings, bao gồm cả tab
remoteđang tồn tại trong hệ thống - Chỉ mô tả delta cho các tab timekeeping:
- clock in/out
- forget clock
- late/early
- change shift
- overtime
- leave
Delta
| Surface | Delta |
|---|---|
| Filter bar | Thêm filter Đơn vị chấm công |
| Table | Thêm cột Đơn vị, Loại phạm vi, Phạm vi áp dụng |
| Form | Chỉ cho chọn approver/scope nằm trong cùng timekeeping_unit |
| Tab gating | Chỉ mở delta cho request type thuộc Day-1 scope; tab remote là flow legacy, không redesign trong feature này |
Rules
| Rule ID | Rule |
|---|---|
| APR-001 | Approver config timekeeping không được làm thay đổi flow approval ngoài timekeeping |
| APR-002 | Chỉ resolve approver trong cùng timekeeping_unit |
| APR-003 | Nếu request type không thuộc scope timekeeping, UI không thay đổi |
| APR-004 | Tab remote vẫn tồn tại theo UI hiện tại; feature này chỉ ràng scope approver cho timekeeping-unit, không tạo tab mới cũng không mô tả redesign flow remote |
B2) SCR-01: Lịch làm việc (Working Schedule)
Reuse
- Reuse
WorkingSchedule.tsxhiện có (filter bar, bảng tuần, tạo/sửa lịch) - Reuse
WorkingScheduleImport.tsx+useWorkingScheduleImport.ts— import Excel đã hoàn chỉnh (parse template, upsert time_slot_user). Chỉ cần scope theo unit - Reuse
WorkingScheduleForm.tsx— form tạo/sửa lịch NV
Vai trò trong flow
SCR-01 là màn "biến cấu hình thành lịch làm thật". Nếu SCR-00 và SCR-00C chưa xong thì SCR-01 phải fail sớm bằng empty state có hướng dẫn, không cho HR tạo lịch trong trạng thái nửa vời.
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Gán ca đúng người, đúng tuần, đúng unit để mobile và bảng công chạy được |
| Primary CTA | Import Excel cho setup hàng loạt; Tạo lịch cho case lẻ |
| Secondary CTA | Xuất Excel, filter theo Đơn vị / Chi nhánh / Phòng ban / Nhóm ca |
| Khối thông tin phải nổi bật | Unit đang thao tác, tuần hiện tại, nhóm ca đang áp dụng, cảnh báo thiếu prerequisite |
| Điều không được gây hiểu nhầm | Import Excel không bypass validation unit/branch/department/shift; mọi row vẫn phải thuộc scope đã map |
Delta
- Thêm filter
Đơn vị chấm công+Chi nhánh(chưa có trên màn hiện tại) - Sau khi chọn unit:
- chỉ hiện branch/department đã map vào unit đó
- chỉ hiện shift template thuộc unit đó
- reset các filter con trước khi load data mới
- nếu unit chưa map branch/department thì hiển thị empty state hướng dẫn về
SCR-00
Layout
text
+---------------------------------------------------------------------------------------------------------------+
| LỊCH LÀM VIỆC |
| [Đơn vị chấm công ▼] [Chi nhánh ▼] [Phòng ban ▼] [Nhóm ca ▼] [Tuần ▼] [Tìm nhân viên...] |
| [Import Excel] [Xuất Excel] [Tạo lịch] |
+---------------------------------------------------------------------------------------------------------------+Interaction Behavior Contract
| Tình huống | Behavior mong muốn |
|---|---|
| Save mode | Manual save cho form tạo/sửa lịch; import là batch job riêng |
| Unsaved changes | Đóng form tạo/sửa lịch khi còn thay đổi phải hỏi confirm |
| Filter apply | Đổi Đơn vị chấm công reset Chi nhánh, Phòng ban, Nhóm ca, Từ khóa rồi mới query |
| Search timing | Ô tìm kiếm debounce 300ms; đổi tuần apply ngay |
| Import batch | Import chạy background, có row result thành công / thất bại / bị bỏ qua |
| Empty prerequisite | Nếu thiếu branch/department/shift thì hiển thị empty state + deeplink về đúng màn gốc |
Action Feedback Contract
| Action / Situation | Feedback UI | Hành động tiếp theo |
|---|---|---|
| Tạo / sửa lịch thành công | Toast xanh + row/calendar refresh | Giữ nguyên filter hiện tại |
| Import thành công một phần | Banner summary + file/error table tải lại được | Cho phép retry các dòng lỗi |
| Import lỗi toàn bộ | Toast đỏ + giữ nguyên context import | Có nút Tải template / Xem lỗi |
| Thiếu prerequisite | Empty state có CTA Mở bước còn thiếu | Quay lại đúng SCR-00 hoặc SCR-00C |
B2) SCR-02: Bảng công (Working Time Sheet)
Vai trò trong flow
SCR-02 là cockpit vận hành hàng ngày của HR/Manager. Màn này không chỉ hiển thị dữ liệu, mà còn là điểm xuất phát cho 3 hành động tiếp theo:
- drill-down sang
SCR-03để xem chi tiết ngày - sang
SCR-07để đối soát request - sang
SCR-06để chốt export theo đúng filter hiện tại
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Phát hiện bất thường theo ngày/tháng và đi tiếp sang màn xử lý đúng |
| Primary CTA | Click ô ngày / row để mở SCR-03 |
| Secondary CTA | Các nút export, mở request liên quan, đổi filter scope |
| Khối thông tin phải nổi bật | Filter Đơn vị chấm công, ô ngày có anomaly, tổng hợp trạng thái theo tháng |
| Điều không được gây hiểu nhầm | Export phải lấy đúng filter đang đứng; SCR-02 là cockpit quan sát, không phải nơi sửa cấu hình |
Delta
- Thêm filter
Đơn vị chấm công - Ngày có thể có:
Đủ mốcThiếu đầu caThiếu giữa trưaThiếu cuối caNghỉ
- Không đổi shell bảng hiện có
- Khi đổi
timekeeping_unit, resetbranch,department,nhóm ca,từ khóa tìm nhân viên
Layout
text
+---------------------------------------------------------------------------------------------------------------+
| BẢNG CÔNG |
| [Đơn vị chấm công ▼] [Chi nhánh ▼] [Phòng ban ▼] [Nhóm ca ▼] [Tháng ▼] [Tìm nhân viên...] |
| [Xuất chi tiết ngày] [Xuất tổng hợp tháng] [Xuất báo cáo đi trễ/về sớm] [Xuất báo cáo tăng ca] [Xuất báo cáo quên chấm] |
+---------------------------------------------------------------------------------------------------------------+Filter rules
| Rule ID | Rule |
|---|---|
| WS-001 | Chi nhánh dropdown chỉ hiện branch đã map vào timekeeping_unit đang chọn |
| WS-002 | Phòng ban dropdown chỉ hiện department đã map vào timekeeping_unit đang chọn |
| WS-003 | Nhóm ca dropdown chỉ hiện shift group thuộc timekeeping_unit đang chọn |
| WS-004 | Đổi Đơn vị chấm công → reset tất cả filter con về rỗng |
| WS-005 | Đổi Chi nhánh → reset Phòng ban (vì department scoped theo branch) |
Interaction Behavior Contract
| Tình huống | Behavior mong muốn |
|---|---|
| Filter apply | Dropdown apply ngay; ô tìm kiếm debounce 300ms |
| Cascade reset | Đổi Đơn vị chấm công reset toàn bộ filter con; đổi Chi nhánh reset Phòng ban |
| Drill-down | Click ô ngày hoặc trạng thái mở SCR-03; focus quay lại đúng ô khi đóng popup |
| Export handoff | Mọi CTA export mở SCR-06 với filter hiện tại ở trạng thái readonly, không cho đổi unit trong modal export |
| Loading | Bảng giữ header/filter sticky, row skeleton trong lúc query lại |
Action Feedback Contract
| Action / Situation | Feedback UI | Hành động tiếp theo |
|---|---|---|
| Đổi filter xong | Refresh table + giữ vị trí scroll đầu bảng | User tiếp tục drill-down hoặc export |
| Không có dữ liệu theo scope | Empty state + helper text nêu thiếu lịch/chưa rollout/chưa có punch | CTA sang SCR-01 hoặc SCR-00 nếu phù hợp |
| Export được khởi tạo | Toast hoặc banner Đang tạo file export | Có link mở SCR-06 hoặc trạng thái job |
B2) SCR-03: Chi tiết ngày công (Working Time Sheet Day Detail)
Contract
- Reuse popup chi tiết ngày
- Với ca 2 mốc: hiển thị
Vào ca,Ra về - Với ca 4 mốc: hiển thị đủ 4 dòng
- Overlay request timekeeping ngay trong popup
- Đây là popup drill-down của
SCR-02, không phải màn độc lập trong navigation
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Hiểu chính xác ngày đó thiếu mốc ở đâu, có request gì, và có cần đi tiếp sang danh sách đơn không |
| Primary CTA | Mở danh sách đơn liên quan khi ngày có request hoặc cần tạo request follow-up |
| Secondary CTA | Đóng popup |
| Khối thông tin phải nổi bật | 2 mốc/4 mốc thực tế, trạng thái ngày, ngày công, request overlay |
| Điều không được gây hiểu nhầm | Thiếu giữa trưa chỉ là trạng thái quan sát; Day-1 không auto suy ra 0.5 công trong popup |
Interaction Behavior Contract
| Tình huống | Behavior mong muốn |
|---|---|
| Open popup | Focus vào heading popup, không mất context row/cell phía sau |
| Close popup | Esc hoặc Đóng trả focus về đúng ô vừa click ở SCR-02 |
| Request drill | Click request mở SCR-07 ở context cùng NV/ngày/unit |
| Readonly mode | Không chỉnh dữ liệu trực tiếp trong popup này |
Action Feedback Contract
| Action / Situation | Feedback UI | Hành động tiếp theo |
|---|---|---|
| Không có request liên quan | Inline helper Chưa có đơn liên quan | CTA Mở danh sách đơn nếu role được phép |
| Thiếu mốc / partial | Warning banner theo trạng thái ngày | User drill sang request list nếu cần |
Layout — Ca 2 mốc
text
┌─────────────────────────────────────────────────┐
│ Nguyễn Văn A — 06/04/2026 (Thứ Hai) │
│ Ca: Ca hành chính (08:00 - 17:00) │
│ Đơn vị: Phương Nam | CN: TT Y khoa Đà Lạt │
│─────────────────────────────────────────────────│
│ Vào ca: 08:15 ⏰ Trễ 15 phút │
│ Ra về: 17:02 ✅ │
│─────────────────────────────────────────────────│
│ Ngày công: 1.0 │
│ Trạng thái: Đủ mốc │
│─────────────────────────────────────────────────│
│ Đơn liên quan: │
│ 📋 REQ-2026-0412 Đi trễ — Đã duyệt bước 1 │
└─────────────────────────────────────────────────┘Layout — Ca 4 mốc
text
┌─────────────────────────────────────────────────┐
│ Trần Thị B — 06/04/2026 (Thứ Hai) │
│ Ca: Bác sĩ Ca 1 (08:00 - 17:00, 4 mốc) │
│ Đơn vị: Daisy | CN: 140 Hai Bà Trưng │
│─────────────────────────────────────────────────│
│ Vào ca: 08:00 ✅ │
│ Ra nghỉ: 12:10 ✅ │
│ Vào lại: — ❌ Thiếu mốc │
│ Ra về: — ❌ Thiếu mốc │
│─────────────────────────────────────────────────│
│ Ngày công: Chưa đủ dữ liệu để kết luận tự động │
│ Trạng thái: Thiếu giữa trưa │
│─────────────────────────────────────────────────│
│ Đơn liên quan: (chưa có) │
└─────────────────────────────────────────────────┘B2) SCR-04: Lịch sử chấm công nhân viên (Employee Timekeeping History)
Delta
- Reuse màn history hiện có
- Hiển thị theo
day summary, không collapse sai theo raw row - Có
Đơn vị chấm côngdạng readonly để giải thích scope - Nếu user không thuộc rollout unit thì giữ nguyên history hiện tại, không mở thêm nhãn 4 mốc
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Tra cứu lịch sử cá nhân hoặc theo hồ sơ nhân viên để đối soát nhanh |
| Primary CTA | Không có CTA ghi dữ liệu; đây là màn readonly |
| Secondary CTA | Xem ngày chi tiết nếu shell hiện có hỗ trợ |
| Khối thông tin phải nổi bật | Badge Đơn vị chấm công, summary theo ngày, trạng thái thực tế |
B2) SCR-05: Chấm công trên mobile cho nhân viên (Staff Mobile Attendance)
Vai trò trong flow
SCR-05 là mặt tiền của toàn bộ feature đối với nhân viên. Toàn bộ cảm giác "dùng thật" trên mobile phụ thuộc vào việc màn này luôn trả lời được 3 câu hỏi:
- Hôm nay tôi phải bấm gì?
- Hệ thống đang hiểu tôi ở trạng thái nào của ca?
- Nếu bị lỗi/chưa đủ mốc thì tôi phải đi đâu tiếp theo?
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Chấm đúng mốc tiếp theo của ca hôm nay và tự hiểu trạng thái hiện tại |
| Primary CTA | CTA động theo state machine: Vào ca / Ra nghỉ / Vào lại / Ra về |
| Secondary CTA | Lịch sử, Tạo đơn, mở calendar/thống kê cá nhân |
| Khối thông tin phải nổi bật | Ca hôm nay, trạng thái hiện tại, GPS hợp lệ hay không, quota đơn còn lại nếu đang ở flow request |
| Điều không được gây hiểu nhầm | CTA được quyết định bởi shift hôm nay, không phải bởi tên unit; Day-1 không có CTA remote mới |
Rollout contract (Updated)
| Unit | Release | CTA | Ghi chú |
|---|---|---|---|
| PN ca thường (Ca HC, Ca 1–8, Ca tối) | 1B | Vào ca, Ra về | 2 mốc |
| PN ca gãy (6 ca) | 1B | Vào ca, Ra nghỉ, Vào lại, Ra về | 4 mốc — PN xác nhận cần track trễ sáng + chiều (DEC-014) |
| Daisy khối DV (BS, Phụ tá, Điều trị) | 1B | Vào ca, Ra nghỉ, Vào lại, Ra về | 4 mốc, ca có break_clocking_required = true |
| Daisy khối VP + ca xuyên trưa | 1B | Vào ca, Ra về | 2 mốc, ca có break_clocking_required = false |
Thay đổi lớn vs v2.0: PN không còn đi trước (1A) với 2 mốc. PN ca gãy cần 4 mốc → PN và Daisy cùng rollout 1B.
CTA được quyết định bởi shift_template.break_clocking_required của ca user hôm đó, không phải theo timekeeping_unit.
Reuse
- Reuse
attendance_screen.dart+attendance_bloc.dart— state machine 2 mốc (CheckIn/CheckOut) đã hoàn chỉnh - Reuse GPS check trong
log_time_keeping.go—CalculateDistance()+ radius check đã có. Remote bypass (remote_onetime,remote_weekly) đã có - Reuse calendar view lịch sử chấm công trên mobile — đã có hiển thị chấm công + đơn theo tháng
As-Is Audit
attendance_screen.darthiện có 2 tabTimekeeping/Statistic, calendar theo tháng, danh sách lịch sử yêu cầu và stats cards cho trễ/sớm, quên chấm, phép năm【không redesign layout tổng thể trong phase này】.- App hiện vẫn có flow request
remotetrongRequestApprovalScreen, và calendar attendance đang xem requestremoteapproved như một điều kiện normalize trạng thái ngày chấm công. - Vì vậy proposal của SCR-05 là: reuse shell mobile hiện tại, chỉ thêm state 4 mốc + quota display + GPS scope theo unit; không viết lại request center.
Delta (build mới)
- State machine 4 mốc: thêm 3 state + 2 event (
BreakOut,BreakIn) vàoattendance_bloc.dart - GPS scope: thay query ALL branches → chỉ branch trong
timekeeping_unit_branch - Quota "X/3 lần": hiện chưa có UI, cần thêm
- Running total tháng: cần thêm
Day-1 guardrail
- Chỉ mở chấm công
on-sitecó GPS. - Remote: reuse logic
remote_onetimehiện có — Daisy Ca 3 Marketing dùng đơn remote approved, logic bypass GPS đã có sẵn tronglog_time_keeping.go(DEC-016). - Feature này không tạo request entry
remotemới trên attendance shell. Nếu tenant/user vẫn được cấp flowremotelegacy ở request center hiện tại thì flow đó tiếp tục chạy theo behavior cũ. - Nếu GPS ngoài branch hợp lệ trong
timekeeping_unit, hiển thị lỗi và không ghi event. - Nếu user chưa được map vào unit rollout, mobile giữ behavior legacy hiện có.
State matrix — Ca 2 mốc (break_clocking_required = false)
| State | Điều kiện | CTA |
|---|---|---|
no_event | Chưa có record hôm nay | Vào ca |
has_clock_in | Có clock_in, chưa có clock_out | Ra về |
complete | Có clock_in + clock_out | Ẩn CTA, chỉ xem lịch sử |
State matrix — Ca 4 mốc (break_clocking_required = true)
| State | Điều kiện | CTA |
|---|---|---|
no_event | Chưa có record | Vào ca |
seg0_open | seg0 có clock_in, chưa clock_out | Ra nghỉ |
break_open | seg0 complete, chưa có seg1 | Vào lại |
seg1_open | seg1 có clock_in, chưa clock_out | Ra về |
complete | seg0 + seg1 đều complete | Ẩn CTA, chỉ xem lịch sử |
Quyết định CTA dựa trên: shift_template.break_clocking_required của ca NV hôm đó. Flutter dev check field này trong response từ API, KHÔNG dùng timekeeping_unit để quyết định.
Mobile request entry
Day-1 chỉ mô tả và mở delta cho các entry request sau trên shell hiện có:
Quên chấm côngĐi trễ / Về sớmĐổi caOT
Không mở thêm trong phạm vi feature này:
Remote- luồng annual leave report riêng
Mobile — Quota đơn còn lại (hiển thị cho NV)
Khi NV mở form tạo đơn trễ/sớm hoặc quên chấm:
text
┌─────────────────────────────────────────────────┐
│ ĐƠN ĐI TRỄ / VỀ SỚM │
│ │
│ Bạn đã sử dụng: 2/3 lần trong tháng 04/2026 │
│ ⓘ Từ lần thứ 4 sẽ bị tính phạt theo quy định │
│ │
│ [Loại: Đi trễ ▼] │
│ [Ngày: 06/04/2026] │
│ [Từ: 08:00] [Đến: 08:45] │
│ [Lý do: ...........................] │
│ │
│ [Gửi đơn] │
└─────────────────────────────────────────────────┘| Rule ID | Rule |
|---|---|
| MOB-REQ-001 | Hiển thị "X/3 lần" — count scoped theo timekeeping_unit + tháng hiện tại |
| MOB-REQ-002 | Count chỉ đếm request có status approved hoặc pending (không đếm rejected/canceled) |
| MOB-REQ-003 | Vẫn cho phép tạo đơn kể cả khi > 3 lần — HR quyết định approve/reject |
Mobile — Lịch sử chấm công cá nhân (NV self-service)
NV xem lịch sử chấm công của mình trên app:
text
┌─────────────────────────────────────────────────┐
│ LỊCH SỬ CHẤM CÔNG — Tháng 04/2026 │
│ │
│ 06/04 (T7) Ca sáng 8h │
│ ✅ Vào ca: 07:58 Ra về: 17:05 │
│ Ngày công: 1.0 │
│ │
│ 05/04 (T6) Ca sáng 8h │
│ ⏰ Vào ca: 08:32 Ra về: 17:00 │
│ Đi trễ: 32 phút │
│ Ngày công: 1.0 │
│ 📋 Đơn đi trễ: Đã duyệt │
│ │
│ 04/04 (T5) Ca BS 4 mốc │
│ ✅ Vào: 08:00 Ra nghỉ: 12:05 │
│ Vào lại: 13:55 Ra về: 17:02 │
│ Ngày công: 1.0 │
│ │
│ Tổng tháng: 5/22 ngày công │
│ Trễ: 1 lần | Sớm: 0 lần | Quên: 0 lần │
└─────────────────────────────────────────────────┘| Rule ID | Rule |
|---|---|
| MOB-HIST-001 | NV chỉ xem data của mình (self-only, scope timekeeping_unit tự resolve) |
| MOB-HIST-002 | Ca 4 mốc hiển thị 4 dòng, ca 2 mốc hiển thị 2 dòng |
| MOB-HIST-003 | Hiển thị running total tháng: ngày công, lần trễ, lần sớm, lần quên |
| MOB-HIST-004 | Overlay request nếu có (đơn đi trễ, quên chấm, nghỉ phép...) |
Interaction Behavior Contract
| Tình huống | Behavior mong muốn |
|---|---|
| Punch CTA | Khi user bấm CTA, lock button hiện tại + spinner cho đến khi API trả kết quả |
| GPS / permission check | Kiểm tra quyền vị trí trước khi gọi API; nếu chưa cấp quyền thì chặn và mở helper xin quyền |
| State refresh | Sau punch thành công, state machine và lịch sử hôm nay refresh ngay trên màn |
| Unsaved / offline | Day-1 không có offline queue; nếu mất mạng thì không ghi local draft punch |
| Retry | Nếu API lỗi tạm thời, giữ nguyên CTA hiện tại và cho phép bấm lại sau khi lỗi biến mất |
| Request follow-up | Từ trạng thái lỗi/thiếu mốc, CTA phụ dẫn sang request flow hiện có mà không làm mất context ngày |
Action Feedback Contract
| Action / Situation | Feedback UI | Hành động tiếp theo |
|---|---|---|
| Punch thành công | Success toast/snackbar + badge trạng thái mới | CTA chuyển sang mốc kế tiếp hoặc ẩn nếu complete |
| GPS ngoài phạm vi | Error banner/snackbar nêu chi nhánh hợp lệ gần nhất hoặc lý do bị chặn | User di chuyển vị trí rồi thử lại |
| Chưa cấp quyền vị trí | Bottom sheet/helper modal hướng dẫn cấp quyền | Sau khi cấp quyền, quay lại CTA cũ |
| Lỗi mạng / server | Error snackbar + nút Thử lại | Không đổi state machine local |
| Không có lịch hôm nay | Empty helper Hôm nay chưa có ca + link lịch sử | Không hiện CTA chấm công |
Guardrails
- Không đổi mobile behavior của user Diva hiện tại nếu không thuộc rollout unit.
- Không mở CTA 4 mốc cho PN ca thường (Ca HC, Ca 1–8, Ca tối). PN ca gãy = 4 mốc (DEC-014 confirmed).
- Không mở CTA 2 mốc đơn giản cho Daisy nếu ca được đánh dấu 4 mốc (
break_clocking_required = true). - Daisy VP user có ca 2 mốc → CTA giống PN, không ép 4 mốc.
- Không mở CTA/request entry
remotemới trên attendance shell của Daisy/PN. - Daisy Ca 3 Marketing Trực Page (chấm từ xa): Day-1 dùng workaround
remote_onetimerequest approved trước (DEC-016).
B2) SCR-06: Xuất dữ liệu chấm công (Timekeeping Export)
Vai trò trong flow
SCR-06 là điểm "chốt sổ vận hành", không phải nơi tự sửa dữ liệu. HR phải đi vào export từ SCR-02 với đúng filter tháng/unit/branch/department đã đứng sẵn, để tránh tình trạng xem một scope nhưng export ra scope khác.
Scope
Export báo cáo vận hành chấm công. Export tính tiền phạt + tiền OT tự động từ config per unit (DEC-012 v3, DEC-026). HR KHÔNG cần tính tay trên Excel.
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Xuất đúng loại báo cáo theo đúng scope đang đứng ở SCR-02 để chốt vận hành |
| Primary CTA | Xuất file / Tạo file export |
| Secondary CTA | Chọn loại file, xem preview cột, quay lại SCR-02 |
| Khối thông tin phải nổi bật | Scope export hiện tại (unit/tháng/branch/department), loại báo cáo, trạng thái job export |
| Điều không được gây hiểu nhầm | Export không được tự đổi scope so với SCR-02; đây không phải payroll bridge |
File types — Reuse map
| File | Mục đích | Reuse codebase | Delta |
|---|---|---|---|
| Bảng công tháng | Chốt công theo tháng | Reuse ExportWorkingTimeSheet.tsx (hiện có 3 cột + grid). Override column mapping thêm 19 cột | Extend column mapping + filter unit + cột Tiền phạt + Tiền OT |
| Chi tiết ngày | Điều tra từng ngày | Reuse ExportWorkingTimeSheetByDay.tsx (hiện đã có 14 cột: STT, Ngày, Họ tên, Mã NV, Phòng ban, CN vào, CN ra, Ca, Giờ vào, Giờ ra, Trễ phút, Sớm phút, Đơn, OT. Logic getTimeLateArrival() + getTimeEarlyLeave() đã có) | Thêm filter unit + 4 cột cho ca 4 mốc + cột Tiền phạt |
| Tổng công | Tổng ngày công tháng | Reuse ExportTotalWorkingTimeSheetByDay.tsx (hiện đã có đủ 5 cột + logic calculateWorkDays()) | Chỉ thêm filter unit + cột đơn vị |
| Báo cáo trễ/sớm | Theo dõi vi phạm | Reuse logic từ getTimeLateArrival() + getTimeEarlyLeave() trong ExportByDay | Tạo component mới, reuse core logic |
| Báo cáo OT | Theo dõi OT | Reuse logic từ getOverTime() trong ExportByDay | Tạo component mới, reuse core logic |
| Báo cáo quên chấm | Theo dõi thiếu mốc | Build mới | Count missing per type per tháng |
| Báo cáo phép năm | Phép năm PN | Reuse AnnualLeaveReport.tsx + AnnualLeaveReportFilter.tsx + AnnualLeaveReportTable.tsx (hiện đã có filter năm/phòng ban/chi nhánh + export Excel + DB view report_annual_leave) | Chỉ thêm filter unit |
Column spec — Export bảng công tháng (tổng hợp)
| Cột | Nội dung |
|---|---|
| Mã NV | user_id |
| Họ tên | display_name |
| Đơn vị chấm công | timekeeping_unit.name |
| Chi nhánh | branch.name (primary branch trong unit) |
| Phòng ban | department.name |
| Nhóm ca | shift_group.name |
| Tổng ngày công | sum(workday) trong tháng |
| Tổng ngày nghỉ (off) | Số ngày off = true |
| Tổng ngày nghỉ phép | Số ngày có request leave approved |
| Tổng lần đi trễ | Count ngày có late_arrival = true |
| Tổng phút đi trễ | sum(late_minutes) |
| Tổng lần về sớm | Count ngày có leave_early = true |
| Tổng phút về sớm | sum(early_minutes) |
| Tổng lần quên chấm | missing_start_count + missing_end_count + missing_break_count |
| Tổng giờ OT | sum(ot_hours) |
| Số đơn đi trễ/về sớm đã dùng | Count request late_arrival_early_leave approved trong tháng |
| Số đơn quên chấm đã dùng | Count request forget_clock_in_out approved trong tháng |
| Công chuẩn | standard_workday (tự tính từ timekeeping_standard_workday_rule theo standard_workday_scope_key) |
| Tiền phạt | Tự tính từ timekeeping_penalty_rule per unit (DEC-012 v3). Đơn vị: VNĐ |
| Tiền OT | ot_hours × ot_rate (BS dùng ot_rate_doctor, còn lại dùng ot_rate_default). Đơn vị: VNĐ |
Column spec — Export báo cáo trễ/sớm
| Cột | Nội dung |
|---|---|
| Mã NV | user_id |
| Họ tên | display_name |
| Chi nhánh | branch.name |
| Phòng ban | department.name |
| Ngày | date |
| Ca | shift_template.name |
| Giờ quy định vào | start_working_time |
| Giờ thực tế vào | clock_in |
| Phút đi trễ | late_minutes |
| Giờ quy định ra | end_working_time |
| Giờ thực tế ra | clock_out |
| Phút về sớm | early_minutes |
| Có đơn xin phép | Có / Không (request late_arrival_early_leave approved) |
| Tổng lần đi trễ/về sớm tháng | Running count trong tháng (để HR biết NV đã dùng mấy lần) |
| Tiền phạt | Tự tính per violation từ timekeeping_penalty_rule (VD: 15p × 10.000đ = 150.000đ) |
Column spec — Export chi tiết ngày
Ca 2 mốc (PN, Daisy VP):
| Cột | Nội dung |
|---|---|
| Mã NV | user_id |
| Họ tên | display_name |
| Đơn vị chấm công | timekeeping_unit.name |
| Ngày | created_at |
| Ca | shift_template.name |
| Giờ vào | clock_in |
| Giờ ra | clock_out |
| Phút đi trễ | late_minutes |
| Phút về sớm | early_minutes |
| Ngày công | workday |
| OT (giờ) | ot_hours (từ request OT approved; PN bỏ qua nếu <30 phút) |
| Trạng thái | complete / missing_start / missing_end / off |
| Tiền phạt | Tự tính per day từ timekeeping_penalty_rule |
| Tiền OT | ot_hours × ot_rate per day |
| Ghi chú | Request overlay nếu có (đơn quên chấm, đổi ca, nghỉ phép...) |
Ca 4 mốc (Daisy DV, PN ca gãy):
| Cột | Nội dung |
|---|---|
| Mã NV | user_id |
| Họ tên | display_name |
| Đơn vị chấm công | timekeeping_unit.name |
| Ngày | created_at |
| Ca | shift_template.name |
| Giờ vào ca | segment_0.clock_in |
| Giờ ra nghỉ | segment_0.clock_out |
| Giờ vào lại | segment_1.clock_in |
| Giờ ra về | segment_1.clock_out |
| Phút trễ đầu ca | seg0_late_minutes |
| Phút sớm cuối ca | seg1_early_minutes |
| Phút trễ vào lại | seg1_late_minutes |
| Phút sớm ra nghỉ | seg0_early_minutes |
| Ngày công | workday |
| OT (giờ) | ot_hours |
| Trạng thái | complete / missing_start / missing_break / missing_end / partial |
| Tiền phạt | Tự tính per day từ timekeeping_penalty_rule |
| Tiền OT | ot_hours × ot_rate per day |
| Ghi chú | Request overlay |
Rules
| Rule ID | Rule |
|---|---|
| EXP-001 | Không có payroll bridge, không có action payroll |
| EXP-002 | Daisy export chi tiết ngày hiện 4 mốc nếu ca break_clocking_required = true |
| EXP-003 | Daisy ca VP (2 mốc) export giống format PN |
| EXP-004 | PN export OT bỏ qua request OT < ot_min_threshold_minutes (PN = 30 phút) |
| EXP-005 | Export tính tiền phạt + tiền OT tự động từ config per unit (DEC-012 v3, DEC-026) |
| EXP-006 | Tiền phạt: đọc timekeeping_penalty_rule, áp dụng exempt_count + exempt_pool (PN = individual, Daisy = shared) |
| EXP-007 | Tiền OT: NV Bác sĩ dùng ot_rate_doctor, còn lại dùng ot_rate_default |
| EXP-008 | Báo cáo phép năm: Day-1 cho PN (DEC-023). Daisy chưa mở entry báo cáo phép năm trong rollout này |
Interaction Behavior Contract
| Tình huống | Behavior mong muốn |
|---|---|
Entry from SCR-02 | Modal/screen export nhận filter hiện tại theo dạng readonly summary; user không đổi Đơn vị chấm công ở đây |
| File type switch | Đổi loại báo cáo refresh preview cột tương ứng, không mất scope |
| Long-running export | Nếu quá ngưỡng async, tạo background job và cho phép đóng modal mà không hủy job |
| Re-trigger export | Nếu cùng một file type + scope đang chạy job, hiện trạng thái Đang tạo file, không tạo job trùng |
Action Feedback Contract
| Action / Situation | Feedback UI | Hành động tiếp theo |
|---|---|---|
| Tạo export thành công (sync) | Toast xanh + link tải file | User ở lại hoặc đóng modal |
| Tạo export async | Banner/job card Đang tạo file | Khi xong gửi notification/in-app status |
| Không đủ dữ liệu | Inline warning nêu scope hiện tại không có bản ghi | User quay lại SCR-02 để đổi filter |
| Lỗi export | Toast đỏ + nút Thử lại | Giữ nguyên loại file và scope |
B2) SCR-07: Danh sách đơn từ chấm công (Request List)
Delta
- Reuse màn danh sách đơn từ hiện có
- Thêm filter
Đơn vị chấm công(auto-select theo user) - Approver chỉ thấy đơn trong
timekeeping_unitmình, KHÔNG cross-unit - Diva user (không có unit) → giữ nguyên, không thấy filter unit
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Duyệt hoặc đối soát đơn đúng trong scope unit mình phụ trách |
| Primary CTA | Duyệt / Từ chối trên request row/detail |
| Secondary CTA | Filter theo loại đơn, trạng thái, Đơn vị chấm công |
| Khối thông tin phải nổi bật | Đơn vị chấm công, loại đơn, trạng thái duyệt, người gửi |
Rules
| Rule ID | Rule |
|---|---|
| REQ-001 | Approver chỉ nhận và thấy đơn từ NV cùng timekeeping_unit |
| REQ-002 | Danh sách đơn filter theo timekeeping_unit_id của approver |
| REQ-003 | Diva approver không bị ảnh hưởng — query legacy |
B2) SCR-08: Hồ sơ nhân viên (Employee Profile) — badge đơn vị chấm công
Delta
- Thêm field readonly "Đơn vị chấm công" trên profile NV (hiện tên unit, VD: "Phương Nam")
- Nếu NV không thuộc unit → không hiện field
- HR mở profile NV PN → thấy badge "Phương Nam" + chi nhánh chính
Mục tiêu màn hình & CTA hierarchy
| Concern | Chốt trong spec |
|---|---|
| Người dùng vào màn để làm gì? | Xác nhận nhanh NV đang thuộc đơn vị chấm công nào khi tra hồ sơ |
| Primary CTA | Không có CTA ghi dữ liệu trên badge này |
| Secondary CTA | Với role phù hợp có thể deeplink sang SCR-00 > Nhân viên áp dụng để chỉnh assignment |
| Khối thông tin phải nổi bật | Badge Đơn vị chấm công + chi nhánh chính |
B4) Hợp đồng thông báo (Notification contract) — reuse event trigger hiện có
Reuse: Event trigger
request_working_schedule_insertđã có sẵn — resolve reviewers viaGetReviewersNextstepWithConfig()→ gửi push notification. Chỉ cần verify routing đúng per-unit approver (approver Daisy không nhận đơn PN).
| Event | Người nhận | Filter unit |
|---|---|---|
| NV tạo đơn mới | Approver cùng timekeeping_unit + cùng department/branch | Có — chỉ approver cùng unit |
| Approver duyệt/từ chối | NV tạo đơn | Không cần filter — gửi cho owner |
| NV chấm trễ >60 phút | Manager cùng unit + department | Có — chỉ manager cùng unit |
| Cronjob phát hiện quên chấm | NV đó | Không cần filter |
Rule: Notification route theo timekeeping_unit. Approver Daisy KHÔNG nhận thông báo đơn PN. Diva notification giữ nguyên logic cũ.
B5) Ma trận quyền (Permission Matrix)
| Surface | Staff | Manager | HR/Admin timekeeping | System Admin |
|---|---|---|---|---|
| SCR-00 | ❌ | ❌ | Xem theo scope | ✅ |
| SCR-00C | ❌ | ❌ | ✅ theo scope | ✅ |
| SCR-00D | ❌ | ❌ | ✅ theo scope | ✅ |
| SCR-01 | ❌ | Theo scope | ✅ | ✅ |
| SCR-02 | ❌ | Theo scope | ✅ | ✅ |
| SCR-05 | ✅ self | ❌ | ❌ | ❌ |
| SCR-06 | ❌ | Theo scope nếu được cấp | ✅ theo scope | ✅ |
B5-1) Cơ chế "theo scope" — Auto-filter & Default Unit
Cách xác định scope của user đang đăng nhập:
- Truy vấn
timekeeping_unit_userWHEREuser_id = current_userANDdisabled = falseANDeffective_from <= todayAND (effective_to IS NULLOReffective_to >= today) - Nếu user thuộc đúng 1 unit →
default_timekeeping_unit_id= unit đó - Nếu user không thuộc unit nào (Diva native) → filter
Đơn vị chấm côngkhông xuất hiện, mọi màn timekeeping hiển thị behavior legacy hiện tại - System Admin (BOD, ITLeader, ITStaff) → filter
Đơn vị chấm cônghiện dropdown tất cả unit active, không auto-select
Behavior khi mở trang admin (SCR-01, SCR-02, SCR-06):
| Loại user | Filter Đơn vị chấm công | Mặc định |
|---|---|---|
| HR Daisy (thuộc unit Daisy) | Hiện, auto-select Daisy, không đổi được sang PN | Chỉ thấy data Daisy |
| HR PN (thuộc unit PN) | Hiện, auto-select PN, không đổi được sang Daisy | Chỉ thấy data PN |
| Manager Daisy | Hiện, auto-select Daisy, chỉ thấy department/branch mình quản lý | Scope Daisy + branch mình |
| System Admin | Hiện dropdown tất cả unit, bắt buộc chọn trước khi xem data | Chưa chọn → empty state |
| Diva native (không có unit) | Không hiện filter unit | Working Sheet/Schedule hiện data Diva như cũ |
Rule:
| Rule ID | Rule |
|---|---|
| PERM-001 | User chỉ thấy data của timekeeping_unit mình thuộc; không cross-unit |
| PERM-002 | System Admin phải chọn unit trước khi thao tác; không có view "all units merged" |
| PERM-003 | Diva native user không bị ảnh hưởng bởi feature mới — mọi màn timekeeping giữ nguyên |
| PERM-004 | HR Daisy không thể đổi filter sang PN hoặc Diva (và ngược lại) |
| PERM-005 | Manager chỉ thấy department/branch mình quản lý TRONG scope unit mình |
B6) Từ điển Copy Text (Copy Text Dictionary)
| Key | Tiếng Việt | Ngữ cảnh |
|---|---|---|
timekeeping_unit.open_missing_step | Mở bước còn thiếu | CTA trên readiness checklist của SCR-00 |
timekeeping_unit.unsaved_changes | Bạn có thay đổi chưa lưu ở tab này | Dirty state khi rời tab trong SCR-00 |
timekeeping_config.apply_tomorrow | Áp dụng từ ngày mai | CTA publish ở Tab Quy định |
timekeeping_shift.runtime_preview | Xem trước kết quả runtime | Tiêu đề panel preview trong SCR-00C |
working_schedule.empty_missing_prerequisite | Chưa thể tạo lịch vì đơn vị này còn thiếu cấu hình đầu vào | Empty state của SCR-01 |
working_sheet.no_data_in_scope | Chưa có dữ liệu chấm công trong phạm vi đang chọn | Empty state của SCR-02 |
attendance.gps_out_of_scope | Bạn đang ngoài phạm vi GPS hợp lệ của ca làm việc này | Error feedback của SCR-05 |
export.job_processing | Hệ thống đang tạo file, bạn có thể tiếp tục làm việc khác | Banner/toast async export ở SCR-06 |
B7) Responsive + Accessibility Rules
B7.1) Responsive Rules
| Surface | Desktop / Laptop | Tablet | Mobile |
|---|---|---|---|
SCR-00, SCR-00C, SCR-00D, SCR-01, SCR-02, SCR-06, SCR-07, SCR-08 | Layout đầy đủ; filter bar được phép wrap thành 2 dòng khi <1280px; bảng có horizontal scroll và sticky header | Ưu tiên tra cứu + thao tác nhẹ; popup full-width hơn; bảng vẫn cho scroll ngang | Admin web không phải target Day-1; nếu mở trên <768px thì ưu tiên helper Vui lòng dùng laptop/desktop thay vì cố nhồi full table |
SCR-03 popup | Popup giữa màn, max-width desktop | Popup rộng gần full-screen | Nếu mở từ mobile web admin thì dùng full-screen modal |
SCR-05 mobile app | Không áp dụng | Không áp dụng | Native mobile là target chính; CTA chấm công phải luôn nằm trong vùng dễ chạm bằng ngón tay cái |
B7.2) Mobile Interaction Contract
| Concern | Rule |
|---|---|
| Sticky CTA | SCR-05 giữ CTA chấm công ở vùng cuối màn hoặc sticky footer, không để user phải cuộn mới thấy CTA chính |
| Modal / drawer trên mobile | Các helper như xin quyền vị trí, lỗi GPS, confirm action dùng bottom sheet hoặc full-screen modal tùy độ dài nội dung |
| Collapse pattern | Ở admin web trên tablet, filter bar wrap xuống 2 dòng; bảng dài dùng horizontal scroll thay vì collapse mất cột quan trọng |
| Touch target | CTA chính và action phá huỷ trên mobile phải có vùng chạm tối thiểu 44x44px |
B7.3) Accessibility / Keyboard Contract
| Concern | Rule |
|---|---|
| Focus order | Popup/admin form focus theo thứ tự trái sang phải, trên xuống dưới; field lỗi đầu tiên nhận focus sau submit fail |
| Focus return | Đóng SCR-03 hoặc popup cấu hình phải trả focus về đúng trigger trước đó |
| Focus trap | Modal / popup trong admin phải trap focus cho tới khi đóng |
| Keyboard | Enter submit form khi hợp lệ; Esc đóng popup nếu không có thao tác phá huỷ đang chờ confirm |
| Screen reader / label | Icon-only buttons như refresh, close, export phải có label text hoặc aria-label tương ứng |
| Không chỉ dựa vào màu | Badge ✅ / ⚠️ / ❌ và trạng thái trễ/sớm/thiếu mốc phải luôn có text, không chỉ dùng màu |
B8) Sự kiện phân tích (Analytics)
| Event | Ý nghĩa |
|---|---|
timekeeping_unit_changed | User đổi filter đơn vị chấm công |
timekeeping_setup_badge_clicked | User click badge readiness để mở bước còn thiếu trong SCR-00 |
timekeeping_config_published | Admin publish draft cấu hình unit |
timekeeping_shift_preview_changed | Admin đổi field trong SCR-00C làm preview runtime thay đổi |
timekeeping_mobile_cta_clicked | User bấm CTA chấm công |
timekeeping_mobile_punch_failed | User chấm công thất bại do GPS/quyền vị trí/network |
timekeeping_export_generated | User xuất báo cáo chấm công |
timekeeping_export_async_completed | Job export async hoàn tất |
B9) Từ điển Tooltip (Tooltip Dictionary)
| Màn hình | Field / Icon | Tooltip text | Điều kiện hiện |
|---|---|---|---|
SCR-00 | Badge Mở bước còn thiếu | Mở đúng màn hoặc tab còn thiếu để hoàn tất điều kiện bật áp dụng | Khi badge là ⚠️ hoặc ❌ |
SCR-00 | Trạng thái Chờ hiệu lực | Chỉ dùng cho gán nhân viên có effective_from trong tương lai | Hover badge ở Tab Nhân viên áp dụng |
SCR-00 | Action Đồng bộ theo cơ cấu hiện tại | So sánh nhân sự trong scope hiện tại với assignment đang active để tạo danh sách thêm / gỡ / chuyển đơn vị | Hover action bulk ở Tab Nhân viên áp dụng |
SCR-00C | Công tối đa của ca | Giá trị công tối đa mà ca này đóng góp trong ngày. Đây là nhãn hiển thị mới của field workday | Hover icon cạnh field |
SCR-00C | Giờ chuẩn của ca | Số giờ chuẩn dùng để quy đổi công khi Cách tính công = Theo giờ | Chỉ hiện khi mode = Theo giờ |
SCR-00C | Yêu cầu chấm 4 mốc | Bật để ca này có thêm Ra nghỉ và Vào lại trên mobile, bảng công và export chi tiết ngày | Hover icon cạnh toggle |
SCR-05 | Quota đơn X/3 lần | Số đơn đã dùng trong tháng hiện tại. Vượt quota vẫn gửi được nhưng có thể bị xử lý theo quy định | Khi mở form trễ/sớm hoặc quên chấm |
SCR-06 | Báo cáo phép năm | Day-1 chỉ mở cho Phương Nam; Daisy chưa có entry này trong rollout hiện tại | Hover vào option file type |
Kết luận
UI spec v3.1.5 không chỉ khóa flow SaaS-ready mà còn khóa thêm lớp product execution: mục tiêu từng màn, CTA hierarchy, interaction behavior, feedback sau action, copy text, responsive và accessibility. Với phạm vi Day-1, file này đã tiến gần hơn mức team FE/UI/QA có thể follow để build đúng intent mà không phải tự suy diễn các điểm hành vi quan trọng.