Skip to content

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 đổiSectionẢ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-014PO, 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 caA10) Tổng quan công thứcFE, 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 approvedZ) Decision Log, FR-003PO, FE, BE, QA, Ops

Version: 3.1.4
Date: 21/04/2026
Author: PO/BA
Type: Enhancement
Complexity: M-L
Module: timekeeping

Changelog

VersionDateAuthorThay đổi
3.1.421/04/2026PO/BAChốt canonical Day-1 cho break_mode = flex, missing_breakremote: 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.315/04/2026PO/BAChuyể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.007/04/2026PO/BAInitial release cho feature chấm công đa đơn vị trên shared tenant
3.0.107/04/2026PO/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.113/04/2026PO/BAChuẩ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.113/04/2026PO/BALà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.214/04/2026PO/BAChuyể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)

IDNhóm (Category)Quyết địnhLý doTrạng thái (Status)
DEC-001BusinessDaisy và Phương Nam dùng app Diva làm kênh chấm công chínhKhông tiếp tục phụ thuộc máy chấm công trong rollout mớiLocked
DEC-002ScopeThu feature về timekeeping-only scope, không retrofit company boundary cho toàn hệ thốngGiảm blast radius, giữ an toàn cho logic cũLocked
DEC-003TechnicalDùng timekeeping_unit thay cho business_unitChỉ phục vụ module chấm công và request chấm côngLocked
DEC-004Technicaltimekeeping_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 legacyLocked
DEC-005TechnicalTách config approver timekeeping sang scope riêng, không sửa semantics các flow approval khácGiảm rủi ro cross-moduleLocked
DEC-006Technicaltime_slot_time_keeping là source of truth cho HR timekeeping; runtime dùng time_slot_user_id + segment_indexHỗ trợ cả ca 2 mốc và 4 mốcLocked
DEC-007RolloutPhase 1A: Foundation (schema + canonical_key + Settings Module). Phase 1B: PN + Daisy đồng thời. Phase 1C: HardeningPN xác nhận 4 mốc → cùng rolloutLocked
DEC-008ScopeBáo cáo trong scope chỉ gồm báo cáo vận hành chấm côngKhông mở payroll/report/dashboard legacyLocked
DEC-009BusinessGPS on-site là bắt buộc cho Day-1 của PN và DaisyKhớp yêu cầu khảo sát và cơ chế kiểm soát đầu vào chấm côngLocked
DEC-010ScopeRemote 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 PNPN xác nhận phép năm bắt buộc. Remote chỉ nhóm nhỏ dùng flow có sẵnUpdated
DEC-011BusinessRule 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-012ScopeDay-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 DBSettings Module toàn diện: penalty config per unitUpdated v3
DEC-013ScopeDay-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ữaKhớ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 branchUpdated v4
DEC-014BusinessPN ca gãy cần 4 mốc để xác định trễ đầu ca sáng VÀ đầu ca chiềuPN 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-015BusinessDaisy 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-016BusinessRemote 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.goKhảo sát Daisy Câu 12 + PN Câu 12Locked
DEC-017BusinessDaisy 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_disabledAuto-rotate schedule defer phase sauLocked
DEC-018BusinessDaisy 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ự độngHR tạo ca riêng phù hợp giờ thực tế NVLocked
DEC-019BusinessDaisy 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átStakeholder 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ànConfirmed
DEC-020BusinessCount đơ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-unitLocked
DEC-021BusinessDaisy đơ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ầnDaisy quy định rõ trong khảo sátLocked
DEC-022BusinessPN 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 modePN xác nhận nghiệp vụ theo giờ; kiến trúc cần cho phép mixed-mode theo caConfirmed
DEC-023ScopeBá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 unitPN xác nhận bắt buộcConfirmed
DEC-024BusinessDaisy Tạp vụ: 6 ca cố định mới, 7 NV. CN Nha Trang có 2 ca 4h riêngDaisy xác nhận: quy chuẩn giờ cố định cho Tạp vụConfirmed
DEC-025BusinessDaisy "hạn chế update" = hạn chế deploy phần mềm quá nhiều lần/tháng. Gộp releaseDaisy xác nhận: ý là deploy version mớiConfirmed
DEC-026TechnicalSettings 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 hardcodeGiả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ànhLocked
DEC-027TechnicalBả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ạtLocked
DEC-028TechnicalDepartment có branch_id → tạo dept per branch cho PN/Daisy. SCR-00 tab Phòng ban áp dụng auto-suggest từ branch đã mapKhông cần "Kế toán Phương Nam" — tên giữ nguyên, phân biệt bằng branchLocked
DEC-032TechnicalGPS 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.DistanceTận dụng code/màn hiện có, tách rõ nguồn dữ liệu tọa độbán kínhLocked
DEC-033TechnicalCá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-034TechnicalGPS 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 caGiữ 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 caLocked

A) Phạm vi nghiệp vụ (PRD)

A1) Tóm tắt giải pháp (Blueprint)

Trường (Field)Giá trị (Value)
FeatureChấm công đa đơn vị chấm công trên shared tenant
Mục tiêuCho 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 đổiDashboard, report doanh thu, report lương, payroll, org/report logic cũ của Diva
PlatformAdmin Web, Staff Mobile, Hasura/Go backend
Scope moduletimekeeping, 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À ca 4 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_unit chỉ trong scope chấm công để phân tách:
    • Diva
    • Daisy
    • Phuong Nam
  • Daisy và Phương Nam dùng app Diva để chấm công.
  • Admin/HR dùng:
    • Working Schedule
    • Working Sheet
    • Timekeeping export
  • Chấm công app của Daisy/PN ở Day-1 là on-site attendance có 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
  • Penalty engine Day-1 (DEC-012 v3): timekeeping_penalty_rule per unit, export tự tính tiền phạt.
  • Công chuẩn tự động Day-1 (DEC-013 v4): timekeeping_standard_workday_rule theo standard_workday_scope_key + bảng map department -> 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ốc cho PN (ca thường) + Daisy (VP, xuyên trưa)
  • App chấm công 4 mốc cho 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_rule per unit (DEC-012 v3, DEC-027)
  • Công chuẩn tự động Day-1 — timekeeping_standard_workday_rule theo standard_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_onetime hiệ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)

GoalMetricTarget
PN chấm công app 2 mốc + 4 mốc ổn địnhPilot pass100% P0
Daisy chấm công app 4 mốc + 2 mốc ổn địnhPilot pass100% P0
GPS scope đúng branch trong unitGPS validation pass100% P0
HR có báo cáo vận hành đủ dùngWorking sheet + export pass100% pilot
Settings Module hoạt động đúng per unitConfig apply đúng unit100% pilot
Penalty engine tính đúng theo rule per unitPenalty calc match rule100% pilot
Không làm ảnh hưởng logic cũ của DivaNon-regression timekeeping legacy pass0 P0 regression

A4) Vai trò người dùng (Personas)

PersonaVai tròJTBD
HR Phương NamTheo dõi công, export công, cấu hình penalty/công chuẩnChốt bảng công tháng cho PN, bao gồm tiền phạt + OT
HR DaisyTheo dõi công 4 mốc, cấu hình penalty/công chuẩnChốt bảng công và xử lý thiếu mốc
ManagerXem công nhân sự thuộc scope mìnhKiểm tra trễ/sớm/quên chấm
StaffChấm công trên appTự chấm và xem lịch sử
System AdminCấu hình timekeeping_unit, Settings ModuleGá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_unit chư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_unit chỉ á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ào timekeeping_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à SettingsInternal SettingsĐơn vị chấm công (SCR-00), không nằm trong UserCreate / EmployeeProfileCreate.
  • [ ] Có thể map branch và department hiện hữu vào timekeeping_unit chỉ để dùng trong timekeeping.
  • [ ] Assignment user vào unit phải lưu đủ scope vận hành Day-1:
    • primary_branch_id
    • primary_department_id
    • effective_from
    • effective_to
  • [ ] Chờ hiệu lực chỉ áp dụng cho assignment user khi effective_from > today.
  • [ ] Mapping Chi nhánh áp dụngPhòng ban áp dụng dùng soft-disable để giữ lịch sử vận hành, không hard delete.
  • [ ] Một user chỉ có 1 active timekeeping_unit tại một thời điểm.
  • [ ] Admin xem được headcount theo timekeeping_unit trong phạm vi chấm công.
  • [ ] Có Readiness ChecklistSCR-00 để theo dõi tối thiểu:
    • Chi nhánh áp dụng
    • Phòng ban áp dụng
    • Nhân viên áp dụng
    • Người duyệt
    • Ca làm việc
    • GPS/Quy định
    • Kiể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 khai trong SCR-00 để bật/tắt rollout admin/mobile theo từng unit.
  • [ ] Bật rollout chỉ 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 khai và 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ốc hoặc 4 mốc bằng break_clocking_required per shift template (DEC-011).
  • [ ] Nếu break_clocking_required = true, admin chọn break_mode:
    • fixed: giờ nghỉ trưa bám đúng start_break_time/end_break_time
    • flex: giờ nghỉ trưa có khung mặc định để tham chiếu, nhưng Day-1 không hard-validate mốc Ra nghỉ/Vào lại theo 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ằng break_time = true, nhưng mobile/admin chỉ render contract 2 mốc.
  • [ ] Daisy khối Dịch vụ (BS, Phụ tá, Điều trị) dùng ca 4 mốc:
    • Vào ca
    • Ra nghỉ
    • Vào lại
    • Ra 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_unit củ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_meters của unit; nếu chưa override thì fallback global AppSettings.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 unit
    • gps_required = false → không chặn punch bởi GPS cho ca đó
  • [ ] Logic remote_onetime / remote_weekly approved tiếp tục là runtime flow riêng Day-1, không render thành option kỹ thuật trong form Ca làm việc.
  • [ ] Runtime resolve cách tính workday từ shift đang được gán trong ngày:
    • fixed → áp dụng logic 0 / 0.5 / 1.0 theo max_workday của ca
    • hourly → áp dụng actual_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 filter timekeeping_unit, filter chi nhánh, multi-record day (4 mốc).

  • [ ] Working Schedule lọc được theo timekeeping_unit.
  • [ ] Working Sheet hiể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.dart chỉ 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 WHERE timekeeping_unit_id + đọc max từ timekeeping_unit thay vì global.
  • Remote bypass: log_time_keeping.go đã có logic remote_onetime + remote_weekly bypass GPS. Daisy Ca 3 Marketing dùng đơn remote_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_onetime hiệ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_rule config 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.tsx hoà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_key migration. BLOCKER — fix time_slot_template_update.go (match bằng name) 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_time config đú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_key unique trong timekeeping_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_flag tí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 flag auto_schedule_disabled skip 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_shift skip user thuộc timekeeping_unit có flag auto_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 WHERE timekeeping_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 WHERE timekeeping_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_minutes chỉ 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_hours chưa có. BE 1-2d.

  • [ ] Nếu NV có đơn trễ/sớm approved → workday = max_workday của ca (full công).
  • [ ] Nếu NV KHÔNG có đơn → workday = actual_hours / standard_hours × max_workday.
  • [ ] standard_hours config 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
  • [ ] Á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 view report_annual_leave. Delta: chỉ thêm filter timekeeping_unit.

  • [ ] Thêm filter Timekeeping Unit vào màn AnnualLeaveReport.tsx hiệ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_logs thêm timekeeping_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_unit gồm các tab:
    • Công chuẩn: rule mapping theo standard_workday_scope_key + map department -> 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
  • [ ] 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_rule table + penalty calculation engine. BE 2d.

  • [ ] Mỗi timekeeping_unit có bộ penalty rules riêng trên bảng timekeeping_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êng
    • shared (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ì hardcode phú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ùng branchLabelID hardcode. Delta: đọc từ primary_department_id -> standard_workday_scope_key -> rule, kết quả Diva không đổi.

  • [ ] Bảng timekeeping_standard_workday_rule lưu rule theo timekeeping_unit_id + standard_workday_scope_key.
  • [ ] Bảng timekeeping_standard_workday_scope_department map department_id vào standard_workday_scope_key trong 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

A6) Giả định (Assumptions)

IDGiả định (Assumption)Người phụ trách (Owner)
ASM-001Daisy và PN chấp nhận dùng app Diva làm kênh chấm công chínhPO
ASM-002Báo cáo phase này chỉ phục vụ vận hành chấm công, không phải payrollPO + HR
ASM-003branchdepartment 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_unitTL
ASM-004Day-1 chỉ cần on-site attendance cho majority; remote hỗ trợ nhóm nhỏ qua remote_onetime hiện cóPO
ASM-005Superseded by DEC-012 v3: Penalty engine Day-1, HR không cần Excel
ASM-006Superseded by DEC-013 v3: Công chuẩn tự động Day-1
ASM-007Daisy 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-008Superseded by DEC-014 confirmed: PN ca gãy = 4 mốc

A7) Rủi ro (Risks)

IDRiskImpactMitigation
RSK-001Runtime mới làm sai chấm công hiện tại của DivaCaoFlag theo timekeeping_unit, non-regression suite cho Diva timekeeping
RSK-002Ca 4 mốc làm mobile/admin hiển thị saiCaoTest matrix kỹ cho cả 2 mốc + 4 mốc. Tách test scenario per shift type
RSK-003Approver timekeeping lẫn với flow khácTrung bìnhDùng config approver timekeeping riêng, scope per unit
RSK-004User/branch/department map sai vào timekeeping_unitTrung bìnhValidation và seed data kiểm soát
RSK-005GPS sai hoặc map branch sai làm user không punch đượcTrung bìnhGPS test matrix + pilot smoke theo branch thật
RSK-006Cronjob log_time_keeping_flag tính sai late/early cho ca gãy PN (break >2h)Trung bìnhUnit test riêng cho ca gãy; verify break window không tính thành trễ/sớm
RSK-007Cron generate_working_shift copy sai cho Daisy ca xoayTrung bìnhSkip auto-generate cho unit có auto_schedule_disabled; Daisy HR import manual
RSK-008Settings Module phức tạp, nhiều tab/config — UX khó dùng hoặc bug config saiTrung bìnhUI review kỹ với HR trước pilot. Default values hợp lý cho mỗi unit. Validation chặt khi save config
RSK-009Penalty engine tính sai tiền phạt do config rule phức tạp (exempt_pool, exempt_count, multi-mode)Trung bìnhUnit 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)

MetricTarget
pn_app_attendance_success_rate>= 99% pilot
daisy_4_punch_success_rate>= 99% pilot
gps_scope_validation_pass_rate100% P0
timekeeping_export_success_rate100% pilot
penalty_calc_accuracy100% match HR manual calc
standard_workday_calc_accuracy100% match legacy result for Diva
legacy_diva_timekeeping_p0_regression0

A9) Bảng thuật ngữ (Glossary)

Thuật ngữENĐịnh nghĩaPhân biệt với
Đơn vị chấm côngTimekeeping UnitLớp scope chỉ dùng trong module chấm côngKhác công ty toàn hệ thống
Ca 2 mốc2-punch shiftVào ca, Ra vềKhác ca 4 mốc
Ca 4 mốc4-punch shiftVà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 attendanceChấm công bằng app khi GPS nằm trong branch hợp lệKhác remote attendance
Bảng côngWorking SheetBảng tổng hợp chấm công theo ngày/thángKhác payroll
Ca gãySplit shiftCa có break dài (>1h), NV rời cơ sở và quay lạiKhác ca 4 mốc (break ngắn)
Ca xoayRotating shiftCa luân phiên sáng/chiều theo tuầnKhác ca cố định
Công chuẩnStandard workdaySố ngày công chuẩn trong tháng dùng tính lươngKhác ngày công thực tế
Mốc chấm côngPunch eventMột lần bấm chấm công (vào/ra)Segment = cặp vào+ra
Nghỉ giữa ca cố địnhFixed breakNghỉ giữa ca bám đúng khung break của shift templateKhác nghỉ giữa ca linh hoạt
Nghỉ giữa ca linh hoạtFlexible breakNghỉ 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ựcPending assignmentAssignment user đã lưu nhưng chưa đến effective_fromKhác Đang áp dụng
Ngưng áp dụngDisabled mappingMapping/unit vẫn còn lịch sử nhưng không còn dùng cho runtime mớiKhác xóa cứng dữ liệu
Checklist sẵn sàng triển khaiReadiness checklistTập điều kiện xác nhận unit đã đủ dữ liệu/config để bật rolloutKhác sign-off go-live
Penalty enginePenalty engineHệ thống tự động tính tiền phạt theo rule per unitKhác tính phạt thủ công bằng Excel
Settings ModuleSettings ModuleMà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ệ:
    • complete
    • missing_start
    • missing_break
    • missing_end
    • partial

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
  • 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 định 12:00-14:00 để phạt/block ở mốc 2 và 3. Hệ thống chỉ yêu cầu đủ 4 mốc, tính actual_hours theo 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) / 60 cho 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_rate
    • ot_rate đọc từ Settings Module per unit:
      • PN: thường ot_rate_default = 50,000đ/h, BS ot_rate_doctor = 150,000đ/h
      • Daisy: thường ot_rate_default = 35,000đ/h, BS ot_rate_doctor = 150,000đ/h
  • Biến số:
    • ot_min_threshold_minutes: config per unit (PN = 30, Daisy = 0)
    • ot_rate_default: rate mặc định per unit
    • ot_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 ca
    • missing_end_count: số ngày thiếu mốc cuối ca
    • missing_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
  • 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
  • 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_rule per unit. Thay thế hardcode phú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ần
    • deduct_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/ra
      • shared (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 trong user_salary.go.
  • Bảng timekeeping_standard_workday_rule: Day-1 mapping timekeeping_unit_id + standard_workday_scope_key -> mode.
  • Bảng timekeeping_standard_workday_scope_department: map department_id củ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 đương countNonSundayDays() hiện tại
    • days_minus_sun_half_sat: trừ CN + nửa ngày T7. VD: 30 - 4 CN - (5 T7 x 0.5) = 23.5 ngày
    • fixed_26: luôn = 26.0 ngày
    • fixed_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
  • Edge cases:
    • Unit/scope chưa có rule -> fallback fixed_26
    • Tháng 2 nhuận -> days_minus_sun tự tính đúng

Kế hoạch phát hành (Release Plan)

ReleaseScopeGhi chú
1A — Foundationtimekeeping_unit schema + canonical_key migration + Settings Module + non-regression DivaKhông có feature mới cho end-user. Settings Module sẵn sàng cho 1B config
1B — PN + Daisy đồng thờiRuntime 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 — HardeningBugfix, performance tuning, mở rộng rollout, export nâng cấp nếu cầnPhase tối ưu hoá và ổn định

Phạm vi để lại giai đoạn sau (Deferred Scope)

ItemLý do deferĐơn vị yêu cầu
Auto-rotate schedule cho ca xoayCron generate_working_shift không hỗ trợ ca xoay. Day-1 HR import manual (DEC-017)Daisy
Đơn xin nghỉ việcThuộc HR module, không phải timekeepingDaisy
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.