Bỏ qua để đến nội dung

Data model

Thứ tự khớp sort trong schema — cũng là thứ tự tạo khi Setup Wizard chạy (bảng phụ thuộc FK nằm sau bảng nó tham chiếu).

# Collection Loại Mô tả
1 hr label (không có bảng) Thư mục gom nhóm trong Data Studio
2 hr_people table Bảng trung tâm — satellite của c_contacts; talent/candidate/employee đều là 1 row ở đây, phân biệt bằng stage
3 hr_departments table Phòng ban, có head_person_idparent_department_id tự tham chiếu (cây phân cấp)
4 hr_employment_types table (lookup, seeded) full_time / part_time / contractor / intern
5 hr_stage_transitions table Audit trail đổi stage, ghi from_stage/to_stage/performed_by
6 hr_applications table Một vòng ứng tuyển của một hr_people (1 người có thể có nhiều application qua các đợt)
7 hr_interviews table Buổi phỏng vấn, gắn với 1 application
8 hr_offers table Offer 1:1 với application (lương, hạn, trạng thái draft/sent/accepted/declined/expired)
9 hr_contracts table Hợp đồng lao động (indefinite/fixed_term/probation/freelance)
10 hr_compensations table Lương/thu nhập hiện tại của 1 người
11 hr_compensation_allowances table Phụ cấp O2M của 1 compensation
12 hr_insurances table Số BHXH/BHYT + trạng thái đủ điều kiện
13 hr_leave_types table (lookup, seeded) annual / sick / unpaid / maternity / paternity
14 hr_leave_balances table Số ngày phép còn lại theo người + loại + năm
15 hr_leave_requests table Đơn xin nghỉ phép (pending/approved/rejected/cancelled)
16 hr_company_holidays table Ngày nghỉ lễ công ty (có thể lặp hằng năm)
17 hr_documents table Tài liệu của 1 người (hợp đồng, offer letter, ID, tài liệu tự sinh)
18 hr_document_templates table Mẫu tài liệu (markdown/rich-text)
19 hr_document_triggers table Sự kiện tự sinh tài liệu (offer_sent, hired, probation_confirmed, resigned…)
20 hr_document_trigger_tpl_jn junction (M2M) Nối hr_document_triggershr_document_templates — 1 trigger có thể sinh nhiều template
21 hr_assets table Tài sản cấp phát (laptop, phone, access card…), có images (files nhiều ảnh)
22 hr_notes table Ghi chú nội bộ theo người
23 hr_policies table Rule tính leave/tenure-bonus/eligibility/probation, params JSON
24 hr_settings singleton Cấu hình toàn module (tên công ty, currency, năm tài chính, nhãn stage tuỳ biến)
25 hr_providers table Provider 3rd-party (Authentik/webhook) cho provisioning tài khoản
26 hr_provisioning_logs table Log provision/deprovision/update tài khoản theo người + provider
27 hr_offboarding table Case nghỉ việc (exit type, notice date, settlement, exit interview)
28 hr_offboarding_tasks table Checklist bàn giao O2M của 1 offboarding case

Collection dùng chung tham chiếu tới nhưng không thuộc HR:

  • c_contacts — master identity, HR là satellite bắt buộc (xem Tích hợp Contacts)
  • odp_users — liên kết tài khoản đăng nhập (hr_people.user_id), performed_by/interviewer/approved_by/author_id/assignee trên các bảng con
  • odp_access / odp_policies / odp_app_permissions — mô hình app-permission (module:action), xem Kiến trúc tổng thể
  • Thư mụcadmin/packages/hr/
    • app/data/hr-schema.ts ModuleSchema — 28 collection + field + relation + seed, chạy qua Setup Wizard
Field Type Ghi chú
contact_id integer, is_unique: true M2O → c_contacts — satellite 1:1, không được trùng contact
stage string, required, default talent 6 giá trị: talentinterviewingofferprobationactiveresigned
employee_id string, unique Sinh tự động (generateNextId) khi vào stage probation/active, không sinh cho stage recruitment
first_name/last_name/display_name/email/phone string Cache identity — backfill một lần từ c_contacts lúc tạo trong people.service.ts#createPerson, không đồng bộ nền liên tục sau đó
user_id uuid, M2O → odp_users Liên kết tài khoản ODP — dùng cho self-service (chưa làm, xem PERMISSIONS.md Phase 3) và Dept-Manager row-scoping
department_id integer, M2O → hr_departments
employment_type_id integer, M2O → hr_employment_types Đổi field này kích hoạt hook recalculatePersonBalances
Từ Field Tới Ghi chú
hr_people contact_id c_contacts nullify — 1:1 master/satellite
hr_people department_id hr_departments nullify
hr_people employment_type_id hr_employment_types nullify
hr_people user_id odp_users nullify
hr_departments head_person_id hr_people self-loop khác bảng — dùng cho Dept-Manager scoping
hr_departments parent_department_id hr_departments self-reference — cây phân cấp
hr_stage_transitions, hr_applications, hr_contracts, hr_compensations, hr_insurances, hr_leave_balances, hr_leave_requests, hr_documents, hr_assets, hr_notes, hr_provisioning_logs, hr_offboarding person_id hr_people O2M, on_delete: "CASCADE" khai báo ở schema (DB-level)
hr_interviews application_id hr_applications O2M cascade
hr_offers application_id hr_applications 1:1 cascade
hr_compensation_allowances compensation_id hr_compensations O2M cascade
hr_leave_balances/hr_leave_requests leave_type_id hr_leave_types nullify
hr_documents template_id hr_document_templates nullify
hr_offboarding_tasks offboarding_id hr_offboarding O2M cascade
hr_provisioning_logs provider_id hr_providers nullify

Junction M2M duy nhất: hr_document_trigger_tpl_jn

Phần tiêu đề “Junction M2M duy nhất: hr_document_trigger_tpl_jn”

Khác Helpdesk (3 junction, đều hydrate bằng raw Knex whereIn + Map), HR chỉ có một junction thật (hr_document_trigger_tpl_jn, nối hr_document_triggershr_document_templates), và cách quản lý cũng khác về chi tiết: triggers.controller.ts build template_ids bằng một query readByQuery rồi .filter() trong JS (không phải Map gộp theo id như Helpdesk, vì số lượng trigger nhỏ):

backend/extensions/hr/src/endpoints/controllers/triggers.controller.ts
const triggers = await svc.readByQuery({ sort: ['id'] });
const mappings = await jnSvc.readByQuery({
fields: ['hr_document_triggers_id', 'hr_document_templates_id'],
limit: -1,
});
const mapped = triggers.map((t: any) => ({
...t,
template_ids: mappings
.filter((m: any) => m.hr_document_triggers_id === t.id)
.map((m: any) => m.hr_document_templates_id),
}));

Ghi (PATCH) cũng theo nguyên tắc “xoá hết rồi insert lại tập đích”, giống syncConversationLabels của Helpdesk — không để ItemsService.updateOne tự M2M-sync mảng id:

backend/extensions/hr/src/endpoints/controllers/triggers.controller.ts
if (Array.isArray(template_ids)) {
const existing = await jnSvc.readByQuery({
filter: { hr_document_triggers_id: { _eq: req.params.id } },
fields: ['id'],
limit: -1,
});
if (existing.length > 0) {
await jnSvc.deleteMany(existing.map((e: any) => e.id));
}
if (template_ids.length > 0) {
await jnSvc.createMany(
template_ids.map((tid: number) => ({
hr_document_triggers_id: req.params.id,
hr_document_templates_id: tid,
}))
);
}
}

Hai tầng cascade delete — không hoàn toàn trùng khớp

Phần tiêu đề “Hai tầng cascade delete — không hoàn toàn trùng khớp”

Xoá một hr_people chạy qua hai cơ chế song song, không phải một:

  1. App-level, filter hook (hooks/index.tscascadeDeletePerson, services/cascade-delete.ts) — chạy TRƯỚC khi row hr_people thật sự bị xoá, xoá tường minh 11 bảng con qua DIRECT_CHILD_COLLECTIONS: hr_notes, hr_stage_transitions, hr_leave_balances, hr_leave_requests, hr_documents, hr_assets, hr_provisioning_logs, hr_applications (kéo theo hr_interviews/hr_offers qua application_id), hr_contracts, hr_compensations (kéo theo hr_compensation_allowances), hr_insurances.
  2. Schema-level on_delete: "CASCADE" khai báo trong relations của hr-schema.ts — DB tự cascade khi FK constraint được tạo thật ở tầng DB.