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

Mở rộng module HR

  • Thư mụcadmin/packages/hr/app/
    • data/hr-schema.ts khai báo collection + field + relation (setup wizard, new install)
    • composables/use-hr-field-registry.ts fetch + normalize field metadata theo danh sách collection tự chọn
    • config/form-layouts.ts hằng số layout viết tay (TALENT_FORM_LAYOUT, EMPLOYEE_FORM_LAYOUT, CONTRACT_LAYOUT, COMPENSATION_LAYOUT, INSURANCE_LAYOUT…)
    • config/column-renderers.ts renderer cột cho bảng danh sách
    • utils/hr-permissions.ts khai báo action module hr (đăng ký qua defineLayerPermissions)
  • Thư mụcbackend/extensions/hr/src/endpoints/
    • controllers/meta.controller.ts COLLECTION_MAP cho GET /hr/fields/:collection

Bước 1 — Khai báo field trong hr-schema.ts (new install)

Phần tiêu đề “Bước 1 — Khai báo field trong hr-schema.ts (new install)”

Field là object phẳng { collection, field, type, meta, schema }, tách khỏi entry đăng ký collection — ví dụ field text đơn giản trên hr_people:

app/data/hr-schema.ts
{
collection: "hr_people",
field: "job_title",
type: "string",
meta: { interface: "input", width: "half" },
schema: {},
},

Field JSON cần editor code (ví dụ tham số policy):

app/data/hr-schema.ts
{
collection: "hr_policies",
field: "params",
type: "json",
meta: { interface: "input-code", special: ["cast-json"], options: { language: "json" }, width: "full" },
schema: {},
},

Bước 2 — DB đã có sẵn: tạo field qua Data Studio / API

Phần tiêu đề “Bước 2 — DB đã có sẵn: tạo field qua Data Studio / API”

Nếu module đã chạy production, tạo field trực tiếp qua Data Studio (hoặc API POST /fields/:collection của ODP core) — hr-schema.ts chỉ dùng để tài liệu hoá / cho lần cài mới sau.

Bước 3 — Field-metadata endpoint: COLLECTION_MAP, không phải allowlist chặn cứng

Phần tiêu đề “Bước 3 — Field-metadata endpoint: COLLECTION_MAP, không phải allowlist chặn cứng”

meta.controller.ts chỉ có một bảng ánh xạ short name → tên collection thật, dùng cho 6 collection hay gọi từ form:

backend/extensions/hr/src/endpoints/controllers/meta.controller.ts
const COLLECTION_MAP: Record<string, string> = {
people: 'hr_people',
departments: 'hr_departments',
employment_types: 'hr_employment_types',
leave_types: 'hr_leave_types',
policies: 'hr_policies',
settings: 'hr_settings',
};
export function fields(ctx: AppContext) {
return async (req: any, res: any) => {
const { collection } = req.params;
const realCollection = COLLECTION_MAP[collection] ?? collection;
// ...
const result = await fieldsService.readAll(realCollection);
return res.send({ data: result });
};
}

Bước 4 — Client field-registry: tự chọn danh sách, không có COLLECTIONS cố định

Phần tiêu đề “Bước 4 — Client field-registry: tự chọn danh sách, không có COLLECTIONS cố định”

Khác helpdesk (use-helpdesk-field-registry.ts có hằng COLLECTIONS cố định 11 phần tử), useHrFieldRegistry() của HR nhận danh sách collection làm tham số mỗi lần gọi:

app/composables/use-hr-field-registry.ts
export function useHrFieldRegistry(collections: string[]) {
const api = useHrApi()
const registry = ref<Record<string, Field[]>>({})
// fetchAll() gọi api.getFields(c) cho từng collection trong `collections`
}

Chuẩn hoá chỉ có 2 việc: đổi select-dropdownselect, và tính lại width (half/full) thành col-span cho grid 24-cột của FormRoot (vì Field.vue mặc định col-span-full, chỉ tôn trọng meta.options.ui.base):

app/composables/use-hr-field-registry.ts
const WIDTH_COLSPAN: Record<string, string> = {
'half': '@xl:col-span-12',
'half-left': '@xl:col-span-12',
'half-right': '@xl:col-span-12',
}

Bước 5 — Wire vào form: layout viết tay là chuẩn, không phải mảng tên field

Phần tiêu đề “Bước 5 — Wire vào form: layout viết tay là chuẩn, không phải mảng tên field”

Phần lớn form HR không dùng kiểu “layout khai tên field, registry tự render” như helpdesk. app/config/form-layouts.ts định nghĩa layout bằng hằng số mô tả chi tiết UI cho từng field — kiểu input, options, điều kiện hiển thị:

app/config/form-layouts.ts — CONTRACT_LAYOUT
export const CONTRACT_LAYOUT = {
sections: [
{
key: 'contract_details',
label: 'contract.sections.details',
fields: [
{ key: 'contract_type', type: 'select', options: ['Indefinite', 'Definite'], required: true },
{ key: 'contract_number', type: 'text', required: true },
{ key: 'start_date', type: 'date', required: true },
{ key: 'end_date', type: 'date', conditional: (form: any) => form.contract_type === 'Definite' },
{ key: 'probation_end', type: 'date', readonly: true },
{ key: 'renewal_count', type: 'number', min: 0 },
],
},
],
} as const

COMPENSATION_LAYOUT còn có type: 'dynamic_list' cho danh sách phụ cấp (itemFields), và field có thể tham chiếu nguồn ngoài (source: 'settings.currency'). Thêm field mới vào layout này nghĩa là thêm object vào đúng fields/itemFields — không chỉ tên field như helpdesk, mà cả type, options, conditional nếu cần.

  1. Thêm entry collection (meta.group: "hr", sort đúng vị trí theo phụ thuộc FK — xem thứ tự 28 collection hiện có ở Data model) + field + relation vào hr-schema.ts.

  2. Thêm controller mới trong backend/extensions/hr/src/endpoints/controllers/. HR không có crud.factory.ts dùng chung như helpdesk — mỗi controller hiện có (departments.controller.ts, holidays.controller.ts, assets.controller.ts…) tự viết bằng ItemsService trực tiếp; controller mới nên mirror controller gần giống nhất về shape dữ liệu (vd một bảng con theo person_id thì mirror notes.controller.ts hoặc assets.controller.ts).

  3. Đăng ký route trong endpoints/routes.ts, bọc bằng guard quyền tương ứng (xem Kiến trúc tổng thể cho chi tiết guard/withAccess).

  4. Thêm route BFF proxy trong admin/packages/hr/server/api/hr/<name>/ + composable client trong app/composables/.

  5. Nếu form cần field metadata, thêm short-name vào COLLECTION_MAP (Bước 3 ở trên).

Action module hr khai báo tập trung trong hr-permissions.ts — mỗi action là 1 object { key, displayName, description?, riskLevel }:

app/utils/hr-permissions.ts
{ key: 'leave.approve.dept', displayName: 'Approve Leave (own department)', description: 'Approve leave only for members of departments the user heads', riskLevel: 'medium' },

Thêm action mới ở đây là điều kiện cần nhưng chưa đủ — action còn phải:

  • Được backend thực sự kiểm tra ở route guard (xem withAccess/validateAppAccess trong Kiến trúc tổng thể).
  • Được seed vào ít nhất 1 policy trong hr-policy-seed-step.ts (HR Admin/HR Manager/HR Recruiter/HR Viewer) nếu muốn có sẵn theo role mặc định — nếu không, admin phải tự thêm quyền tay qua Settings → Permissions/Roles. Chi tiết ma trận role→action hiện có ở Thiết lập hệ thống.