Dentolize · HR Module Walkthrough
On this pageBusiness viewTechnical view

Leave, Schedules & Attendance

Business view

This is the everyday HR loop: who's scheduled, who's off, who came in late, and who's owed overtime — with the regional rules Gulf clinics need baked in.

Leave. Staff request leave (a date range, or a few hours for hourly leave types); managers approve or reject; managers can also assign leave directly. Paid leave is capped at the remaining balance; unpaid is uncapped. Annual entitlement can step up with tenure (e.g. 21 → 30 days after N years), blended fairly across the anniversary year. When you approve, a non-blocking coverage warning tells you how many teammates are already off on those dates.

Schedules. Each employee's working week comes from their branch, an offset, or a custom per-day schedule. Reduced-hours periods (Ramadan, summer) shift the day — a later start and a shorter day — company-wide or per branch.

Attendance & overtime. Lateness is measured against the expected start (with a grace window). Overtime only pays if it's approved (when the company requires approval) — unapproved extra time is dropped, not quietly paid. And if someone forgets to check out, the day is capped (default 16h) and flagged auto-closed so one forgotten shift can't invent a fortune in overtime.

Technical view

Leave workflow (resolvers/mutations/actions/hr/hrMutations.js)

  • createLeaveRequest (:205) — target = args.user || requester. Requesting

for someone else needs DO_ALL/APPROVE_LEAVE (:211). Hourly types need start/end on a single day (:228); day types count working days via countLeaveDays (:41, excludes holidays/off-days, half-day = 0.5). Paid balance cap (:242): blocks if requested > remaining — and a paid type with no balance entry is treated as 0 (:247), so it can't be over-booked; unpaid is uncapped. A manager assigning to someone else auto-approves (:258).

  • reviewLeaveapproveLeaveRequest / rejectLeaveRequest (:314, :344)

— tenant-scoped, must be PENDING, and self-approval is blocked: if (leave.userId === session.user.id) throw hr.cannotReviewOwnLeave (:320).

  • cancelLeaveRequest (:348) — own request, or any with

DO_ALL/APPROVE_LEAVE.

Leave balance math (resolvers/queries/actions/hr/hrQueries.js)

computeLeaveBalancesBatch (:77) is the single source of truth. It batch-loads company inputs once and filters per user to kill N+1s. Key pieces:

  • Tenure bump (:108): when PayrollSettings.tenureLeaveEnabled and the

service anniversary at tenureLeaveAfterYears (default 5) is reached, entitlement becomes max(base, tenureLeaveDays) (default 30). In the anniversary year it's a day-accurate blend: base × beforeFraction + bump × (1 − beforeFraction).

  • Accrual (:130): incremental accrual for the ANNUAL type when enabled,

prorated by elapsed periods, capped at the annual total.

  • Remaining (:175): paid day types →

max(0, entitlement + carriedOver − used − pending); unpaid → always 0 (uncapped); floored at 0.

Coverage warning (hrQueries.js:235)

leaveCoverage finds peers in the same department intersected with the target's branch (falls back to branch-only if no department), excludes the target, and returns APPROVED overlapping leaves — { scope, peers, onLeave, overlaps }. Purely informational; approval isn't blocked.

Scheduling engine (utils/hr/schedule.js)

  • Reduced-hours periodsresolveDay (:120): a matching active

SchedulePeriod first applies startDelayHours (later start, also the lateness baseline) then subtracts reduceHoursBy from the end; the day clamps to ≥ 0.

  • Schedule sources (:96): INHERIT_BRANCH, OFFSET (± minutes), CUSTOM

(per-weekday JSON).

  • Defaults (:185, :243): latenessGraceMinutes 10, overtimeDailyThresholdMinutes

0, maxDailyWorkedHours 16, observeBranchHolidays true.

  • SchedulePeriod model: schema.prisma:3979.

Overtime & the checkout guard

  • Overtime (accounting/payroll/attendanceSummary.js): `dayOtPayable =

!overtimeRequiresApproval || rec.otApproved (:92) — when approval is required, unapproved excess is dropped **and** worked time is capped at scheduled minutes so it can't leak into base (:157). Regular-day OT at overtimeMultiplier (1.5) and rest-day/holiday work at restDayOvertimeMultiplier (2) are separate buckets (:197`).

  • Missing-checkout guard (resolvers/mutations/userMutations.js:816): a

session longer than maxDailyWorkedHours × 3600 seconds is capped and autoClosed = true persisted on the Attendance (:868). A stale session force-closed at the next check-in also sets autoClosed (:672).

  • setOvertimeApproval (hrMutations.js:163, gated MANAGE_ATTENDANCE) — sets

Attendance.overtimeApproved (new field at schema.prisma:2245).

Web UI

  • Leave queue: HrTabs.js › LeaveTab (:129) at /hr/leave, with the

Assign Leave modal and the coverage-warning modal. Self-service request: RequestLeaveModal.js:14.

  • Reduced-hours: SchedulePeriods.js (Settings sub-tab).
  • Attendance: HrAttendance.js at /hr/attendance.

Validation

upsertSchedulePeriodInput (inputRules.js:4148), upsertLeaveTypeInput (:4130), upsertHolidayInput (:4139). Numeric ranges (reduce/delay hours 0–12, at least one non-zero) are enforced imperatively in hrMutations.js:132.