Skip to content

Nguồn sự thật chuẩn (Source of Truth) — Chấm công đa đơn vị v3.1.4

File này là nguồn duy nhất cho tất cả decisions, scope, và facts. Session mới dùng file này + codebase thực tế để viết lại 6 spec files sạch.

Ngày: 21/04/2026

Changelog

VersionDateAuthorThay đổi
3.1.421/04/2026PO/BAKhóa lại canonical Day-1 cho break_mode = flex, missing_breakremote: break linh hoạt chỉ là metadata, không auto trừ công vì thiếu mốc giữa ca, và nhóm nhỏ tiếp tục dùng flow remote legacy đã approved
3.1.315/04/2026PO/BAChuyển model Công chuẩn sang nhóm áp dụng công chuẩn (standard_workday_scope_key) + map department -> scope, thay cho proposal cũ bám branch_label_id
3.007/04/2026PO/BAInitial source of truth cho multi-unit timekeeping
3.0.107/04/2026PO/BAĐồng bộ canonical package về break_mode, annual leave Day-1, remote nhóm nhỏ và QA/go-live wording
3.113/04/2026PO/BABổ sung canonical decisions cho primary_department_id, soft-disable, readiness checklist, tab Triển khai và vocabulary vận hành
3.1.113/04/2026PO/BALàm rõ GPS source-of-truth: branch coordinates tiếp tục dùng branch master data hiện có; gps_radius_meters là override per-unit và fallback global chỉ dùng khi unit chưa cấu hình
3.1.214/04/2026PO/BAChuyển cách tính công từ cấp timekeeping_unit xuống time_slot_template: mỗi ca tự quyết định Theo ngày công hoặc Theo giờ, reuse field workday hiện có làm công tối đa của ca; đồng thời chuyển yêu cầu GPS của ca xuống cấp shift dưới dạng gps_required = true/false

1. Bài toán

PN (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.

2. Giải pháp

timekeeping_unit + Settings Module toàn diện. unit_id = NULL → legacy code nguyên vẹn.

3. Inputs (đã có sẵn — đọc TRƯỚC khi viết)

FileĐường dẫnNội dung
Logic codebase chấm công/Users/sontho/Downloads/timekeeping-flow.md4 module: Work-Shift, Working-Schedule, Working-Sheet, Approver
Khảo sát Daisy/Users/sontho/Downloads/Daisy - Phiếu khảo sát yêu cầu chấm công.pdf10 trang, 25 câu
Khảo sát PN/Users/sontho/Downloads/Phương Nam - Phiếu khảo sát yêu cầu chấm công.pdf10 trang, 25 câu
Phản hồi Daisy/Users/sontho/Downloads/Câu hỏi Daisy.pdf4 câu trả lời
Phản hồi PN/Users/sontho/Downloads/Câu hỏi Phương Nam.pdf4 câu trả lời

4. Quyết định chuẩn (Canonical decisions) — CHỐT, không thay đổi

Scope & Architecture

IDQuyết địnhTrạng thái (Status)
DEC-001Daisy + PN dùng app Diva chấm côngLocked
DEC-002Timekeeping-only scope, không retrofit company boundaryLocked
DEC-003Dùng timekeeping_unit (không business_unit)Locked
DEC-004timekeeping_unit không đổi branch/department/report/dashboard/salary globalLocked
DEC-005Tách approver timekeeping sang scope riêngLocked
DEC-006time_slot_time_keeping = source of truth, runtime dùng time_slot_user_id + segment_indexLocked
DEC-008Báo cáo = vận hành chấm công scopeLocked
DEC-009GPS on-site bắt buộc Day-1Locked
DEC-028Department có branch_id → tạo dept per branch, mapping qua timekeeping_unit_departmentLocked
DEC-029Assignment user vào timekeeping_unit phải lưu primary_branch_id + primary_department_id + effective_from/effective_to để khớp filter/export/approver scope Day-1Locked
DEC-030Mapping timekeeping_unit_branch + timekeeping_unit_department dùng soft-disable (disabled = true), không hard delete Day-1Locked
DEC-031SCR-00Readiness Checklist + tab Triển khai; Bật rollout chỉ khả dụng khi checklist không còn badge Locked
DEC-032GPS Day-1 không build màn master riêng: tọa độ chi nhánh tiếp tục quản lý ở branch master data hiện có; gps_radius_meters nằm ở SCR-00 > Quy định, và nếu unit chưa override thì fallback global AppSettings.Position.Value.DistanceLocked
DEC-033Cách tính công không cấu hình ở timekeeping_unit. Day-1 đặt tại time_slot_template để mỗi ca tự chọn fixed hoặc hourly; field workday hiện có được reuse làm công tối đa của ca, thêm standard_hours ở cấp ca để hỗ trợ mode hourlyLocked
DEC-034GPS bypass không cấu hình ở timekeeping_unit. Day-1 đặt tại time_slot_template dưới dạng gps_required = true/false; timekeeping_unit chỉ giữ radius mặc định. Flow remote_onetime / remote_weekly vẫn là runtime/request logic riêng, không expose thành option cấu hình trong màn Ca làm việcLocked

Rollout

IDQuyết địnhTrạng thái (Status)
DEC-007Phase 1A: Foundation (schema + canonical_key + Settings Module). Phase 1B: PN + Daisy đồng thời. Phase 1C: HardeningLocked

Chấm công 2 mốc / 4 mốc

IDQuyết địnhTrạng thái (Status)
DEC-011Rule nghỉ giữa ca nằm per shift template, reuse field sẵn có break_time, start_break_time, end_break_time; bổ sung break_clocking_required, break_mode = fixed/flex, break_flex_minutes (không config ở cấp unit). 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ếUpdated v5
DEC-014PN ca gãy = 4 mốc (xác nhận bởi PN: cần track trễ đầu ca sáng + chiều)Confirmed
DEC-019Daisy 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átConfirmed
DEC-024Daisy Tạp vụ: 6 ca cố định mới, 7 NVConfirmed

Settings Module toàn diện

IDQuyết địnhTrạng thái (Status)
DEC-012Penalty engine Day-1timekeeping_penalty_rule per unit. HR KHÔNG cần ExcelUpdated v3
DEC-013Công chuẩn Day-1timekeeping_standard_workday_rule mapping theo nhóm áp dụng công chuẩn (standard_workday_scope_key), và map department -> scope để phản ánh đúng xác nhận HCNS của PN/DaisyUpdated v4
DEC-022PN tính công theo giờ (actual_hours / standard_hours) nhưng áp dụng ở cấp ca làm việc, không khóa cứng toàn đơn vị. Có đơn approved → full công. Daisy mặc định fixed nhưng vẫn cho phép mở rộng mixed-mode về sauConfirmed
DEC-026Settings Module toàn diện: tất cả config per unit trên UI, backend đọc DB, không hardcodeLocked v3
DEC-027Bảng timekeeping_penalty_rule. exempt_pool = 'individual' (PN) / 'shared' (Daisy)Locked v3

Remote

IDQuyết địnhTrạng thái (Status)
DEC-010Remote hỗ trợ nhóm nhỏ Day-1: Daisy Ca 3 Marketing + PN 3-4 NVUpdated
DEC-016Reuse logic remote_onetime hiện có — code log_time_keeping.go đã có bypass GPS cho remote_onetime + remote_weekly. Day-1 giữ flow này như request/runtime logic riêng, không build feature remote mới và không đưa thành option riêng trong cấu hình caLocked

Approval

IDQuyết địnhTrạng thái (Status)
DEC-015Daisy + PN = 2 cấp duyệtConfirmed
DEC-020Count đơn scoped per timekeeping_unitLocked
DEC-021Daisy max 60 phút per đơn trễ/sớm. PN không giới hạnLocked

Phép năm

IDQuyết địnhTrạng thái (Status)
DEC-023Báo cáo phép năm bắt buộc Day-1 cho PN. Reuse AnnualLeaveReport.tsx + thêm filter unitConfirmed

Khác

IDQuyết địnhTrạng thái (Status)
DEC-017Daisy ca xoay → HR import Excel, cron generate_working_shift skip auto_schedule_disabledLocked
DEC-018Daisy Tạp vụ giờ linh động → HR tạo ca riêng đúng giờ NVLocked
DEC-025Daisy "hạn chế deploy" = gộp releaseConfirmed

5. Hiện trạng code THỰC TẾ (chưa đổi gì)

Tất cả dưới đây là code hiện tại — chưa có bất kỳ thay đổi nào cho feature mới.

Schema hiện tại (CHƯA có timekeeping_unit_id, segment_index)

BảngColumns liên quanGhi chú
time_slot_templateid, name, start_time, end_time, break_time, start_break_time, end_break_time, workday, roles, disabledworkday đã có sẵn và nên reuse làm công tối đa của ca. CHƯA CÓ: timekeeping_unit_id, canonical_key, break_clocking_required, break_mode, break_flex_minutes, workday_calculation_mode, standard_hours, gps_required
time_slot_userid, user_id, name, start_time, end_time, break_time, workday, branch_id, time_keeping_idCHƯA CÓ: timekeeping_unit_id
time_slot_time_keepingid, created_by, clock_in, clock_out, position_in/out, branch_id, late_arrival, leave_early, off, workday, overtimeCHƯA CÓ: timekeeping_unit_id, time_slot_user_id, segment_index
request_working_scheduleid, code, type, behavior, status_id, from, to, reason, workday, num_approvedCHƯA CÓ: timekeeping_unit_id, segment_index
request_approverid, department_id, branch_id, user_id, request_type, level_idCHƯA CÓ: timekeeping_unit_id (approver resolve theo dept/branch, KHÔNG theo unit)
departmentid, name, branch_id, code, disabledCÓ branch_id — dept scoped per branch
branchid, name, code, branch_label_id, company_fullname, tax_codeCÓ branch_label_id — metadata branch hiện có, nhưng Day-1 công chuẩn mới KHÔNG dùng làm business scope
employee_profileremaining_leave_days, prev_remain_leavePhép năm balance
annual_leave_logsid, account_id, leave_days, request_idCHƯA CÓ: timekeeping_unit_id
general_salarybasic_salary, standard_workday, actual_workday, paid_day_off, overtime_hourSalary data

Backend logic hiện tại

FileLogicKey points
log_time_keeping.goAction chấm cônglimit: 1 query → toggle clock_in/clock_out (SINGLE pair). GPS: CalculateDistance() check ALL branches. Remote bypass: remote_onetime + remote_weekly ĐÃ CÓ
log_time_keeping_flag.goCronjob 23:15Late > 1p → flag. Late > 60p → trừ 0.5 công. late_early_fine = phút × 10,000 (HARDCODE). 15 workers, timeout 355s. Salary: GenerateSalary() tự động
time_slot_template_update.goEvent triggerMatch time_slot_user bằng name (KHÔNG phải ID/key) — BLOCKER phải fix trước
generate_working_shift.goCron CN 23:00Copy tuần trước → tuần sau. Match template bằng mapTemplateByName[item.Name]cũng dùng name
request_working_schedule_insert.goEvent triggerResolve reviewers bằng GetReviewersNextstepWithConfig() → query request_approver WHERE department_id + request_type + level_id. KHÔNG có dimension unit
user_salary.goInit salaryHiện hardcode branchLabelID == "general"/"office" → countNonSundayDays(), else → 26.0. Cần replace bằng resolve qua primary_department_id -> standard_workday_scope_key
count_remaining_*PostgreSQL functions2 functions đếm đơn trễ/sớm + quên chấm. Đọc max từ hrm_master_data (GLOBAL, không per unit)
app_settingConfigrequest_leave.value: min_working_day, reset_date, leave_expiry_date (GLOBAL)

FE hiện tại

FileCó gìCHƯA CÓ gì
WorkingTimeSheet.tsxFilter: DepartmentSelect, ShiftGroupSelect, DateRange, search. Cell: clock_in/out, late/early badge, request overlay. Export: 3 loạiCHƯA CÓ: filter timekeeping_unit, filter chi nhánh, multi-record day (4 mốc)
WorkingSchedule.tsxFilter: DepartmentSelect, ShiftGroupSelect, DateRange, search. Import Excel: WorkingScheduleImport.tsxCHƯA CÓ: filter unit, filter chi nhánh
ExportWorkingTimeSheetByDay.tsx14 cột đã có: STT, Ngày, Họ tên, Mã NV, Phòng ban, CN vào/ra, Ca, Giờ vào/ra, Trễ phút, Sớm phút, Đơn, OT. Logic: getTimeLateArrival(), getTimeEarlyLeave(), getOverTime()CHƯA CÓ: filter unit, 4 cột ca 4 mốc, cột tiền phạt
ExportTotalWorkingTimeSheetByDay.tsx5 cột đã có: STT, Họ tên, Mã NV, Phòng ban, Tổng ngày công. Logic: calculateWorkDays()CHƯA CÓ: filter unit
AnnualLeaveReport.tsxHoàn chỉnh: filter năm/phòng ban/chi nhánh, export Excel, DB view report_annual_leaveCHƯA CÓ: filter unit
ApproverPage.tsx15 tab (clock_in_out, change_shift, forget_clock, late_arrival_early_leave, leave, overtime, remote...). SettingApprover*.tsx componentsCHƯA CÓ: filter unit, scope per unit
ShiftSetting.tsx2 tab: Work Shift + Shift Group. CRUD templatesCHƯA CÓ: filter unit, contract 2 mốc/4 mốc, mode nghỉ giữa ca fixed/flex, field break_clocking_required, break_mode, break_flex_minutes
BranchForm.tsxĐã có latitude / longitude trong branch master dataCHƯA CÓ: bán kính GPS per-unit; đây không phải màn timekeeping settings
attendance_bloc.dartState machine 2 mốc: CheckIn → CheckOut. GPS check. Calendar historyCHƯA CÓ: state machine 4 mốc, quota hiển thị, running total

6. Phân loại effort THỰC TẾ

Phải build MỚI hoàn toàn (code chưa có gì)

FeatureLý doEffort estimate
Bảng timekeeping_unit + schema mớiConcept chưa tồn tạiBE 2-3d
Settings Module UI (SCR-00, 6 tab + Tab Quy định)Chưa cóFE 4-5d
timekeeping_penalty_rule table + engineChưa có penalty config per unitBE 2d
timekeeping_standard_workday_rule + timekeeping_standard_workday_scope_departmentChưa có (hiện hardcode trong user_salary.go)BE 1-1.5d
Multi-segment runtime (4 mốc) trong logTimeKeepingHiện chỉ toggle clock_in/clock_out. Cần state machine mớiBE 2-3d
Multi-segment cronjob trong log_time_keeping_flagHiện xử lý 1 record/user/day. Cần xử lý 2 records/user/dayBE 2-3d
Mobile 4 mốc state machineHiện chỉ CheckIn/CheckOut. Cần thêm BreakOut/BreakInFE Mobile 2-3d
Tính công theo giờ (PN)Logic actual_hours / standard_hours chưa cóBE 1-2d
Export bảng công tháng (17 cột)Hiện chỉ 3 cột + grid. Cần rewrite column mappingFE 2d

Phải MIGRATE / FIX trước (Phase 0 blocker)

TaskHiện trạng codeTargetBlocker?
canonical_key backfill + đổi propagationtime_slot_template_update.go match bằng name. generate_working_shift.go dùng mapTemplateByNameMatch bằng canonical_key + timekeeping_unit_idBLOCKER — fix trước khi seed template PN/Daisy
ADD COLUMN timekeeping_unit_id (nullable)4 bảng chưa có column nàyALTER TABLE + nullableBLOCKER — schema phải có trước khi code mới chạy
ADD COLUMN segment_index (nullable)time_slot_time_keeping + request_working_schedule chưa cóALTER TABLE + nullableBLOCKER cho 4 mốc
Approver resolve thêm dimension unitHiện resolve bằng dept/branch + type + level. KHÔNG có unitThêm timekeeping_request_approver table hoặc extend request_approverBLOCKER — không có thì đơn PN chuyển sang approver Daisy
Assignment lưu primary_department_idUI Day-1 đã filter/list/export theo phòng ban áp dụng, nhưng schema hiện tại chưa có field tương ứngThêm cột vào timekeeping_unit_userBLOCKER — không có thì FE/BE phải tự suy diễn department
Mapping branch/department giữ lịch sửUI Day-1 dùng Ngưng áp dụng/Xóa khỏi đơn vị, nhưng schema hiện tại chưa có disabledThêm disabled vào bảng mapping + contract reactivateBLOCKER — không có thì trạng thái UI không thể persist đúng
Readiness checklist + tab Triển khaiUI Day-1 đã có checklist/bật rollout, nhưng chưa có aggregate contract trong code hiện tạiThêm payload aggregate + persistence rollout snapshotBLOCKER — không có thì rollout tab chỉ là mock

Reuse ĐƯỢC (chỉ cần extend filter/scope)

FeatureCodebase hiện cóDelta thực tế
Export chi tiết ngàyExportWorkingTimeSheetByDay.tsx — 14 cột + logic trễ/sớm/OTThêm filter unit + 4 cột ca 4 mốc
Export tổng côngExportTotalWorkingTimeSheetByDay.tsx — 5 cộtThêm filter unit
Phép năm reportAnnualLeaveReport.tsx — hoàn chỉnhThêm filter unit
Import Excel lịchWorkingScheduleImport.tsx — hoàn chỉnhScope theo unit
Remote bypass GPSlog_time_keeping.goremote_onetime + remote_weekly logicKHÔNG cần build mới. Daisy Ca 3 tạo đơn remote → logic bypass đã có
Notification tạo đơnEvent trigger request_working_schedule_insert → resolve reviewers → pushVerify route đúng per-unit approver (cần extend resolver, không chỉ "verify")
Count requests 3/tháng2 PostgreSQL functionsALTER thêm WHERE timekeeping_unit_id + đọc max từ timekeeping_unit thay vì global
Late/early detectionCronjob: 1p flag, 60p trừ 0.5Logic 2 mốc KHỚP 100%. Delta CHỈ là multi-segment
GPS checkCalculateDistance() + radiusThay query ALL branches → chỉ branch trong unit
Công chuẩn logicuser_salary.go đang dùng branchLabelIDExtend: thêm nhóm áp dụng công chuẩn + mapping department -> scope per unit

7. Phản hồi đã xác nhận

Daisy

CâuKết quả
Break BS/Phụ táMặc định 12:00–14:00, break_flex_minutes = 60 chỉ là metadata Day-1. Ca "xuyên trưa" = 2 mốc
Tạp vụ6 ca cố định mới (chi tiết giờ + giờ nghỉ trưa cố định). 7 NV. CN Nha Trang 2 ca 4h riêng
"Hạn chế update"= Hạn chế deploy phần mềm quá nhiều
Tính phạt thủ côngOK 2-3 tháng đầu (nhưng Settings toàn diện → không cần thủ công)

Phương Nam

CâuKết quả
Ca gãy4 mốc — cần track trễ đầu ca sáng + đầu ca chiều
Tính công theo giờTất cả NV. Có đơn → full công. Không đơn → actual/standard. DV 8h, VP 7.5h
Phép nămBắt buộc Day-1
Tính phạt thủ côngOK 2-3 tháng đầu. Cần bảng công 4 lần vào/ra khối DV

8. Rollout & Timeline

Tuần 1:     TL review + chốt spec
Tuần 2-3:   Phase 1A — Foundation (1.5 tuần)
            Schema + canonical_key fix + Settings Module + non-regression Diva
            Gate: Diva pass ✓
Tuần 3-5:   Phase 1B — 3 track song song (3 tuần)
            BE + FE Admin + FE Mobile
Tuần 6-7:   Pilot (2 tuần)
            Gate: Pilot + Diva NR pass ✓
            GO-LIVE đầu tháng 6/2026
Sau:        Phase 1C — Hardening

Resource: 1 BE senior + 1 FE admin + 1 FE mobile + QA

9. Quy tắc cho session viết lại

  1. Đọc Source of Truth + codebase TRƯỚC khi viết
  2. Phân biệt rõ "hiện tại code" vs "target design" — dùng thì khác nhau
  3. Mỗi feature ghi Reuse / Extend / Build mới + file path + delta thực tế
  4. Wording phản ánh effort thật — không nhẹ hoá
  5. 1 quyết định = 1 nội dung canonical — không cho phép 2 files nói khác
  6. Sweep cross-file trước khi deliver
  7. Blocker = blocker — không ghi "delta nhỏ" cho schema change
  8. Vocabulary mới là canonical — dùng Chi nhánh áp dụng, Phòng ban áp dụng, Nhân viên áp dụng, Triển khai; không quay lại Tab Branch/Department/User/Rollout