Dentolize · HR Module Walkthrough
On this pageBusiness viewTechnical view

Employee Master & Org

Business view

Before you can pay, schedule, or manage anyone, you need a real record of who they are. This is that foundation.

Every user in the clinic gets an employee record — one per person — holding the things HR actually needs but the login account never captured:

  • Employment: department, position, manager (the reporting line that draws

the org chart), job grade, employee type (full-time, etc.), job title, and hire / termination / contract-end dates.

  • Identity & compliance: national ID / iqama, passport, and professional

license numbers — each with an expiry date that feeds the daily reminder.

  • Emergency contact and banking (bank, account number, IBAN, SWIFT) — the

banking is what the WPS file pays into.

You organize people with departments (each has a head) and positions (optionally under a department), and the reporting lines render as a live org chart (reporting tree or by-department).

Records are created lazily — a new user gets a record the first time HR opens their profile — and the old free-text User.job is backfilled into the new job title. Nothing has to be migrated by hand.

Technical view

Data model (packages/prisma/schema.prisma)

  • EmployeeRecordschema.prisma:4302. One-to-one with User (userId

@unique). Holds departmentId, positionId, jobTitle, employeeType (EmployeeType default FULL_TIME), managerId (scalar reporting line), hireDate / terminationDate / contractEndDate (all @db.Date), the compliance IDs + expiries (nationalId+nationalIdExpiry, passportNumber+passportExpiry, licenseNumber+licenseExpiry), emergency contact, banking (bankName, bankAccountNumber, iban, swiftCode), gradeIdJobGrade, and notes. Indexed by company, department, and manager.

  • Departmentschema.prisma:4265. Company-scoped, name unique per

company, optional managerId, relations to positions and employees.

  • Positionschema.prisma:4283. Company-scoped, optional departmentId.
  • JobGradeschema.prisma:4567. code (G1…), level, and

minSalary / midSalary / maxSalary bands.

  • File.expiryDate — added at schema.prisma:3402 (plus documentType) so

uploaded employee documents (contracts, certificates) carry an expiry too.

Mutations (resolvers/mutations/actions/hr/employeeMasterMutations.js)

All gated by MANAGE_HR. Two shared guards: assertInCompany (:8) tenant-scopes any referenced id or throws; duplicateNameGuard (:15) maps Prisma P2002 to a friendly hr.duplicateName.

  • upsertDepartment (:25) / deleteDepartment (:43) — validates the

manager belongs to the company; delete relies on ON DELETE SET NULL, so positions and employee records detach rather than block.

  • upsertPosition (:52) / deletePosition (:69) — same pattern,

validates the department is in-company.

  • updateEmployeeRecord (:84) — the core per-employee upsert, keyed on

userId (:108). Validates the target user and each referenced department/position/manager/grade are in-company. Blocks self-management: if (args.manager === args.user) throw hr.cannotManageSelf (:91). Text fields (RECORD_TEXT_FIELDS, :78) are trimmed with empty→null; date fields (RECORD_DATE_FIELDS, :77) parsed to Date or null; undefined args are skipped, giving partial-update semantics. On create it seeds jobTitle from the legacy user.job (:113).

Honest note: the master-mutation file's header mentions masking, but the masking is not implemented here — it happens read-side (see below and Security & Permissions).

Lazy provisioning & masking (read side)

The employeeProfile query (resolvers/queries/actions/payroll/payrollQueries.js:38) is where a missing record is lazily created (:72) and where sensitive fields are masked (:86): unless the viewer is the employee or holds DO_ALL/MANAGE_HR/MANAGE_SALARY_HUB, the fields bankName, bankAccountNumber, iban, swiftCode, nationalId, passportNumber are set to null.

Org chart & directory (resolvers/queries/actions/hr/)

  • orgChart (hrBoard.js:196) — a flat list of active staff with

managerId/dept/position/jobTitle; the client (OrgChart.js:69) builds the tree, cycle-safe.

  • departments (hrQueries.js:299) — includes _count of positions/employees

and hydrates each department's manager via a company-scoped lookup.

  • positions (hrQueries.js:319) — optional department filter, exposes

employeesCount.

Web UI (packages/clinic-web/src/components/dashboard/SalaryHub/)

  • Directory: HrPeople.js:16 (route /hr/people).
  • Profile: EmployeeProfilePage.js:119 (/people/profile/:id) and the

quick-edit EmployeeProfileDrawer.js:141.

  • Master form: EmployeeMasterForm.js:49 (sends only changed fields).
  • Departments/positions: DepartmentsPositions.js (a Settings sub-tab).
  • Org chart: OrgChart.js:69 (route /hr/org).

Validation (permissions/inputRules.js)

updateEmployeeRecordInput (:3837) — all fields optional/nullable with length caps (jobTitle ≤100, IDs ≤50, notes ≤2000) and an IBAN regex ^[A-Za-z]{2}[0-9A-Za-z]{13,32}$.