Appearance
Tài liệu yêu cầu sản phẩm (PRD) — Chấm công đa đơn vị trên shared tenant
v3.1.4 — 21/04/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
Chốt break_mode = flex Day-1 chỉ là metadata tham chiếu: không hard-validate/phạt theo khung nghỉ, chỉ yêu cầu đủ 4 mốc và tính actual_hours theo mốc thực tế | Z) Decision Log, FR-002, FR-014 | PO, FE, BE, QA |
Chốt missing_break chỉ là trạng thái cảnh báo để HR đối soát; Day-1 không auto suy ra workday = 0.5 chỉ vì thiếu mốc giữa ca | A10) Tổng quan công thức | FE, BE, QA |
Khóa lại scope remote Day-1: không mở CTA/request entry remote mới trong attendance shell, nhưng nhóm nhỏ vẫn tiếp tục dùng flow legacy đã được approved | Z) Decision Log, FR-003 | PO, FE, BE, QA, Ops |
Version: 3.1.4
Date: 21/04/2026
Author: PO/BA
Type: Enhancement
Complexity: M-L
Module: timekeeping
Changelog
| Version | Date | Author | Thay đổi |
|---|---|---|---|
| 3.1.4 | 21/04/2026 | PO/BA | Chốt canonical Day-1 cho break_mode = flex, missing_break và remote: không hard-validate break window linh hoạt, không auto trừ công vì thiếu mốc giữa ca, không mở remote entry mới nhưng vẫn giữ flow legacy cho nhóm nhỏ đã approved |
| 3.1.3 | 15/04/2026 | PO/BA | Chuyển Công chuẩn sang model nhóm áp dụng công chuẩn (standard_workday_scope_key) + map department -> scope, thay thế proposal cũ bám branch_label_id |
| 3.0 | 07/04/2026 | PO/BA | Initial release cho feature chấm công đa đơn vị trên shared tenant |
| 3.0.1 | 07/04/2026 | PO/BA | Đồng bộ canonical package về break_mode ở cấp ca, annual leave Day-1, remote nhóm nhỏ và wording QA/go-live |
| 3.1 | 13/04/2026 | PO/BA | Chuẩn hóa lại spec theo hướng triển khai được: assignment có primary_department_id, mapping branch/department dùng soft-disable, thêm tab Triển khai + readiness checklist, đồng bộ vocabulary vận hành và route/menu SCR-00 |
| 3.1.1 | 13/04/2026 | PO/BA | Làm rõ GPS contract: không build màn GPS master mới; tọa độ chi nhánh tiếp tục dùng branch master data hiện có, còn gps_radius_meters là override per-unit trong SCR-00 và có fallback global AppSettings.Position.Value.Distance |
| 3.1.2 | 14/04/2026 | PO/BA | Chuyển cách tính công xuống cấp Ca làm việc: reuse field workday hiện có làm công tối đa của ca, thêm workday_calculation_mode + standard_hours ở shift để hỗ trợ mixed-mode trong cùng một unit; đồng thời chuyển yêu cầu GPS của ca xuống cấp shift dưới dạng gps_required = Bắt buộc / Không bắt buộc |
TL;DR — Đọc 2 phút
Bài toán: Phương Nam (247 NV, 4 cơ sở) và Daisy (150 NV, 9 cơ sở) cần chấm công trên app Diva. Mỗi bên quy định riêng. Diva đang chạy ổn định — KHÔNG được ảnh hưởng.
Giải pháp: Thêm timekeeping_unit + Settings Module toàn diện. unit_id = NULL → legacy code nguyên vẹn.
Scope Day-1 (Phase 1B):
- Chấm công 2 mốc + 4 mốc (PN ca gãy, Daisy khối DV)
- GPS scoped per unit
- Settings Module: penalty engine + công chuẩn + OT rate + cấu hình ca tính theo ngày công/giờ
- Export bảng công có cột tiền phạt + tiền OT + công chuẩn → HR KHÔNG cần Excel
- Báo cáo phép năm PN (reuse
AnnualLeaveReport.tsx) - Remote nhóm nhỏ (Daisy Ca 3 Marketing + PN 3-4 NV)
KHÔNG đụng: Dashboard, report doanh thu, payroll, salary, org global.
Timeline: 1A Foundation (1 tuần) → 1B PN+Daisy (3 tuần dev + 2 tuần pilot) → GO-LIVE đầu tháng 6/2026.
Resource: 1 BE senior + 1 FE admin + 1 FE mobile + QA.
Đọc tiếp:
- PO/TL: Nhật ký quyết định (Decision Log - Z) + FRs (A5) + Kế hoạch phát hành (Release Plan)
- FE: FRs + Đặc tả giao diện (UI Spec) (
ui-spec.md) - BE: FRs + Công thức (A10) + Đặc tả kỹ thuật (Dev Spec) (
dev-spec.md) - QA: FRs + Kế hoạch kiểm thử (QA Test Plan) (
qa-test-plan.md)
Z) Nhật ký quyết định (Decision Log)
| ID | Nhóm (Category) | Quyết định | Lý do | Trạng thái (Status) |
|---|---|---|---|---|
| DEC-001 | Business | Daisy và Phương Nam dùng app Diva làm kênh chấm công chính | Không tiếp tục phụ thuộc máy chấm công trong rollout mới | Locked |
| DEC-002 | Scope | Thu feature về timekeeping-only scope, không retrofit company boundary cho toàn hệ thống | Giảm blast radius, giữ an toàn cho logic cũ | Locked |
| DEC-003 | Technical | Dùng timekeeping_unit thay cho business_unit | Chỉ phục vụ module chấm công và request chấm công | Locked |
| DEC-004 | Technical | timekeeping_unit không làm thay đổi branch, department, report, dashboard, salary hiện có | Tránh ảnh hưởng logic thống kê và payroll legacy | Locked |
| DEC-005 | Technical | Tách config approver timekeeping sang scope riêng, không sửa semantics các flow approval khác | Giảm rủi ro cross-module | Locked |
| DEC-006 | Technical | time_slot_time_keeping là source of truth cho HR timekeeping; runtime dùng time_slot_user_id + segment_index | Hỗ trợ cả ca 2 mốc và 4 mốc | Locked |
| DEC-007 | Rollout | Phase 1A: Foundation (schema + canonical_key + Settings Module). Phase 1B: PN + Daisy đồng thời. Phase 1C: Hardening | PN xác nhận 4 mốc → cùng rollout | Locked |
| DEC-008 | Scope | Báo cáo trong scope chỉ gồm báo cáo vận hành chấm công | Không mở payroll/report/dashboard legacy | Locked |
| DEC-009 | Business | GPS on-site là bắt buộc cho Day-1 của PN và Daisy | Khớp yêu cầu khảo sát và cơ chế kiểm soát đầu vào chấm công | Locked |
| DEC-010 | Scope | Remote hỗ trợ nhóm nhỏ Day-1: Daisy Ca 3 Marketing + PN 3-4 NV. Reuse logic remote_onetime hiện có. Báo cáo phép năm kéo vào Day-1 cho PN | PN xác nhận phép năm bắt buộc. Remote chỉ nhóm nhỏ dùng flow có sẵn | Updated |
| DEC-011 | Business | Rule nghỉ giữa ca gắn theo shift template: reuse break_time, start_break_time, end_break_time; bổ sung break_clocking_required, break_mode = fixed/flex, break_flex_minutes. Day-1 với break_mode = flex: không hard-validate mốc nghỉ theo khung giờ, chỉ yêu cầu đủ 4 mốc và tính actual_hours theo mốc thực tế | Daisy khối VP 2 mốc, khối DV 4 mốc. PN ca gãy 4 mốc, ca thường 2 mốc. Tránh auto hóa sai khi stakeholder còn mơ hồ về "linh hoạt x phút" | Updated v5 |
| DEC-012 | Scope | Day-1 build penalty engine trong Settings Module. Export tự tính tiền phạt từ config per unit. HR KHÔNG cần Excel. Bảng timekeeping_penalty_rule per unit, backend đọc DB | Settings Module toàn diện: penalty config per unit | Updated v3 |
| DEC-013 | Scope | Day-1 tính công chuẩn tự động từ bảng timekeeping_standard_workday_rule theo nhóm áp dụng công chuẩn (standard_workday_scope_key) và bảng map department -> scope. Không dùng branch_label_id làm business scope nữa | Khớp xác nhận HCNS: PN theo khối, Daisy có ngoại lệ trong cùng khối VP nên cần scope nghiệp vụ linh hoạt hơn branch | Updated v4 |
| DEC-014 | Business | PN ca gãy cần 4 mốc để xác định trễ đầu ca sáng VÀ đầu ca chiều | PN xác nhận: cần 4 mốc (vào ca, ra nghỉ trưa, vào lại chiều, ra về) | Confirmed |
| DEC-015 | Business | Daisy approval flow = 2 cấp (Trưởng BP = cấp 1, QLCN/HR = cấp 2) | Khảo sát Câu 18 xác nhận. Dùng 2-level approval hiện có | Locked |
| DEC-016 | Business | Remote cho nhóm nhỏ Day-1: Daisy Ca 3 Marketing + PN 3-4 NV. Reuse logic remote_onetime + remote_weekly hiện có trong log_time_keeping.go | Khảo sát Daisy Câu 12 + PN Câu 12 | Locked |
| DEC-017 | Business | Daisy có ca xoay sáng/chiều theo tuần — Day-1 HR tạo lịch thủ công/import Excel. Cron generate_working_shift skip auto_schedule_disabled | Auto-rotate schedule defer phase sau | Locked |
| DEC-018 | Business | Daisy Tạp vụ "giờ linh động" → Day-1 dùng ca theo giờ thực tế NV đăng ký, không hỗ trợ flexible_start tự động | HR tạo ca riêng phù hợp giờ thực tế NV | Locked |
| DEC-019 | Business | Daisy BS/Phụ tá break mặc định 12:00-14:00, break_flex_minutes = 60 chỉ lưu metadata tham chiếu cho Day-1. Ca "xuyên trưa" = 2 mốc. Với ca 4 mốc linh hoạt, hệ thống không block/phạt theo khung nghỉ; chỉ yêu cầu đủ 4 mốc, tính actual_hours theo mốc thực tế và export đủ 4 mốc cho HR đối soát | Stakeholder mô tả chưa nhất quán giữa "linh hoạt x phút" và "chỉ cần xuất 4 mốc để tự tính", nên Day-1 chọn hướng an toàn | Confirmed |
| DEC-020 | Business | Count đơn trễ/sớm + quên chấm scoped theo timekeeping_unit, không dùng global count | Đảm bảo Daisy 3/tháng, PN 3/tháng — không gộp cross-unit | Locked |
| DEC-021 | Business | Daisy đơn đi trễ/về sớm mỗi lần tối đa 60 phút. PN không giới hạn per lần | Daisy quy định rõ trong khảo sát | Locked |
| DEC-022 | Business | PN mặc định dùng ca tính công theo giờ thực tế. Có đơn approved → full công. Không có đơn → workday = actual_hours / standard_hours × max_workday. Daisy mặc định dùng ca fixed (0/0.5/1.0), nhưng spec không khóa cứng cả unit chỉ có 1 mode | PN xác nhận nghiệp vụ theo giờ; kiến trúc cần cho phép mixed-mode theo ca | Confirmed |
| DEC-023 | Scope | Báo cáo phép năm kéo vào Day-1 scope cho PN (bắt buộc go-live). Reuse AnnualLeaveReport.tsx + thêm filter unit | PN xác nhận bắt buộc | Confirmed |
| DEC-024 | Business | Daisy Tạp vụ: 6 ca cố định mới, 7 NV. CN Nha Trang có 2 ca 4h riêng | Daisy xác nhận: quy chuẩn giờ cố định cho Tạp vụ | Confirmed |
| DEC-025 | Business | Daisy "hạn chế update" = hạn chế deploy phần mềm quá nhiều lần/tháng. Gộp release | Daisy xác nhận: ý là deploy version mới | Confirmed |
| DEC-026 | Technical | Settings Module toàn diện: tất cả config per unit trên UI (công chuẩn, đơn từ, vi phạm & phạt, tăng ca, ca, GPS). Báo cáo phép năm PN reuse report hiện có, không thêm toggle config riêng trong SCR-00. Backend đọc DB, KHÔNG hardcode | Giảm deploy khi đổi config. HR tự quản lý. Tránh tạo thêm config thừa không có giá trị vận hành | Locked |
| DEC-027 | Technical | Bảng timekeeping_penalty_rule cho vi phạm & phạt per unit. exempt_pool = 'individual' (PN: mỗi loại đếm riêng) / 'shared' (Daisy: gộp cả 3 loại) | PN và Daisy quy chế phạt khác nhau hoàn toàn — cần bảng linh hoạt | Locked |
| DEC-028 | Technical | Department có branch_id → tạo dept per branch cho PN/Daisy. SCR-00 tab Phòng ban áp dụng auto-suggest từ branch đã map | Không cần "Kế toán Phương Nam" — tên giữ nguyên, phân biệt bằng branch | Locked |
| DEC-032 | Technical | GPS Day-1 không tạo màn master riêng: tọa độ branch tiếp tục quản lý ở màn Branch, còn bán kính/rule GPS nằm ở SCR-00 > Quy định; nếu unit chưa override thì fallback global AppSettings.Position.Value.Distance | Tận dụng code/màn hiện có, tách rõ nguồn dữ liệu tọa độ và bán kính | Locked |
| DEC-033 | Technical | Cách tính công không đặt ở timekeeping_unit. Day-1 chuyển xuống Ca làm việc để mỗi shift tự chọn Theo ngày công hoặc Theo giờ; field workday hiện có được reuse làm công tối đa của ca, thêm standard_hours ở shift khi dùng mode Theo giờ | Tránh khóa cứng 1 đơn vị = 1 cách tính công, khớp tốt hơn với màn Work Shift hiện có | Locked |
| DEC-034 | Technical | GPS override không đặt ở timekeeping_unit. Day-1 chuyển xuống Ca làm việc dưới dạng gps_required = true / false; timekeeping_unit chỉ giữ radius mặc định. Logic remote_onetime / remote_weekly vẫn là runtime/request flow riêng, không đưa thành option trong form ca | Giữ UI vận hành đơn giản Bắt buộc / Không bắt buộc, đồng thời không trộn flow remote với cấu hình ca | Locked |
A) Phạm vi nghiệp vụ (PRD)
A1) Tóm tắt giải pháp (Blueprint)
| Trường (Field) | Giá trị (Value) |
|---|---|
| Feature | Chấm công đa đơn vị chấm công trên shared tenant |
| Mục tiêu | Cho Daisy và Phương Nam chấm công được trên app Diva và có báo cáo vận hành |
| Không đổi | Dashboard, report doanh thu, report lương, payroll, org/report logic cũ của Diva |
| Platform | Admin Web, Staff Mobile, Hasura/Go backend |
| Scope module | timekeeping, phần timekeeping trong settings, app attendance, export chấm công |
A2) Bối cảnh (Context)
As-Is
- Diva đã có
working schedule,working sheet,attendance app,request timekeeping. - Runtime chấm công hiện thiên về mô hình
1 ngày = 1 pair clock_in/clock_out. - Daisy cần ca
4 mốc. - Phương Nam cần ca
2 mốc(ca thường) VÀ ca4 mốc(ca gãy). - Hệ thống hiện có nhiều report/dashboard/payroll đang bám
branch/department; đây là vùng không muốn tác động trong phase này. - Penalty (phạt trễ/sớm/quên chấm) hiện hardcode trong
log_time_keeping_flag.go:late_early_fine = phút x 10,000đ. Không có config per unit. - Công chuẩn hiện hardcode trong
user_salary.go:branchLabelID == "general"/"office" → countNonSundayDays(), else → 26.0. Không có bảng rule theo nhóm áp dụng nghiệp vụ.
To-Be
- Thêm
timekeeping_unitchỉ trong scope chấm công để phân tách:DivaDaisyPhuong Nam
- Daisy và Phương Nam dùng app Diva để chấm công.
- Admin/HR dùng:
Working ScheduleWorking SheetTimekeeping export
- Chấm công app của Daisy/PN ở Day-1 là
on-site attendancecó kiểm tra GPS. - Settings Module toàn diện (DEC-026): tất cả config per unit trên UI — công chuẩn, đơn từ, vi phạm & phạt, tăng ca, ca, GPS. Báo cáo phép năm PN reuse report hiện có, không thêm toggle riêng.
- GPS Day-1 tách thành 2 nguồn dữ liệu:
- tọa độ chi nhánh (
latitude/longitude) tiếp tục quản lý ở branch master data hiện có - bán kính/rule GPS (
gps_radius_meters) quản lý ởSCR-00 > Quy định
- tọa độ chi nhánh (
- Penalty engine Day-1 (DEC-012 v3):
timekeeping_penalty_ruleper unit, export tự tính tiền phạt. - Công chuẩn tự động Day-1 (DEC-013 v4):
timekeeping_standard_workday_ruletheostandard_workday_scope_key+ bảng mapdepartment -> scope. - Không mở payroll automation, không sửa dashboard/report legacy.
In Scope
- Timekeeping unit schema + Settings Module toàn diện (DEC-026)
- User assignment vào timekeeping unit
- Shift template theo timekeeping unit
- Approver config cho request chấm công
- App chấm công
2 mốccho PN (ca thường) + Daisy (VP, xuyên trưa) - App chấm công
4 mốccho PN (ca gãy) + Daisy (DV break) - Kiểm tra GPS theo branch đã map trong
timekeeping_unit - Working sheet, lịch sử chấm công, export vận hành
- Penalty engine Day-1 —
timekeeping_penalty_ruleper unit (DEC-012 v3, DEC-027) - Công chuẩn tự động Day-1 —
timekeeping_standard_workday_ruletheostandard_workday_scope_key(DEC-013 v4) - OT rate per role — cấu hình trong Settings Module (
ot_rate_default,ot_rate_doctor) - Tính công theo giờ PN — mặc định cấu hình ở cấp ca làm việc:
actual_hours / standard_hours × max_workday(DEC-022) - Báo cáo phép năm PN — reuse
AnnualLeaveReport.tsx+ filter unit (DEC-023) - Remote nhóm nhỏ Day-1 — Daisy Ca 3 Marketing + PN 3-4 NV (reuse
remote_onetimehiện có, DEC-016)
Out of Scope
- Dashboard các module khác
- Report doanh thu/marketing/customer/staff legacy
- Payroll, salary export, manual payroll bridge
- Global org refactor cho
branch,department,branch_user,department_user - Approval flows ngoài timekeeping
- Full remote attendance (beyond nhóm nhỏ Day-1 đã hỗ trợ qua
remote_onetime) - Auto-rotate schedule cho ca xoay (Day-1 HR import manual, DEC-017)
- Đơn xin nghỉ việc (thuộc HR module)
A3) Mục tiêu (Goals)
| Goal | Metric | Target |
|---|---|---|
| PN chấm công app 2 mốc + 4 mốc ổn định | Pilot pass | 100% P0 |
| Daisy chấm công app 4 mốc + 2 mốc ổn định | Pilot pass | 100% P0 |
| GPS scope đúng branch trong unit | GPS validation pass | 100% P0 |
| HR có báo cáo vận hành đủ dùng | Working sheet + export pass | 100% pilot |
| Settings Module hoạt động đúng per unit | Config apply đúng unit | 100% pilot |
| Penalty engine tính đúng theo rule per unit | Penalty calc match rule | 100% pilot |
| Không làm ảnh hưởng logic cũ của Diva | Non-regression timekeeping legacy pass | 0 P0 regression |
A4) Vai trò người dùng (Personas)
| Persona | Vai trò | JTBD |
|---|---|---|
| HR Phương Nam | Theo dõi công, export công, cấu hình penalty/công chuẩn | Chốt bảng công tháng cho PN, bao gồm tiền phạt + OT |
| HR Daisy | Theo dõi công 4 mốc, cấu hình penalty/công chuẩn | Chốt bảng công và xử lý thiếu mốc |
| Manager | Xem công nhân sự thuộc scope mình | Kiểm tra trễ/sớm/quên chấm |
| Staff | Chấm công trên app | Tự chấm và xem lịch sử |
| System Admin | Cấu hình timekeeping_unit, Settings Module | Gán user/ca/approver đúng scope, cấu hình toàn bộ rule per unit |
A5) Yêu cầu chức năng (Functional Requirements)
FR-001: Timekeeping Unit Scope
🆕 Build mới: Concept
timekeeping_unitchưa tồn tại trong codebase. BE 2-3d.
- [ ] Hệ thống cho phép tạo tối thiểu 3
timekeeping_unit:Diva,Daisy,Phuong Nam. - [ ]
timekeeping_unitchỉ áp dụng cho module chấm công và request chấm công. - [ ] Không thay đổi company boundary của
branch/departmentở toàn hệ thống. - [ ] Mỗi request/timekeeping record mới trong rollout đều gắn
timekeeping_unit_id.
FR-001A: Assignment và hiển thị scope chấm công
🆕 Build mới: SCR-00 tab
Nhân viên áp dụng— gán user vàotimekeeping_unit. Map branch/department. FE trong scope Settings Module.
- [ ] Có màn cấu hình gán user vào
timekeeping_unit. - [ ] Entry point của màn này là
Settings→Internal Settings→Đơn vị chấm công(SCR-00), không nằm trongUserCreate/EmployeeProfileCreate. - [ ] Có thể map branch và department hiện hữu vào
timekeeping_unitchỉ để dùng trong timekeeping. - [ ] Assignment user vào unit phải lưu đủ scope vận hành Day-1:
primary_branch_idprimary_department_ideffective_fromeffective_to
- [ ]
Chờ hiệu lựcchỉ áp dụng cho assignment user khieffective_from > today. - [ ] Mapping
Chi nhánh áp dụngvàPhòng ban áp dụngdùngsoft-disableđể giữ lịch sử vận hành, không hard delete. - [ ] Một user chỉ có
1 active timekeeping_unittại một thời điểm. - [ ] Admin xem được headcount theo
timekeeping_unittrong phạm vi chấm công. - [ ] Có
Readiness ChecklistởSCR-00để theo dõi tối thiểu:Chi nhánh áp dụngPhòng ban áp dụngNhân viên áp dụngNgười duyệtCa làm việcGPS/Quy địnhKiểm tra xuất báo cáo
FR-001B: Triển khai rollout theo đơn vị
🆕 Build mới: SCR-00 tab
Triển khai— không seed dữ liệu, chỉ dùng để bật cờ rollout sau khi dữ liệu đã sẵn sàng.
- [ ] Có tab
Triển khaitrongSCR-00để bật/tắt rollout admin/mobile theo từng unit. - [ ]
Bật rolloutchỉ khả dụng khi checklist readiness không còn❌. - [ ] Checklist readiness phải có điều kiện tính toán rõ ràng, không nhập tay.
- [ ] Lưu được
ghi chú triển khaivà snapshot checklist để QA/Ops đối chiếu trước go-live.
FR-002: Ca 4 mốc của Daisy và PN
🆕 Build mới: Multi-segment runtime (4 mốc) trong
logTimeKeeping. Hiện chỉ toggle clock_in/clock_out, cần state machine mới. BE 2-3d.
- [ ] Admin cấu hình được ca có/không có giờ nghỉ giữa ca bằng field sẵn có của shift template:
break_time,start_break_time,end_break_time. - [ ] Admin cấu hình được ca
2 mốchoặc4 mốcbằngbreak_clocking_requiredper shift template (DEC-011). - [ ] Nếu
break_clocking_required = true, admin chọnbreak_mode:fixed: giờ nghỉ trưa bám đúngstart_break_time/end_break_timeflex: giờ nghỉ trưa có khung mặc định để tham chiếu, nhưng Day-1 không hard-validate mốcRa nghỉ/Vào lạitheo khung này
- [ ] Nếu
break_clocking_required = false, hệ thống vẫn cho phép ca có nghỉ trưa cố định bằngbreak_time = true, nhưng mobile/admin chỉ render contract2 mốc. - [ ] Daisy khối Dịch vụ (BS, Phụ tá, Điều trị) dùng ca 4 mốc:
Vào caRa nghỉVào lạiRa về
- [ ] Daisy khối Văn phòng (Telesale, CSKH, Marketing, TCHC, Kế toán) có thể dùng ca 2 mốc (
break_clocking_required = false). - [ ] PN ca gãy (break 2-3h) cũng dùng 4 mốc (
break_clocking_required = true) — PN xác nhận cần track trễ đầu ca sáng + đầu ca chiều (DEC-014 confirmed).
FR-003: Runtime multi-clock
🆕 Build mới: Multi-segment cronjob trong
log_time_keeping_flag. Hiện xử lý 1 record/user/day, cần xử lý 2 records/user/day. BE 2-3d.
- [ ] Runtime map event về
time_slot_user_id + segment_index. - [ ] Ca 2 mốc vẫn chạy đúng như semantics chấm công hiện tại.
- [ ] Ca 4 mốc aggregate đúng trạng thái thiếu đầu ca/giữa trưa/cuối ca.
- [ ] Event chấm công Day-1 chỉ hợp lệ khi GPS nằm trong branch đã map vào
timekeeping_unitcủa user. - [ ] Không bind nhầm sang branch cùng tọa độ nhưng khác
timekeeping_unit. - [ ] Tọa độ để tính GPS lấy từ branch master data hiện có, không nhập lại trong
SCR-00. - [ ] Bán kính GPS Day-1 ưu tiên
gps_radius_meterscủa unit; nếu chưa override thì fallback globalAppSettings.Position.Value.Distance. - [ ] Runtime resolve yêu cầu GPS từ shift đang được gán trong ngày:
gps_required = true→ check GPS bình thường theo radius của unitgps_required = false→ không chặn punch bởi GPS cho ca đó
- [ ] Logic
remote_onetime/remote_weeklyapproved tiếp tục là runtime flow riêng Day-1, không render thành option kỹ thuật trong formCa làm việc. - [ ] Runtime resolve cách tính
workdaytừ shift đang được gán trong ngày:fixed→ áp dụng logic 0 / 0.5 / 1.0 theomax_workdaycủa cahourly→ áp dụngactual_hours / standard_hours × max_workday
FR-004: Admin working schedule / working sheet / history
🔧 Extend:
WorkingTimeSheet.tsx,WorkingSchedule.tsx— đã có filter DepartmentSelect, ShiftGroupSelect, DateRange, search. Delta: thêm filtertimekeeping_unit, filter chi nhánh, multi-record day (4 mốc).
- [ ]
Working Schedulelọc được theotimekeeping_unit. - [ ]
Working Sheethiển thị được nhiều event trong một ngày. - [ ] Employee history hiển thị đúng theo summary ngày.
FR-005: Mobile self-service
🆕 Build mới: Mobile 4 mốc state machine. Hiện
attendance_bloc.dartchỉ CheckIn/CheckOut, cần thêm BreakOut/BreakIn. FE Mobile 2-3d.
- [ ] Release 1B: PN dùng app cho ca 2 mốc (ca thường) VÀ ca 4 mốc (ca gãy).
- [ ] Release 1B: Daisy dùng app cho ca 2 mốc (VP, xuyên trưa) VÀ ca 4 mốc (DV break).
- [ ] Mobile hiển thị CTA đúng theo trạng thái từng ngày.
- [ ] Mobile Day-1 chỉ mở
on-site attendance. - [ ] Nếu GPS không hợp lệ thì chặn punch và hiển thị lý do rõ ràng.
FR-006: Request / Approver cho timekeeping
🔧 Extend:
- Notification: event trigger
request_working_schedule_insertđã có → resolve reviewers → gửi push. Cần extend resolver thêm dimension unit (BLOCKER).- Count requests: 2 PostgreSQL functions
count_remaining_late_early_requests()+count_remaining_forget_clock_in_out_requests()đã có. ALTER thêm WHEREtimekeeping_unit_id+ đọc max từtimekeeping_unitthay vì global.- Remote bypass:
log_time_keeping.gođã có logicremote_onetime+remote_weeklybypass GPS. Daisy Ca 3 Marketing dùng đơnremote_onetime→ logic bypass đã sẵn.
- [ ] Các request type trong scope timekeeping vẫn dùng được (reuse toàn bộ flow hiện có):
- quên chấm công
- đi trễ/về sớm
- đổi ca
- OT
- nghỉ phép nếu đang overlay bảng công
- [ ] Remote Day-1: reuse logic
remote_onetimehiện có cho Daisy Ca 3 Marketing + PN 3-4 NV. KHÔNG build feature remote mới (DEC-016). - [ ] Approver config trong scope timekeeping không lẫn giữa các
timekeeping_unit. - [ ] Không làm thay đổi approval flow ngoài timekeeping.
FR-007: Báo cáo vận hành chấm công
🔧 Extend: Export chi tiết ngày (
ExportWorkingTimeSheetByDay.tsx— đã có 14 cột + logic trễ/sớm/OT), export tổng công (ExportTotalWorkingTimeSheetByDay.tsx— đã có 5 cột), phép năm (AnnualLeaveReport.tsx— đã hoàn chỉnh). Chỉ cần thêm filter unit + extend 4 mốc.🆕 Build mới: Export bảng công tháng (17 cột) — hiện chỉ 3 cột + grid, cần rewrite column mapping. FE 2d.
- [ ] Export bảng công tháng — reuse
ExportWorkingTimeSheet.tsx, override column mapping thêm 17 cột. - [ ] Export chi tiết ngày — reuse
ExportWorkingTimeSheetByDay.tsx(14 cột +getTimeLateArrival()+getTimeEarlyLeave()đã có), thêm filter unit + 4 cột ca 4 mốc. - [ ] Export tổng công — reuse
ExportTotalWorkingTimeSheetByDay.tsx(5 cột +calculateWorkDays()đã có), chỉ thêm filter unit. - [ ] Báo cáo trễ/sớm — reuse logic từ ExportByDay, tạo component mới.
- [ ] Báo cáo OT — reuse logic
getOverTime()từ ExportByDay, tạo component mới. - [ ] Báo cáo quên chấm — build mới (count missing per type).
- [ ] Daisy export được chi tiết 4 mốc (extend ExportByDay thêm 4 cột).
- [ ] Export tính tiền phạt từ
timekeeping_penalty_ruleconfig per unit (DEC-012 v3). - [ ] Export tính tiền OT từ Settings Module config:
ot_rate_default,ot_rate_doctor(DEC-026). - [ ] Báo cáo phép năm PN — reuse
AnnualLeaveReport.tsxhoàn chỉnh, chỉ thêm filter unit (DEC-023).
FR-008: Guardrails rollout nhỏ và an toàn
✅ Reuse: pattern feature flag hiện có.
- [ ] Không sửa dashboard/report legacy.
- [ ] Không sửa salary/payroll logic hiện tại.
- [ ] Không thay đổi branch/department behavior ngoài timekeeping.
- [ ] Rollout có feature flag theo
timekeeping_unit.
FR-009: Seed shift templates cho PN và Daisy
🆕 Build mới: seed data +
canonical_keymigration. BLOCKER — fixtime_slot_template_update.go(match bằngname) trước khi seed.
- [ ] PN: seed ít nhất 16 ca (Ca HC, Ca 1-8, Ca tối, 6 ca gãy) với
break_timeconfig đúng. - [ ] Daisy: seed ít nhất 10 ca (Marketing 3 ca, BS 2 ca, Phụ tá 2 ca, Bảo vệ, Tạp vụ multi-ca, Kế toán, Labo 5D).
- [ ] Mỗi ca có
canonical_keyunique trongtimekeeping_unit. - [ ] Ca gãy PN (break 2-3h) phải có
break_time = true,start_break_time/end_break_timeđúng. - [ ] Cronjob
log_time_keeping_flagtính đúng late/early cho ca gãy: ra nghỉ trưa và vào lại sau break không tính là về sớm/đi trễ.
FR-010: Daisy ca xoay sáng/chiều theo tuần
🔧 Extend:
generate_working_shift.go— cron hiện copy tuần trước. Delta: thêm flagauto_schedule_disabledskip per unit.
- [ ] Day-1 workaround: HR Daisy tạo lịch hàng tuần bằng import Excel hoặc thủ công (DEC-017).
- [ ] Cron
generate_working_shiftskip user thuộctimekeeping_unitcó flagauto_schedule_disabled = true. - [ ] Deferred: auto-rotate schedule engine cho ca xoay.
FR-011: Phân quyền HR xem đúng đơn vị mình
🆕 Build mới: permission scoping per
timekeeping_unit.
- [ ] HR thuộc unit nào thì admin auto-select unit đó, không đổi được sang unit khác (PERM-004).
- [ ] Manager chỉ thấy department/branch mình quản lý trong scope unit mình (PERM-005).
- [ ] System Admin chọn được tất cả unit nhưng phải chọn trước khi xem data (PERM-002).
- [ ] Diva native user (không thuộc unit) không thấy filter
Timekeeping Unit, mọi màn timekeeping giữ nguyên legacy (PERM-003).
FR-012: Đơn đi trễ/về sớm — rule per unit
🔧 Extend: 2 PostgreSQL functions
count_remaining_late_early_requests()đã có. Delta: ALTER thêm WHEREtimekeeping_unit_id.
- [ ] Count đơn đi trễ/về sớm scoped per
timekeeping_unit(DEC-020). - [ ] Daisy: mỗi lần xin phép tối đa 60 phút; validation reject nếu duration > 60 phút (DEC-021).
- [ ] PN: không giới hạn duration per lần.
- [ ] Mobile hiển thị "Đã sử dụng X/3 lần trong tháng" khi NV tạo đơn.
- [ ] Vẫn cho phép tạo đơn khi > 3 lần — HR quyết định approve/reject.
FR-013: Đơn quên chấm công — count scoped per unit
🔧 Extend: PostgreSQL function
count_remaining_forget_clock_in_out_requests()đã có. Delta: ALTER thêm WHEREtimekeeping_unit_id.
- [ ] Count đơn quên chấm scoped per
timekeeping_unit(DEC-020). - [ ] Mobile hiển thị "Đã sử dụng X/3 lần trong tháng" khi NV tạo đơn quên chấm.
FR-014: Daisy ca linh hoạt (Tạp vụ + BS/Phụ tá break)
🆕 Build mới: 6 ca cố định mới cho Tạp vụ (seed data). Day-1 với BS/Phụ tá break linh hoạt chỉ ghi nhận đủ 4 mốc + tính actual hours theo mốc thực tế.
- [ ] Tạp vụ: HR tạo ca riêng phù hợp giờ thực tế NV (DEC-018). Không có flexible_start tự động.
- [ ] BS/Phụ tá break "linh động 2h": shift template giữ break window mặc định 12:00-14:00 bằng
start_break_time/end_break_time. - [ ] Với ca này,
break_clocking_required = true,break_mode = flex,break_flex_minutes = 60. - [ ] Day-1 với
break_mode = flex: hệ thống không phạt/block vì mốc 2 và 3 lệch khung break mặc định. - [ ] Hệ thống chỉ:
- yêu cầu đủ 4 mốc
- tính
actual_hours = (mốc 2 - mốc 1) + (mốc 4 - mốc 3) - export đủ 4 mốc để HR tự đối soát/tính nếu cần
- [ ]
break_flex_minuteschỉ lưu metadata tham chiếu cho HR/phase sau; không thay thếstart_break_time/end_break_time.
FR-015: PN — Tính công theo giờ thực tế (DEC-022)
🆕 Build mới: Logic
actual_hours / standard_hourschưa có. BE 1-2d.
- [ ] Nếu NV có đơn trễ/sớm approved →
workday = max_workdaycủa ca (full công). - [ ] Nếu NV KHÔNG có đơn →
workday = actual_hours / standard_hours × max_workday. - [ ]
standard_hoursconfig per ca làm việc:- PN Khối DV thường = 8h,
max_workday = 1.0 - PN Khối VP thường = 7.5h,
max_workday = 1.0 - part-time có thể dùng
standard_hours = 4h,max_workday = 0.5
- PN Khối DV thường = 8h,
- [ ] Áp dụng cho tất cả NV PN (full-time + part-time), không chỉ CTV.
- [ ] Daisy KHÔNG dùng formula này — Daisy vẫn tính 0/0.5/1.0 theo logic cũ.
- [ ] Diva KHÔNG bị ảnh hưởng.
FR-016: Báo cáo phép năm — Day-1 cho PN (DEC-023)
✅ Reuse:
AnnualLeaveReport.tsx— hoàn chỉnh: filter năm/phòng ban/chi nhánh, export Excel, DB viewreport_annual_leave. Delta: chỉ thêm filtertimekeeping_unit.
- [ ] Thêm filter
Timekeeping Unitvào mànAnnualLeaveReport.tsxhiện có (reuse pattern filter giống Working Sheet). - [ ] HR PN chọn unit PN → chỉ thấy NV PN. Diva user không thấy filter unit (giữ nguyên).
- [ ] Cron
add_annual_leave+reset_annual_leave: NV PN/Daisy cũng được cộng/reset phép — cron hiện query ALL employees nên tự bao gồm, chỉ cần verify không bị filter sai. - [ ]
annual_leave_logsthêmtimekeeping_unit_id(nullable) cho audit trail per unit. - [ ] Config
app_setting.request_leave(min_working_day, reset_date, expiry_date): Day-1 dùng global config chung 3 đơn vị. Nếu PN/Daisy cần config khác → phase sau thêm per-unit override vào Settings Module. - [ ] Daisy: chưa nằm trong scope báo cáo phép năm Day-1 — không mở entry/menu báo cáo phép năm trong rollout này.
FR-017: Mobile NV xem lịch sử chấm công cá nhân
🔧 Extend:
attendance_bloc.dart— đã có calendar history. Delta: thêm hiển thị 4 dòng cho ca 4 mốc, running total tháng.
- [ ] NV xem được lịch sử chấm công tháng hiện tại và tháng trước.
- [ ] Ca 4 mốc hiển thị đủ 4 dòng, ca 2 mốc hiển thị 2 dòng.
- [ ] Hiển thị running total tháng: ngày công, lần trễ, lần sớm, lần quên chấm.
- [ ] Overlay request nếu có (đơn đi trễ, quên chấm, nghỉ phép...).
FR-018: Settings Module toàn diện (DEC-026)
🆕 Build mới: Settings Module UI (SCR-00) + backend config tables. FE 4-5d, BE 2d.
- [ ] UI Settings cho mỗi
timekeeping_unitgồm các tab:- Công chuẩn: rule mapping theo
standard_workday_scope_key+ mapdepartment -> scope(DEC-013 v4) - Đơn từ: max requests per month, max duration per request
- Vi phạm & Phạt: penalty rules per violation type (DEC-012 v3, DEC-027)
- Tăng ca: OT rate default, OT rate doctor, OT min threshold
- Ca: shift templates thuộc unit
- GPS: branch mapping, radius mặc định của unit
- Công chuẩn: rule mapping theo
- [ ] Backend đọc config từ DB, KHÔNG hardcode.
- [ ] Thêm đơn vị mới chỉ cần tạo
timekeeping_unit+ config trên Settings Module, không cần deploy code.
FR-019: Penalty Engine Day-1 (DEC-012 v3, DEC-027)
🆕 Build mới:
timekeeping_penalty_ruletable + penalty calculation engine. BE 2d.
- [ ] Mỗi
timekeeping_unitcó bộ penalty rules riêng trên bảngtimekeeping_penalty_rule. - [ ] Hỗ trợ 3 mode phạt:
per_minute(phạt theo phút),fixed_amount(phạt cố định per lần),deduct_workday(trừ công). - [ ] Mỗi rule có
exempt_count(số lần miễn phạt) vàexempt_pool:individual(PN): mỗi loại vi phạm đếm riêngshared(Daisy): gộp tất cả loại vi phạm đếm chung
- [ ] Cronjob
log_time_keeping_flagáp dụng penalty rule từ DB thay vì hardcodephút x 10,000đ. - [ ] Export bảng công tự tính cột tiền phạt từ penalty rules.
FR-020: Công chuẩn tự động Day-1 (DEC-013 v3)
🆕 Build mới:
timekeeping_standard_workday_rule+timekeeping_standard_workday_scope_department. BE 1-1.5d. 🔧 Extend:user_salary.go— hiện dùngbranchLabelIDhardcode. Delta: đọc từprimary_department_id -> standard_workday_scope_key -> rule, kết quả Diva không đổi.
- [ ] Bảng
timekeeping_standard_workday_rulelưu rule theotimekeeping_unit_id+standard_workday_scope_key. - [ ] Bảng
timekeeping_standard_workday_scope_departmentmapdepartment_idvàostandard_workday_scope_keytrong từng unit. - [ ] Hỗ trợ các mode:
days_minus_sun(trừ CN),days_minus_sun_half_sat(trừ CN + nửa T7),fixed_26,fixed_custom. - [ ]
user_salary.gođọc từ bảng rule thay vì hardcode. Diva chuyển sang kết quả không đổi. - [ ] Mỗi unit có thể tạo nhiều nhóm áp dụng công chuẩn khác nhau. Ví dụ:
- PN:
PN_OFFICE,PN_SERVICE - Daisy:
DAISY_OFFICE_ACCOUNTING,DAISY_OFFICE_TELE_CSKH_PAGE_BRANCH,DAISY_SERVICE
- PN:
A6) Giả định (Assumptions)
| ID | Giả định (Assumption) | Người phụ trách (Owner) |
|---|---|---|
| ASM-001 | Daisy và PN chấp nhận dùng app Diva làm kênh chấm công chính | PO |
| ASM-002 | Báo cáo phase này chỉ phục vụ vận hành chấm công, không phải payroll | PO + HR |
| ASM-003 | branch và department hiện hữu tiếp tục được dùng làm master data nền, chỉ được map thêm vào timekeeping_unit | TL |
| ASM-004 | Day-1 chỉ cần on-site attendance cho majority; remote hỗ trợ nhóm nhỏ qua remote_onetime hiện có | PO |
| ASM-005 | Superseded by DEC-012 v3: Penalty engine Day-1, HR không cần Excel | — |
| ASM-006 | Superseded by DEC-013 v3: Công chuẩn tự động Day-1 | — |
| ASM-007 | Daisy Ca 3 Marketing Trực Page có thể dùng workaround remote_onetime request approved trước cho từng ngày (DEC-016) | PO + Daisy Ops |
| ASM-008 | Superseded by DEC-014 confirmed: PN ca gãy = 4 mốc | — |
A7) Rủi ro (Risks)
| ID | Risk | Impact | Mitigation |
|---|---|---|---|
| RSK-001 | Runtime mới làm sai chấm công hiện tại của Diva | Cao | Flag theo timekeeping_unit, non-regression suite cho Diva timekeeping |
| RSK-002 | Ca 4 mốc làm mobile/admin hiển thị sai | Cao | Test matrix kỹ cho cả 2 mốc + 4 mốc. Tách test scenario per shift type |
| RSK-003 | Approver timekeeping lẫn với flow khác | Trung bình | Dùng config approver timekeeping riêng, scope per unit |
| RSK-004 | User/branch/department map sai vào timekeeping_unit | Trung bình | Validation và seed data kiểm soát |
| RSK-005 | GPS sai hoặc map branch sai làm user không punch được | Trung bình | GPS test matrix + pilot smoke theo branch thật |
| RSK-006 | Cronjob log_time_keeping_flag tính sai late/early cho ca gãy PN (break >2h) | Trung bình | Unit test riêng cho ca gãy; verify break window không tính thành trễ/sớm |
| RSK-007 | Cron generate_working_shift copy sai cho Daisy ca xoay | Trung bình | Skip auto-generate cho unit có auto_schedule_disabled; Daisy HR import manual |
| RSK-008 | Settings Module phức tạp, nhiều tab/config — UX khó dùng hoặc bug config sai | Trung bình | UI review kỹ với HR trước pilot. Default values hợp lý cho mỗi unit. Validation chặt khi save config |
| RSK-009 | Penalty engine tính sai tiền phạt do config rule phức tạp (exempt_pool, exempt_count, multi-mode) | Trung bình | Unit test cho từng mode phạt + edge case exempt. Verify với bảng tính tay của HR trước go-live |
A8) Chỉ số thành công (Success Metrics)
| Metric | Target |
|---|---|
pn_app_attendance_success_rate | >= 99% pilot |
daisy_4_punch_success_rate | >= 99% pilot |
gps_scope_validation_pass_rate | 100% P0 |
timekeeping_export_success_rate | 100% pilot |
penalty_calc_accuracy | 100% match HR manual calc |
standard_workday_calc_accuracy | 100% match legacy result for Diva |
legacy_diva_timekeeping_p0_regression | 0 |
A9) Bảng thuật ngữ (Glossary)
| Thuật ngữ | EN | Định nghĩa | Phân biệt với |
|---|---|---|---|
| Đơn vị chấm công | Timekeeping Unit | Lớp scope chỉ dùng trong module chấm công | Khác công ty toàn hệ thống |
| Ca 2 mốc | 2-punch shift | Vào ca, Ra về | Khác ca 4 mốc |
| Ca 4 mốc | 4-punch shift | Vào ca, Ra nghỉ, Vào lại, Ra về | Khác nghỉ phép nửa ngày |
| Chấm công tại chỗ | On-site attendance | Chấm công bằng app khi GPS nằm trong branch hợp lệ | Khác remote attendance |
| Bảng công | Working Sheet | Bảng tổng hợp chấm công theo ngày/tháng | Khác payroll |
| Ca gãy | Split shift | Ca có break dài (>1h), NV rời cơ sở và quay lại | Khác ca 4 mốc (break ngắn) |
| Ca xoay | Rotating shift | Ca luân phiên sáng/chiều theo tuần | Khác ca cố định |
| Công chuẩn | Standard workday | Số ngày công chuẩn trong tháng dùng tính lương | Khác ngày công thực tế |
| Mốc chấm công | Punch event | Một lần bấm chấm công (vào/ra) | Segment = cặp vào+ra |
| Nghỉ giữa ca cố định | Fixed break | Nghỉ giữa ca bám đúng khung break của shift template | Khác nghỉ giữa ca linh hoạt |
| Nghỉ giữa ca linh hoạt | Flexible break | Nghỉ giữa ca có khung mặc định trong shift template, nhưng Day-1 không hard-validate mốc nghỉ theo khung này; hệ thống chỉ yêu cầu đủ 4 mốc và tính theo giờ thực tế | Khác nghỉ giữa ca cố định |
| Chờ hiệu lực | Pending assignment | Assignment user đã lưu nhưng chưa đến effective_from | Khác Đang áp dụng |
| Ngưng áp dụng | Disabled mapping | Mapping/unit vẫn còn lịch sử nhưng không còn dùng cho runtime mới | Khác xóa cứng dữ liệu |
| Checklist sẵn sàng triển khai | Readiness checklist | Tập điều kiện xác nhận unit đã đủ dữ liệu/config để bật rollout | Khác sign-off go-live |
| Penalty engine | Penalty engine | Hệ thống tự động tính tiền phạt theo rule per unit | Khác tính phạt thủ công bằng Excel |
| Settings Module | Settings Module | Màn hình cấu hình toàn diện per unit (tính công, phạt, OT, công chuẩn...) | Khác hardcode config trong code |
A10) Tổng quan công thức (Formula Overview)
FORMULA-001: Trạng thái ngày công
- Mô tả: Xác định ngày đó đã đủ mốc chấm công hay chưa.
- Công thức:
status = aggregate(events, shift_template) - Kết quả hợp lệ:
completemissing_startmissing_breakmissing_endpartial
FORMULA-002: Tổng phút vi phạm trong ngày
- Mô tả: Tổng phút đi trễ/về sớm theo các segment của ca.
- Công thức:
- Ca 2 mốc:
total_violation_minutes = late_minutes + early_minutes - Ca 4 mốc:
total_violation_minutes = segment_1_late + segment_1_early + segment_2_late + segment_2_early
- Ca 2 mốc:
- Biến số:
late_minutes= max(0, actual_clock_in - scheduled_start)early_minutes= max(0, scheduled_end - actual_clock_out)- Segment 1: vào ca -> ra nghỉ; Segment 2: vào lại -> ra về
- Ca gãy PN (4 mốc,
break_mode = fixed): Break window cố định trong template. Cronjob tính late cho clock_in segment 0 (đầu ca sáng) và segment 1 (đầu ca chiều), early cho clock_out segment 0 (ra nghỉ) và segment 1 (ra về).break_flex_minutes = 0. - Daisy BS/Phụ tá Ca 2 (4 mốc,
break_mode = flex): Day-1 không dùng break window mặc định12:00-14:00để phạt/block ở mốc 2 và 3. Hệ thống chỉ yêu cầu đủ 4 mốc, tínhactual_hourstheo 2 segment thực tế, và export đủ 4 mốc cho HR. - Edge cases:
- Clock_in/clock_out null -> segment đó = "missing", không tính minutes
- Ca 4 mốc thiếu mốc giữa trưa ->
missing_break, workday không trừ tự động (HR quyết định)
FORMULA-003: OT hours + tiền OT
- Mô tả: Tổng giờ tăng ca trong ngày, từ request OT approved. Tính tiền OT từ Settings Module config.
- Công thức giờ OT:
ot_hours = sum(request.to - request.from) / 60cho mỗi OT request approved trong ngày- PN: chỉ tính OT khi duration >=
ot_min_threshold_minutes(default 30 phút). OT < 30p -> bỏ qua - Daisy:
ot_min_threshold_minutes = 0(không có threshold)
- Công thức tiền OT:
ot_amount = ot_hours * ot_rateot_rateđọc từ Settings Module per unit:- PN: thường
ot_rate_default = 50,000đ/h, BSot_rate_doctor = 150,000đ/h - Daisy: thường
ot_rate_default = 35,000đ/h, BSot_rate_doctor = 150,000đ/h
- PN: thường
- Biến số:
ot_min_threshold_minutes: config per unit (PN = 30, Daisy = 0)ot_rate_default: rate mặc định per unitot_rate_doctor: rate cho BS per unit
- Edge cases:
- OT request span qua ngày -> chỉ tính phần thuộc ngày đó
ot_rate= 0 hoặc NULL -> export hiện giờ OT nhưng cột tiền = 0
FORMULA-004: Số lần quên chấm công trong tháng + tiền phạt
- Mô tả: Đếm số lần quên chấm (thiếu mốc) theo tháng, phân theo loại mốc. Tính tiền phạt từ
timekeeping_penalty_rule. - Biến số:
missing_start_count: số ngày thiếu mốc đầu camissing_end_count: số ngày thiếu mốc cuối camissing_break_count: số ngày thiếu mốc giữa trưa (chỉ ca 4 mốc)
- Công thức phạt: Xem FORMULA-006 (Penalty Engine).
- Edge cases:
- Có đơn quên chấm approved -> không tính vào missing count
FORMULA-005: Tính công theo giờ thực tế — PN (DEC-022)
- Mô tả: PN tính công dựa trên giờ làm thực tế, không chỉ 0/0.5/1.0.
- Áp dụng: Tất cả NV PN (full-time + part-time). KHÔNG áp dụng cho Daisy và Diva.
- Công thức:
- Nếu có đơn trễ/sớm approved trong ngày ->
workday = shift_template.workday(full công) - Nếu KHÔNG có đơn ->
workday = (actual_hours / standard_hours) × shift_template.workday
- Nếu có đơn trễ/sớm approved trong ngày ->
- Biến số:
actual_hours= (clock_out - clock_in - break_duration) tính bằng giờ. Ca 4 mốc: sum 2 segments.standard_hours: số giờ chuẩn của chính ca làm việc. Ví dụ PN ca DV = 8.0h, PN ca VP = 7.5h, part-time = 4.0h
- Ví dụ:
- NV Khối DV ca 08:00-17:00 (8h chuẩn), về sớm 15:00 (thực 7h), không có đơn ->
workday = 7/8 = 0.875 - NV Khối VP ca 08:00-17:00 (7.5h chuẩn), về sớm 15:00 (thực 5.5h), có đơn approved ->
workday = 1.0 - NV part-time ca 08:00-12:00 (
standard_hours = 4,max_workday = 0.5), đi đủ ->workday = (4/4) × 0.5 = 0.5
- NV Khối DV ca 08:00-17:00 (8h chuẩn), về sớm 15:00 (thực 7h), không có đơn ->
- Edge cases:
- actual_hours > standard_hours -> cap tại shift_template.workday (không vượt quá)
- actual_hours <= 0 -> workday = 0
- Round: 2 decimal (VD: 0.88)
FORMULA-006: Penalty Engine (DEC-012 v3, DEC-027)
- Mô tả: Tính tiền phạt tự động theo
timekeeping_penalty_ruleper unit. Thay thế hardcodephút x 10,000đhiện tại. - Bảng
timekeeping_penalty_rule: Mỗi row = 1 rule cho 1 loại vi phạm trong 1 unit. - 3 mode phạt:
per_minute:penalty_amount = violation_minutes * rate_per_minute. VD: PN trễ 15p, rate 10,000đ/p -> 150,000đfixed_amount:penalty_amount = fixed_amount_per_occurrence. VD: Daisy quên chấm giữa trưa -> 50,000đ/lầndeduct_workday:workday_deduction = deduction_value. VD: Daisy quên chấm đầu/cuối ca -> trừ 0.5 công
- Exempt (miễn phạt):
exempt_count: số lần miễn phạt per tháng (VD: Daisy = 3, PN quên vào/ra = 0, PN quên giữa trưa = 3)exempt_pool:individual(PN): mỗi loại vi phạm đếm riêng. VD: 3 lần miễn quên giữa trưa, KHÔNG chia sẻ với quên vào/rashared(Daisy): gộp tất cả loại vi phạm đếm chung. VD: 3 lần miễn cho tổng (trễ + sớm + quên chấm)
penalty_applied = max(0, violation_count - exempt_count) * penalty_per_occurrence
- Ví dụ Daisy (shared, exempt_count=3):
- Tháng có: 2 lần trễ + 1 lần quên chấm + 1 lần sớm = tổng 4 lần
- 3 lần miễn -> 1 lần tính phạt (lần thứ 4)
- Ví dụ PN (individual, quên giữa trưa exempt_count=3):
- 4 lần quên giữa trưa -> 3 miễn -> 1 lần phạt 30,000đ
- 2 lần quên vào ca -> exempt_count=0 -> 2 lần phạt x 30,000đ = 60,000đ
- Edge cases:
- Unit chưa có penalty rule -> penalty = 0, không phạt
rate_per_minute= 0 -> penalty = 0- Violation count = 0 -> không tính
FORMULA-007: Công chuẩn tự động (DEC-013 v4)
- Mô tả: Tính số ngày công chuẩn trong tháng từ
timekeeping_standard_workday_rule, thay thế hardcode tronguser_salary.go. - Bảng
timekeeping_standard_workday_rule: Day-1 mappingtimekeeping_unit_id+standard_workday_scope_key-> mode. - Bảng
timekeeping_standard_workday_scope_department: mapdepartment_idcủa user vào đúng nhóm áp dụng công chuẩn trong unit. - 4 mode:
days_minus_sun: tổng ngày trong tháng trừ CN. VD: tháng 4/2026 = 30 ngày, 4 CN -> 26 ngày. Tương đươngcountNonSundayDays()hiện tạidays_minus_sun_half_sat: trừ CN + nửa ngày T7. VD: 30 - 4 CN - (5 T7 x 0.5) = 23.5 ngàyfixed_26: luôn = 26.0 ngàyfixed_custom: giá trị custom do HR nhập. VD: 24.0
- Mapping mặc định (Day-1):
- PN
PN_SERVICE->days_minus_sun - PN
PN_OFFICE->days_minus_sun_half_sat - Daisy
DAISY_OFFICE_ACCOUNTING->fixed_custom = 24 - Daisy
DAISY_OFFICE_TELE_CSKH_PAGE_BRANCH->fixed_26 - Daisy
DAISY_SERVICE->fixed_26
- PN
- Edge cases:
- Unit/scope chưa có rule -> fallback
fixed_26 - Tháng 2 nhuận ->
days_minus_suntự tính đúng
- Unit/scope chưa có rule -> fallback
Kế hoạch phát hành (Release Plan)
| Release | Scope | Ghi chú |
|---|---|---|
| 1A — Foundation | timekeeping_unit schema + canonical_key migration + Settings Module + non-regression Diva | Không có feature mới cho end-user. Settings Module sẵn sàng cho 1B config |
| 1B — PN + Daisy đồng thời | Runtime 4 mốc + 2 mốc, GPS, admin working sheet/schedule/export, mobile 4 CTA + 2 CTA, approver scope, penalty engine, công chuẩn tự động, OT rate, tính công theo giờ PN, báo cáo phép năm PN, remote nhóm nhỏ | PN và Daisy cùng rollout vì cả 2 đều cần 4 mốc. Settings Module config per unit |
| 1C — Hardening | Bugfix, performance tuning, mở rộng rollout, export nâng cấp nếu cần | Phase tối ưu hoá và ổn định |
Phạm vi để lại giai đoạn sau (Deferred Scope)
| Item | Lý do defer | Đơn vị yêu cầu |
|---|---|---|
| Auto-rotate schedule cho ca xoay | Cron generate_working_shift không hỗ trợ ca xoay. Day-1 HR import manual (DEC-017) | Daisy |
| Đơn xin nghỉ việc | Thuộc HR module, không phải timekeeping | Daisy |
| Full remote attendance (beyond nhóm nhỏ Day-1) | Cần thiết kế feature remote riêng. Day-1 nhóm nhỏ dùng remote_onetime hiện có (DEC-016) | Cả hai |
Kết luận
Package v3.1.2 này thu feature về timekeeping-only scope với Settings Module toàn diện, nhưng chuyển cách tính công xuống đúng nơi phù hợp là Ca làm việc thay vì khóa cứng ở Đơn vị chấm công. Mục tiêu là để Daisy và Phương Nam chấm công được trên app Diva và có báo cáo vận hành, đồng thời mỗi ca vẫn có thể chọn Theo ngày công hoặc Theo giờ mà không làm logic cũ của Diva ngoài module chấm công bị kéo vào scope thay đổi.