New Features
Geofencing
- Allowed clock-in locations — New
/settings/geofencingarea lets Administrators and Executives define the sites where employees can clock in / out. Department heads can also manage geofences — scoped to their own department's users and projects. Every fence carries a name, type (Site / Project Site / Client Site / Home Office / Other), optional address, and a description field visible only to admins. Fences can be toggled Active / Inactive without losing history. - Circle and polygon shapes — A fence can be either a circle (centre + radius) or an arbitrary polygon (ordered list of vertices). Switch between the two with a toggle in the editor. The server stores both forms and runs the right inside/outside test per shape; polygon fences also store a centroid + bounding-sphere radius so list views, nearest-fence queries, and other circle-shaped code paths keep a sensible reference point.
- Interactive map editor — A full-page, split-layout editor with the map on the left and a side rail for metadata. Click "Draw circle" or "Draw polygon" to place a shape by hand; drag the marker or the polygon vertices to refine; "Use current location" drops the shape at the admin's own GPS fix and zooms in. Coordinates + radius inputs stay in sync with the map in real time. When
GOOGLE_MAPS_API_KEYis missing the editor gracefully falls back to coordinate inputs. - Google Maps + OpenStreetMap — Provider toggle in the editor toolbar. Google is the default when a key is configured; OpenStreetMap (Leaflet + leaflet-draw) is available as an alternative — same drawing tools, same shape edit handles, no API key required. Native map controls (zoom, map-type switcher) stay visible and out of the way of the custom toolbar on both providers.
- Configuration + Assignments tabs — The editor splits fence geometry (Configuration) from access control (Assignments) into two top-level tabs, matching the pattern that enterprise IAM and HRIS tools use for resources + permissions. The tab state round-trips through the URL (
?tab=assignments) for bookmarks and back-button navigation. Newly-created fences redirect straight to the Assignments tab so the admin never forgets the follow-up step. - Assignment scopes — Every fence must be assigned to at least one target before employees see it. Supported scopes are Organization (everyone), Department, User, and Project. Resolution order when an employee clocks in is Project → User → Department → Organization; the most specific match wins. Per-assignment flags control whether clock-in, clock-out, or both are enforced — useful for sites where workers must clock in at a specific location but can clock out anywhere. Assignments can be added, edited, and removed from a shared modal matching the rest of the product's dialog treatment.
- Three enforcement modes —
REQUIREDblocks clock-ins that fall outside an applicable fence,OPTIONALlets them through but flags the entry on the compliance dashboard, andLOG_ONLYsilently records the out-of-bounds condition for admin review without surfacing anything to the employee. Mode + GPS accuracy tolerance + maximum acceptable GPS accuracy are all configured from/settings/timetracking. - Live status on the clock-in screen — A full-width status banner above the Clock In / Clock Out buttons on
/time-trackingshows the current state: inside the allowed area ("At HQ"), outside ("1.4 km from HQ"), no GPS signal, or still acquiring. Colours are muted (emerald for good, amber for warning) so the banner reads as information, not an error. Updates in near-real-time as the employee's location changes. When the employee picks a project in the clock-in modal, the banner re-evaluates against that project's fences so they see immediately whether the chosen project is clockable from here. - Rejection modal with nearest-fence guidance — When an attempt is blocked, a modal shows the nearest allowed site by name, the distance to it, and a list of all allowed locations for the action — so employees know where to go without having to call their manager. GPS-accuracy rejections include a specific hint to move outdoors or near a window for a better fix.
- Compliance dashboard + CSV export — Administrators / Executives (and dept heads for their own department) get a paginated list of out-of-bounds clock-ins with 7-day and 30-day summary counts. CSV export uses the same filters and encodes rows safely for spreadsheet consumers.
- Audit trail on every change — Every create, update, delete, assignment change, and blocked clock-in attempt is recorded in the organization's audit log with actor, fence id, and the before/after payload, so compliance has a reviewable trail of every geofence policy decision.
- Geofenced time entries record their fence context — When an entry is created inside a fence, the fence id, the GPS accuracy at the time, and a confidence classification (HIGH / MEDIUM / LOW / UNVERIFIED) are stored on the entry. Historical entries retain this information even if the fence is later renamed, deactivated, or deleted.
- Mobile-ready clock API — Clock-in / clock-out endpoints emit an
X-API-Versionresponse header and return structureddata.errorcodes so future native mobile clients can match on stable identifiers instead of parsing human messages. An offline-queue timestamp protocol (clockInAt/clockOutAtbody fields with a ±15 min future / 24 h past tolerance) lets mobile apps replay clock events queued while the device was offline. - Shared row-action pattern — The
/settings/geofencinglist and the Assignments table both use the same ellipsis-menu row action (Edit / Manage assignments / Delete, teleported dropdown so it escapes table clipping) that the admin dashboard already uses — one interaction pattern, one mental model, regardless of which table the user is on.
Time Tracking & Project Management
- Pay-period lockdown — Admins can set a lockdown date after which time entries with a clock-in before that date can no longer be edited or deleted by employees. A configurable grace window (in days) lets managers close out late-submitted hours after the lock. Only Administrators and Executives can override the lock for corrections; every override is written to the audit log with the entry's date and the lockdown cutoff so compliance has a reviewable trail of every exception. Department heads cannot override.
- Configurable clock time rounding — New time-tracking setting to snap clock-in/out to a configured interval (1–60 minutes) with UP / DOWN / NEAREST direction. Disabled by default. Applied uniformly to live clock-in/out, manual entry, and edits so billable hours line up with the org's rounding policy regardless of how precisely the user clicks.
- Free-form tags on time entries — Each entry can carry up to 10 lowercase tags (32 characters each) for categorisation and filtering. Tags appear in the entry modal as a comma-separated input — paste anything, the system normalises (lowercase, strips punctuation, dedupes). Filterable through the entries API.
- Project money budgets — New
budgetAmountfield on projects complements the existing hours budget with a cost ceiling. Tracked against (hours × hourly rate) of billable entries only — non-billable time never draws down the cost budget. Project detail page renders a second progress bar alongside the hours budget with matching 80/100% colour thresholds. Currency inherits from the organization's default. - Bulk delete time entries — Multi-select in the timesheet list view with a floating action bar to soft-delete up to 200 entries in one request. Fails closed: if any row in the batch is blocked, nothing is deleted. Selection clears when the week changes.
- Restart-timer (duplicate) — One-click duplicate on any past entry clones the project / task / description / notes and starts it as a fresh active timer. Stops any currently-running timer first so the "one active timer per user" invariant holds.
- Branded PDF export for timesheets —
GET /api/reports/timesheets.pdfreturns a per-employee branded breakdown with summary totals, billable split, and an optional money column. Same filters as the JSON report plusprojectId. Admins/Executives get org-wide; department heads are scoped to their own department; employees see their own. New "Export PDF" button on the timesheet top bar. - Expanded timesheet filters and report drill-down — Project, billable, and approval-status filters on the timesheet list, all bound to URL query params so filtered views are shareable. Time-report tables now drill down: clicking a row navigates to the timesheet view pre-filtered to that user / project, so a manager can go from "this person logged 40h" straight to the underlying entries.
- Bulk edit on timesheets — Multi-select in the list view now supports field-level bulk edits alongside bulk delete. Pick any combination of project, billable flag, and hourly-rate override, apply to up to 200 entries in one request. PATCH semantics: only the fields the caller explicitly ticks get written, so one column can be restamped without touching the others. Same fail-closed lockdown and ownership gates as bulk delete.
- Cost aggregation in time reports — The time report now surfaces fully-loaded cost alongside hours for administrators and executives. Rate is drawn from each employee's effective Compensation record at the time each entry was logged, so mid-period raises are picked up automatically. SALARY pay types are normalised to hourly at 2080 hours/year; HOURLY is used as-is. A new Total Cost summary card appears on the report, and the Employee breakdown picks up a Cost column. Entries for employees with no Compensation on file are flagged ("(N unpriced)") so admins can close data gaps rather than silently under-report. Department heads continue to see hours but never cost.
Team Matrix View for Schedules and Timesheets
The calendar view overlays every visible employee's shifts or time entries onto one week × 24-hour grid. With a handful of users it works fine; past 20-30 it becomes unreadable — impossible to spot who's missing entries, who's scheduled for overtime, or what still needs approval. Enterprise timesheet and scheduling tools (Workday, SAP SuccessFactors, UKG/Kronos, ADP, Oracle HCM, Ceridian Dayforce, BambooHR) all solve this the same way: one row per employee, one column per day. This release ships that view alongside the existing Calendar and List views on both /schedules and /time-tracking/timesheets, and makes it the default.
- Employee × day matrix — Each visible employee is a row; the seven days of the current week are columns. Each cell shows the relevant aggregate (hours + status dots for timesheets; shift time range + type for schedules), color-coded by dominant state (green approved / amber pending / red rejected for timesheets; shift-type colors for schedules). A Week total column at the right gives the per-employee totals at a glance. The employee column and header row are sticky, so scrolling horizontally keeps the name and date context in view.
- Click-to-drill — Clicking a cell with one entry or shift opens it directly in the edit modal. Clicking a cell with multiple entries expands the row inline, grouped per day, so you can see every entry side-by-side without leaving the page. Clicking an empty cell opens an Add Entry / Add Shift modal pre-seeded to that employee and date (and a 9 AM default start time for schedules), so admins don't have to re-pick the user they just clicked on. A chevron on the employee name toggles the inline expansion for the whole week.
- Drag-resizable columns — Every column (Employee, each day, Week, Status) has a drag handle on its right edge. Widths clamp between 60 and 600 pixels and persist per-browser in
localStorageso the layout you tuned last week is still there when you reload. Double-click any handle to reset that column to its default; a toolbar button clears every custom width at once. - Density toggle — Compact (36–40px rows) / Normal (52–60px) / Spacious (72–84px). Remembered across sessions. Compact hides secondary lines (department name under employee, shift-type label) so a 30-person team fits on a single screen; Spacious adds breathing room around the data when you're presenting or reviewing one team closely.
- Sort and filter — Sort by name, week hours ↑/↓, pending count, or missing days (timesheets) / unscheduled days (schedules). Filter chips at the top toggle between All / Missing / Pending / Approved / Rejected (timesheets) or All / Unscheduled / Scheduled / 40h+ (schedules), each with a live count so you can see at a glance how many employees fall into each bucket. A text search narrows by name, email, or department. All three controls persist.
- Holidays and leave aware — Holiday cells are tinted green with the holiday name in the header; cells where the employee is on leave show the leave type label instead of a blank, so "no shift scheduled" is visibly distinct from "scheduled off". Open shifts (schedules without an assigned user) render with a muted "Open Shift" label so they're easy to spot in the matrix.
- Calendar and List views still available — The top bar's toggle is now three buttons instead of two: Team (default) / Calendar / List. The URL
?view=query parameter accepts all three values, so existing deep-links to the calendar or list view continue to work unchanged, and a team-view link can be shared with colleagues.
Approvals & Reports UX Redesign
- Approvals dashboard overview —
/approvalsis now a dashboard-style overview. Four edge-joined KPI stat cards (Total Pending, Leave Requests, Expense Reports, Timesheet Entries — hidden when time tracking is off) each link to their detail queue. Below, a unified "Pending Workload" card shows the big-number total + a horizontal bar breakdown per queue, followed by a Review Queues table (Queue badge, Status, Pending count, Review link). Status dot in the header pulses amber when items are waiting and turns green when the queue is clear. A per-queue skeleton covers the pre-fetch window so the cards never flash "0" before counts arrive. - Per-type approval pages — Three new dedicated routes:
/approvals/leave,/approvals/expenses,/approvals/time. Each follows a consistent layout: breadcrumb + page header, search + filter dropdowns + export toolbar, a data table with<colgroup>column widths, employee avatar cell, colored status / type badges, row-click detail slideover, action dropdown menu per row, reject modal with a dedicated header / body / footer + live character counter + keyboard shortcut hint, and a bottom-pinned pagination footer with rows-per-page selector and prev/next navigation. - Reports dashboard overview —
/reportsis now a dashboard overview. Five edge-joined KPI stat cards (Leave requests, Expenses total, Time hours, Active projects, Scheduled shifts — the last three hidden when time tracking is off) link to their detail pages and surface live numbers fetched in parallel. A two-column chart row shows the Expense Trend (3 or 6 months, area chart with fill) and Leave by Employee (top 5/10/15/20, bar chart rendering compact initials on the x-axis with full names in the hover tooltip — 10-user cap enforced on the API fan-out to prevent chart breakage on large orgs). A three-column chart row below (gated on time tracking) shows Hours Tracked over the last 6 months, Project Hours as a stacked area chart where each project is rendered in its own color (configurable top 3/5/7/10), and Scheduled Shifts by month. Each chart has a settings gear with contextual options (period, metric, max items) and a primary-colored "Done" button. An "Available Reports" table (responsive: desktop table, mobile card list) links to each sub-page. - Per-type report pages — Five new dedicated routes:
/reports/leave,/reports/expenses,/reports/time,/reports/projects,/reports/scheduling. Each wraps the existing tab component inside a consistent shell (breadcrumb + page header). Permission gates and time-tracking redirects preserved. - Tab → query-param navigation — Hash-based tab navigation on
/approvalsand/reports(e.g.#leave) replaced with query params (?tab=leave). URLs are shareable, round-trip through Vue Router, and cooperate with existing drill-down query params (e.g.?tab=time&userId=…). A shareduseTabParamcomposable handles the state, URL sync, and the async-validation edge case wherevalidTabsdepends on settings (e.g.isTimeTrackingEnabled) that load after the initial render. - Shared UI primitives for approvals and reports — New
components/common/directory:PageHeader,PageTabs,PageToolbar,StatCard,StatusBadge,SearchInput,FilterSelect,FilterBar,SlideOver,ActionMenu. The stat card uses an edge-joinedfirst:rounded-l-lg last:rounded-r-lgstrip pattern so a row of cards reads as a single unit. The action menu teleports its dropdown to<body>with fixed positioning so it escapes any clipping ancestor (table wrappers, slideovers). The slideover is a right-aligned 400px drawer with backdrop click / Esc to close. All built in raw Tailwind against the existing CSS variables so they match the rest of the product's design system. - Cost flow fix in time reports — The Reports Time tab was dropping
cost,totalCost, andentriesMissingRatebetween the API response and the chart/card render paths on the previousby_employeemerge. Refactored the merge logic out intoutils/time-report-shape.tswith proper types. The "(N unpriced)" hint now renders correctly on the Total Cost summary card when entries lack effective compensation, and per-employee cost flows through to the table column. - Full-width page treatment —
/approvals,/reports, and all 8 sub-pages drop themax-w-7xlcap and stretch to the full content-area width, matching the/scheduleslayout so wide tables don't leave empty side margins on large monitors. - Pass-through of avatar role colour + badges — Every new approval/report surface uses the existing
<UserAvatar>component, so an employee's initials circle, role-tinted colour, and Administrator/Executive crown/star badge render identically across/users,/calendar,/approvals, and/reports. No new palette, no drift.
Settings Search
- Global settings search bar in the top header — On every
/settings/*route, the top toolbar renders a compact Stripe-style search input (left-aligned with the page H1). Press/anywhere on a settings page to focus it. Hidden on mobile to keep the toolbar usable. - Deep-linkable individual settings — Search results resolve to
path#anchor(e.g. "time zone" →/settings/general#time-zone, "auto clock out" →/settings/timetracking#auto-clock-out). Anchor IDs added across ~30 settings components covering General, Time Tracking, Expenses, Carry Forward, Leave Types, Departments, Holidays, Email, Policies, Documents, Training, Performance, Onboarding/Offboarding, Company Directory, SSO, Integrations, QuickBooks, Branding, Security, Support, Danger Zone, Password, 2FA, Notifications, Connected Apps, Guided Tours, and Trusted Devices. Landing on a deep link smooth-scrolls to the target element and paints a soft primary-colored focus ring that fades after ~2.6s. - Hand-curated search index with keyword aliases —
composables/useSettingsSearch.tsdefines a static index of ~75 entries (one per settings page + one per prominent sub-setting) with label, description, parent-page, icon, hash, and keyword aliases. Scoring prioritizes exact label match (1000) > label prefix (500) > keyword exact (400) > label substring (300) > keyword prefix (200) > keyword substring (150) > description match (75) > multi-term all-match (100). Case-insensitive; multi-word queries require every term to appear somewhere in label/description/keywords. - Billing-aware lock badges in results — Each result carries
locked,planLabel,isAddOn. Locked entries render a🔒 Pro/🔒 Business/🔒 Enterprise/🔒 Add-onpill matching the badge style on/settingsoverview cards — so users can see a gated feature exists and know which plan unlocks it before they click. Locked rows stay in the list (for discovery) but rank lower via a-25score penalty. Uses the sameisFeatureLocked+getLockedPlanLabelhelpers as/billingso the labels stay consistent across the product. The bar callsloadSubscription()on mount to ensure badges reflect the org's actual plan. - Role-aware visibility — The search only indexes
visibleItemsfromuseSettingsNavigation, so an employee searching for "audit logs" gets no result (the page is admin/exec only), while an administrator does. Department heads see onboarding/offboarding entries. - Tabbed-page auto-switch — On pages with tabs (Security, Connected Apps), navigating to
#audit-logs/#personal-webhooks/ etc. now auto-selects the matching tab via awatch(route.hash)wired to a static anchor-to-tab map, so a result click lands on actual content rather than the default tab. - Keyboard navigation — Arrow keys move the active row, Enter commits, Esc closes,
/global shortcut focuses the input (ignored when the user is already typing in another field). - First-paint layout stability — The authenticated layout's outer wrapper animated
lg:pl-[68px]↔lg:pl-[260px]when the sidebar collapsed state hydrated from localStorage, dragging the header — including the search bar — left/right for a split second on every refresh. Suppressed the transition on the very first paint via alayoutReadyflag so the layout snaps instantly; sidebar-toggle animation still works for later user interactions.
Guided Tours Overhaul
- Redesigned popover — Each tour step now leads with an icon chip, a clearer title, and a short body. Optional "Tip" strips surface pro moves and shortcuts; optional keyboard-shortcut chips (e.g.
/,A,R) appear where relevant. Subtle entrance animation, a gentle pulse on the highlighted element, full dark-mode parity, and a viewport-aware width so the popover never overflows on small screens (respectsprefers-reduced-motion). - Rewritten tour copy across the app — Every tour — Welcome, Calendar, Time Tracking, Timesheets, Schedules, Expenses, Approvals, Reports, Users, Documents, Billing — replaces the old label-style descriptions ("Click here", "View data") with outcome-led coaching that names the actual job ("One tap to clock in", "Find anyone, fast", "Select many, change once"). Welcome ends with a concrete next action.
- Five new tours — Geofencing, Projects, Profile, Onboarding, Training. Each 3–4 steps, auto-opens once per user on first visit to the matching page, replayable any time from Settings → Guided Tours. The Geofencing tour walks through the map editor, the Google / OSM provider switch, and the assignments step that most admins miss.
- "Don't show tours" opt-out inside the popover — A subtle text link on step 1 of every tour lets a user dismiss every tour they haven't seen yet, without navigating to Settings. Marks them all complete server-side and locally; individual tours can still be replayed later.
- Smarter placement — Tours now scroll the highlighted element into view only when it's actually off-screen (with a 64px top margin for sticky headers), anchor the popover directly next to the target instead of drifting to the bottom of the page, and size themselves to the viewport so narrow phones don't get a clipped card. Fixed a positioning regression where the popover could drift hundreds of pixels from its target on tour start.
Page Redesigns & Data Export
- Clean flat list view for timesheets — New admin-audit-log-style table replaces the day-grouped list view. Shows all entries across the selected range in a single flat table with columns: Date, Employee (UserAvatar + department), Project (color dot + task), Description, Time (in→out), Duration, Status (CommonStatusBadge), and Actions (CommonActionMenu). Server-side pagination (50 rows/page default) keeps the API efficient. Checkbox multi-select supports bulk edit and bulk delete. The entries API endpoint now returns summary aggregates (approved/pending/rejected minutes via
groupBy) alongside the page so header stats stay accurate across pagination. View mode (calendar/list) persists in the URL query param (?view=calendar/?view=list) — no flash on refresh. - AG Grid export page for timesheets — Clicking the export button navigates to
/time-tracking/timesheets/exportwith the active date range and filters as query params. The page uses AG Grid Community (ag-grid-vue3) with: four theme variants (Quartz / Alpine / Balham / Material) auto-switching to dark mode, custom Excel-style checkbox set filter per column (Select All / Deselect All with search — replaces the Enterprise-only Set Filter), Columns dropdown to show/hide columns with live column re-fit, quick filter search across all columns, pinned bottom totals row that recalculates on every filter change, ResizeObserver for responsive column sizing on window resize, and CSV export that respects current filters and visible columns. All data loads in a single infinite-scroll grid — no pagination. - PDF preview in new tab — PDF export now opens in a new browser tab instead of auto-downloading, so users can review before saving. The PDF also now respects the date range filter (was previously always the current week). Logo alignment in the PDF header is fixed — vertically centered with the brand name text using proper pdf-lib baseline math.
- List view + export for schedules — All timesheet list-view features ported to
/schedules: flat table (Date, Employee, Department, Shift Type badge, Time, Break, Duration, Notes, Actions), calendar/list toggle persisted in URL, AG Grid export page at/schedules/exportwith the same tooling, Export CSV and PDF buttons in the top bar, sidebar and date range hidden based on view mode, stat chips updated to uniform neutral style, scrollbar auto-hide in list view. Client-side pagination since the schedules API returns all shifts at once. - Users page redesign —
/usersredesigned to match the admin dashboard's users table pattern. Modal-based filter replaced with inline filter dropdowns (Department, Role, Status) always visible in the toolbar. Table upgraded from a 12-column grid layout to a proper<table>with 8 columns: Name+Email (UserAvatar cell), Department, Role (StatusBadge), Job Title, Status (StatusBadge with Offboarded variant), Last Login, Joined, Actions (CommonActionMenu preserving all existing actions). Client-side pagination with rows-per-page selector (10/20/50). CSV export of filtered users. Full-width layout matching/approvals. AG Grid export at/users-export. - Uniform stat chip styling — Timesheet and schedule top bars use consistent neutral-tone stat chips (
bg-muted/60 ring-1 ring-border) instead of rainbow-colored pills, matching the admin dashboard pattern across all pages. - Responsive top bar wrapping — Both timesheet and schedule top bars use
flex-wrap gap-y-2so stat chips, view toggle, and action buttons wrap gracefully on narrow viewports instead of overflowing. - Sidebar visibility gating — Employee sidebar toggle button hidden in list view on both timesheets and schedules (sidebar is only relevant for the calendar grid).
- Date range visibility gating — Date range filter row hidden in calendar view on both timesheets and schedules (calendar is fixed at 7-day columns).
- Unified Export dropdown — New
CommonExportDropdowncomponent replaces separate CSV/PDF/Excel buttons with a single "Export" button + dropdown menu showing "Export in Excel", "Export in PDF", and "Export as CSV". Applied across all pages: timesheets, schedules, all 3 approval pages, users, expenses. - AG Grid export pages for approvals — Three new export pages:
/approvals/leave-export,/approvals/expense-export,/approvals/time-export— each with the full AG Grid tooling (theme picker, column visibility, custom set filters, quick filter, pinned totals). - Client-side branded PDF export — New shared utility
utils/export-pdf.tsgenerates branded PDFs client-side using pdf-lib. Fetches org logo + company name from/api/branding(which now falls back toSMTP_LOGO_URLenv var). Logo + brand name header on page 1, proper page breaks, footers with page numbers. Used by all approval pages, users, and expenses for PDF export. - Manage Departments modal redesign — Compact header/body/footer sections matching the approvals reject modal pattern. Contextual icon badges per department (primary when active, muted when inactive). Clickable status badges. Inline add form with 2-column grid.
- Expenses page redesign — Full-width layout, admin-style table, inline filter dropdowns (status, sort). Nav tabs removed — Reports and Approvals moved to toolbar action buttons with pending badge. ExpenseTableRow actions replaced with CommonActionMenu (3-dot ellipsis). AG Grid export at
/expenses-export. - Expense approvals redesign — Admin-style table replacing grid-cols-12 layout. CommonActionMenu per row. Reject modal with header/body/footer sections, contextual chip, character counter. Pagination footer.
- Expense reports redesign — Admin-style table, CommonPageToolbar with inline filters, edge-joined CommonStatCard strip (Total Claims, Purchases, Per Diem, Travel, Grand Total), pagination, branded PDF via shared utility.
- Reports overview fixes — All 5 charts use settings gear overlay with period options (3 months / 6 months / YTD). Period badge next to gear button. Fixed expense chart month bucketing (departureDate not createdAt). Fixed schedule chart date range. Fixed leave count (org-wide via leaves-data endpoint). Stat card labels updated to "total" instead of "this month".
- Leave report tab redesign — Transformed from "report generator" (download cards) into a data dashboard. Admin-style table showing actual leave requests with UserAvatar, leave type badges, status badges, CommonActionMenu. Server-side pagination with aggregate stats. Leave distribution section preserved.
- Expense report tab redesign — CommonPageToolbar with search and inline filters. Edge-joined CommonStatCard strip. Admin-style table with CommonActionMenu and CommonStatusBadge. Client-side search. Pagination. PDF export via shared utility.
- Time report tab redesign — Removed all charts (Area, Bar, Line, Donut). Admin-style data table with individual time entries, server-side pagination, CommonStatusBadge, CommonActionMenu. Stats use server aggregates.
- Projects report tab redesign — Stat card strip, CommonPageToolbar with search and inline filters, admin-style table with project stats and progress bars, CommonActionMenu, pagination, export.
- Scheduling report tab redesign — Stat card strip, CommonPageToolbar, per-shift detail table with shift type badges (REGULAR=primary, FLEXIBLE=info, ON_CALL=warning, HOLIDAY=success, WEEKEND=neutral, OVERNIGHT=error), pagination, export.
- Projects list page redesign — Full-width, admin-style table with project color dots, status badges, progress bars, CommonActionMenu, pagination footer, export dropdown.
- Documents page redesign — Full-width, admin-style table with CommonActionMenu and CommonStatusBadge. New
CommonSegmentedTabscomponent replaces underline tab bar — pill-shaped toggle group with sliding primary-color indicator, ResizeObserver for responsive positioning. URL sync changed from#hashto?view=query params.
Improvements & Fixes
UX Refinements
- Org chart list view matches the /users table styling — The
/users/orgchart?view=listPeople page has been realigned with the admin-dashboard<table>treatment used on/users: a single sharedCommonPageToolbarwith search + Department / Employment Type / Status inline filters, a proper<table>with<colgroup>column widths and rounded header cells, inlineUserAvatarcells,CommonStatusBadgepills for the employment-type status (Full-time / Part-time / Contract / Intern / Temporary / Seasonal), and the standard rows-per-page pagination footer (10 / 20 / 50). Employment-type tones are mapped through the shared StatusBadge palette so colors stay consistent with the rest of the product. Column visibility (Employee # / Department / Location) is preserved as a trailing toolbar action. Mobile switches to a card layout. - Current-session indicator on /settings/trusted-devices — The trusted devices page now shows a dedicated "Current session" card at the top with your parsed browser + IP (e.g. "Chrome on macOS · 192.168.1.10") so you always know which device you're looking at — even when none of the stored trusted-device rows match. Per-row matching uses two signals: a cookie-based hash match flags "This device" with high confidence, and a heuristic fallback (single trusted row whose stored user-agent and IP both equal the current request) flags "Likely this device". Ambiguous matches are intentionally left unlabeled rather than guessed. The matched row is sorted to the top of the list so the device you're on is always the first thing you see.
- Persisted day-row heights on the schedule grid — When a user drags the row-resize handle on
/schedulesto make a day taller or shorter, the new height now survives page reloads instead of snapping back to the 80px default. Heights are keyed by day index (Mon → Sun), clamped to the same 60–400px range the drag handle enforces, and written only when the drag ends (not during every mouse-move tick). Double-clicking a handle still resets that row, and when every row has been reset the storage entry is cleared entirely. - Leave history no longer shows phantom -1 day deductions for non-deducting types — The History table on
/users/:id?tab=timeoffwas rendering "-1.0" for every leave regardless of whether the leave type actually drew from a balance bucket. Work-from-home, Meeting, Training, and other tracking-only types were reporting balance hits that contradicted the Leave Balance card on/calendar/:id. A new sharedutils/leaveBucket.tshelper mirrors the same ANNUAL / SICK / NONE classification used server-side (including the legacy fallback for rows that predate thedeductionBucketcolumn), and the History cell now shows "—" with an explanatory tooltip for leaves that don't deduct. - Mark all tours completed in one click —
/settings/toursgains a "Mark All as Completed" button next to "Reset All Tours". Admins and returning users who aren't interested in replaying the onboarding walkthroughs can now dismiss every visible tour at once instead of triggering each one to collect its completion flag. The button disables itself when every visible tour is already marked complete, so it can't be accidentally re-run.useGuidedTour.completeTours(ids)is the underlying helper for any future flow that needs to mark tours complete in bulk.
Bug Fixes
- Notification settings (
/settings/notifications) crashed for orgs with legacy defaults — When an organization's saved notification defaults were stored in an older shape, the page would throwCannot read properties of undefined (reading 'enabled')instead of rendering. The server endpoint now normalises partial payloads through the sharedresolveNotificationPreferenceshelper, and the client merges fetched values over the form's defaults so a missing nested key can no longer blow up the view. - Password manager hints on settings — Added the right
autocompleteattributes to every password / client-secret input across settings (change password, SMTP password, SSO client secret). Browsers no longer warn in the console, and password managers can now offer the correct suggestion for each field. - Due-date calculation ignored BEFORE/AFTER direction — Template tasks configured as "7 days before hire date" were being created with due dates after the hire date (e.g. hire May 1 → laptop task due May 8 instead of Apr 24). The frontend import path in
pages/users/[id]/index.vuewas always adding the offset; it now defers to the server which honorsdueDaysDirection: 'BEFORE' | 'AFTER'with UTC date arithmetic. - "Start Onboarding" silently picked the first template — Clicking Start Onboarding auto-selected whichever template was alphabetically first with no way to choose. Added a picker modal that lists every eligible task list (or "Start with no tasks (add later)") before the instance is created.
- "Import Task List" merged into an existing list — Selecting IT Setup when HR was already attached appended IT tasks into the HR card, erasing the grouping. Import now calls
POST /api/onboarding/instanceswith the template ID, creating a separateOnboardingInstancewith its owntemplateName/templateIconsnapshots so each imported list renders as its own card. - Already-imported templates were re-selectable — The Import and Start modals showed every template every time, allowing duplicate imports of the same list onto one user. Both modals now use
availableTemplatesForType/availableTemplatesForStartcomputeds that filter out templates already present in the user's instances. - Employees could mark their own onboarding tasks complete — The subject user (the employee being onboarded) was able to tick off their own checklist items, including completing compliance tasks that require admin/HR review. Completion is now restricted to administrators, executives, the department head of the employee's department, or the explicit task assignee. Employees keep read access to see what's coming up.
- Signed documents did not auto-complete their linked tasks — When an employee signed a document attached to an onboarding task, the task stayed in
PENDINGstatus until someone manually ticked it. AddedautoCompleteTasksForSignedDocumentwhich fires from the document sign handler, matches tasks by(documentId, instance.userId), flips status toCOMPLETED, rolls up instance status, and dispatches the task-completed notification. Works for both the direct per-user clone and the source-template document cases. - Task list modal was too small and cut off the icon grid — The "New Task List" modal was
max-width="sm"which made picking an icon require scrolling a 47-item grid inside a 256px container. Bumped tomax-width="2xl"with a two-column layout (Name + Department side-by-side) and the icon picker given full breathing room below. - Shrine icon rendered as a blank square —
lucide:shrineis not a valid Lucide icon name and showed empty in the picker. Removed it. Added ~85 additional icons covering HR & onboarding (user-plus, id-card, handshake, badge-check), IT & equipment (laptop, headphones, server, wifi, printer), access & security (key, lock, fingerprint, shield-check), finance & payroll (banknote, calculator, receipt), communication (mail, phone, video, megaphone), documents (file-signature, file-check, folder), facilities (building-2, factory, truck), sales & analytics (target, trophy, rocket, pie-chart), and common actions (check-circle, bell, settings). - Department heads could not manage their own team's task lists — Only ADMINISTRATOR and EXECUTIVE could CRUD onboarding templates, forcing every HR/IT/Sales lead to route changes through an admin. Added
OnboardingTemplate.departmentId(nullable). Department heads can now create, edit, delete, and manage task lists scoped to their own department. Templates withdepartmentId: nullstay admin/exec only. Enforced via the newserver/utils/onboarding-access.tshelper across all template, taskdef, instance, and per-user-task endpoints. - Department heads could not see Onboarding / Offboarding in /settings — The settings sidebar and page wrappers had hard
adminOnlygates. Introduced adeptHeadAllowedflag onSettingsNavItemandSettingsPageWrapper. Onboarding and Offboarding now opt in; department heads see both entries in their settings sidebar and can open either page. - DH saw all templates on the settings page — After unlocking the page for DH, they were seeing org-wide (admin-only) task lists they couldn't edit. Added
?manageableOnly=truetoGET /api/onboarding/templates. The settings page passes this flag, so a DH only sees their own department's templates. The user-page Import/Start flow still fetches the full visible set (org-wide + dept) because DHs should be able to import admin-built lists for their team. - 403 on /api/onboarding/packets for department heads — Opening
/settings/onboarding-templatesas a DH crashed the data load because the packets endpoint is admin-only.loadData()now skips the packets fetch entirely for non-admins and hides the "New Hire Packet Templates" tab. If a DH lands on?tab=packetsdirectly, it coerces totasks. - Misleading "All departments (admin-only)" dropdown label — The department selector in the New/Edit Task List modal didn't make clear which roles could manage the list. Replaced with "Who can manage this list" — "Administrators & Executives only" for no-department, and "{Dept name} department — Administrators, Executives & {Dept name} Head" for each option. Plus explanatory helper text below: "Administrators and Executives can always manage any list. Picking a department additionally grants that department's head manage access. Employees and department heads of other departments cannot edit the list." For DH callers, the dropdown is auto-pinned to their own department and disabled.
- Task list cards had no spacing between them — Multiple onboarding/offboarding instances rendered flush against each other with no gap. Added
mb-4 last:mb-0to each instance card on both the Onboarding and Offboarding tabs. - Add Task button disappeared from the user's onboarding tab — When the flow was restructured to support multiple task lists per user, the header-level "Add Task" button (which ambiguously targeted the first instance) was replaced with a tiny
+icon that was easy to miss. Each task list card now shows a clearly labeled "+ Add Task" button next to Remove, targeting that specific list unambiguously. - Per-user Add Task modal missed fields that existed in the template modal — The modal was missing Task List selector (required now that multiple lists exist per user), attached-document picker, and "Require signature before complete" option. Rebuilt to match the settings "Edit Task" layout: Task Name, Task List, Assign to, Category, Due Date, Description, Attach Document + signature gate. Intentionally omits the template-only "Import this task when onboarding (All/Some)" and "Update existing employee tasks" fields because per-user tasks only ever apply to one user.
- Assigned Role dropdown in the per-user Add Task modal was redundant — The modal showed both an "Assign to" employee picker AND an "Assigned Role" fallback dropdown, which was confusing for per-user tasks. Removed — the server still defaults to
'HR'when not provided, and the template flow keeps the full role picker since template tasks may be authored without a specific user in mind. - Task completion emails only went to the assignee — When a task was marked complete (or auto-completed by a document signing), no one in HR/management learned about it. Added
sendOnboardingTaskCompletedNotificationwhich emails administrators, executives, AND the task assignee (deduped). Includes the employee name in the subject line and body. - Due-date reminders only went to one person — The 1-day-before / 3-day / 7-day reminder cron sent to the assignee if one existed, otherwise to the employee. Now fans out to all administrators + executives + the task assignee (deduped). Subject line includes the employee name so admins know whose onboarding it belongs to. The employee being onboarded is intentionally not reminded because they can't mark their own tasks complete.
- Task removed → no notification — Deleting a task from an active instance silently disappeared the row. Admins/execs/DH now receive an email + in-app notification with the employee name, task title, assignee, and who performed the removal. Sent synchronously before the delete so details can still be read.
- Instance removed → no notification — Same for removing an entire task list from a user's profile. Admins/execs/DH are now notified with the list name, task count deleted, and who removed it.
- No audit trail for onboarding/offboarding settings changes — Template and task mutations were not appearing in
/settings/security → Audit Logs. AddedlogOnboardingAudithelper that writesAuditLogentries with IP + user-agent for every CREATE/UPDATE/DELETE onONBOARDING_TEMPLATE,ONBOARDING_TASK_DEF,ONBOARDING_INSTANCE, andONBOARDING_TASK. UPDATEs include before/after snapshots. Task PATCHes distinguishreason: STATUS(marking complete/incomplete) fromreason: EDIT(field change) so the audit feed is actionable. - No way to reorder tasks within a task list — Once a task was added, its sort order was locked unless you deleted and recreated it. Each task row in
/settings/onboarding-templatesand/settings/offboarding-templatesnow has a grip handle and isdraggable="true". Drag to reorder;sortOrderis renumbered locally and each changed row is PATCHed. Optimistic UI — reverts on error. - Email CTA buttons used a hardcoded purple — All onboarding/offboarding email action buttons ("View My Tasks", "View Task", "View Onboarding", "View Profile") rendered
#4f46e5regardless of the organization's white-label branding. AddedprimaryColortoEmailConfigwhich readsCustomDomain.primaryColor(falling back to#3B82F6). All eight CTA buttons across the onboarding email templates now render the organization's brand color. OnboardingInstancelost its identity if the template was edited or deleted — When an admin renamed or deleted a template, every instance using it suddenly showed "Onboarding" as its card title or crashed on dereference. AddedtemplateName+templateIconsnapshots onOnboardingInstance. Each imported/started list keeps its own identity even after the source template changes. The UI prefers the snapshot (inst.templateName) over the live relation.canCompleteTaskin the frontend allowed the instance owner to complete their own tasks — Mirrored the backend fix in thetaskDetailcompletion gate. The modal's "Mark Complete" button now requires admin/exec, the explicit assignee, or a matching department head — not just "is this your own instance".- Saving a time-tracking setting could fail with a 400 after the page was reloaded — Toggling any switch on
/settings/timetracking(for example "Require Location") sometimes responded with "autoApproveAfterDays must be a number" and rolled the UI back to the previous state. The page hydrates its form by merging the full GET response into local state, so nullable numeric columns were being echoed back asnullon save — which a strict type check was then rejecting. The save handler now treatsnullthe same as an omitted field (skip), so the toggle-and-save flow works regardless of which optional settings happen to be unset on the row. Real type errors (strings in numeric fields, out-of-range numbers, invalid dates, unknown enum values) still fail with 400 as before.
Security & Compliance
- Department-head scope enforcement — A department head cannot use a template scoped to another department (enforced on
POST /api/onboarding/instanceswhen they try to pass atemplateIdbelonging to a different department). They also cannot re-scope a template between departments via PATCH. - Template-move authorization — Moving a task definition between templates (via
PATCH /api/onboarding/templates/[id]/tasks/[taskDefId]with a newtemplateId) now re-runsassertCanManageTemplateagainst the target template's department. A DH cannot move a task into a template they don't own. - Document auto-complete cross-user safety —
autoCompleteTasksForSignedDocumentfilters byinstance.userId === signerUserId, so signing a document only completes the signer's own tasks — never another user's task that happens to reference the same source document. - DH departmentId self-pin on create — When a department head creates a new task list, the backend always pins
departmentIdto their own department regardless of what the request sent, closing the gap if the UI-level dropdown is manipulated. - Tenant isolation on time-entry and project mutations — Foreign-key fields written on time-entry create/edit (
projectId,taskId) and project create/edit (managerId) are validated against the caller's organization at every write path. - Department-head scope on timesheet PDF export — When a department head runs the timesheet PDF, results are constrained to their department's users on every code path; an out-of-department
userIdfilter returns 403 instead of an empty report so the caller knows the request was denied. - Project financials visibility — Hourly rates, budget amounts, and billed-amount stats on projects are restricted to Administrators, Executives, and Department Heads. Regular employees can still see hours and the hours budget on projects they're working on; the monetary values are stripped from the API response entirely for non-privileged callers.
- Lockdown-override audit trail — Every admin/exec edit, create, or delete that bypasses the pay-period lockdown is recorded in the audit log with the entry's clock-in and the lockdown cutoff. Audit writes are fire-and-forget so a failed log never rolls back the user-visible action — the override itself still happens, but compliance always sees it.
- Hardened external fetch in PDF generator — The optional org-logo embed in PDF reports validates the URL through the same SSRF guard used by webhook delivery (HTTPS only, blocks private and link-local addresses), with a fetch timeout and response-size cap. A broken or missing logo never fails the report.
- Stricter input validation on time-entry edits — Break duration must be a non-negative finite number and cannot exceed the entry's total span. Invalid clock-in / clock-out ranges are rejected up front so downstream billing math never sees a negative duration.
- Strengthened input validation across all time-entry endpoints — All mutation endpoints (create, edit, bulk update, duplicate, clock) now enforce stricter type checks, length limits on free-text fields, and range constraints on numeric settings. Invalid or out-of-range values are rejected early with clear error messages.
- Improved multi-tenant isolation on write paths — Foreign-key references passed during create and update operations are now validated against the caller's organization before persistence, closing potential data-integrity gaps in multi-tenant environments.
- Tighter scoping for department-level roles — Department heads are now consistently constrained to their own department's data across reports, PDF exports, and list endpoints. Org-scoped lookups ensure role-based filtering cannot be bypassed via direct API calls.
- Hardened settings update endpoint — Boolean, numeric, and enum fields on the time-tracking settings endpoint are validated by type and range before persistence. Numeric settings enforce documented min/max bounds.
- Soft-deleted entries excluded from aggregations — Report endpoints and project statistics queries now consistently exclude soft-deleted records, ensuring accurate totals and preventing stale data from surfacing in dashboards.
- Sanitized custom CSS in branding — The branding endpoint strips potentially dangerous CSS constructs (dynamic URLs, imports, expressions, script bindings) from custom CSS before serving it to clients.
- Bumped vulnerable transitive dependencies — Updated
follow-redirects(1.15.11 → 1.16.0),protocol-buffers-schema(3.6.0 → 3.6.1),dompurify(3.3.3 → 3.4.0), andhono(4.12.12 → 4.12.14) via npm overrides to resolve Dependabot security advisories.
Performance
- Search debounce (300ms) —
CommonSearchInputnow debounces emit by 300ms, preventing re-filtering on every keystroke across all pages. Single fix, global impact. - Leaves endpoint server-side pagination —
GET /api/reports/leaves-datanow supportspage/limitparams. Returnspaginationmetadata +summaryaggregates (approved/pending/rejected counts + totalDays viagroupBy). Runscount,groupBy, andfindManyinPromise.allfor zero extra latency. - LeaveTab server-side pagination — Stats show real totals from server aggregates (accurate across ALL data, not just current page). Page changes trigger re-fetch. Filters reset to page 1.
- Projects N+1 query eliminated — Replaced per-project
Promise.all(projects.map(aggregate))(N individual queries) with a singletimeEntry.groupBy({ by: ['projectId'] })query mapped back by project ID. O(N) → O(1). - TimeTab server-side pagination — Uses
serverTotalfor real entry count andserverSummaryfor accurate hours across all pages (not just current page of 50). - Branding endpoint SMTP fallback —
/api/brandingnow falls back toSMTP_LOGO_URLenv var when no white-label logo is configured, ensuring PDF exports always have a logo. - Reports overview now loads in one pass —
/reportswas firing roughly seventy requests on mount to paint five stat cards and six charts. The summary strip now calls a single newGET /api/reports/summary-statsendpoint that returns all five KPI numbers via DB aggregates; the leave-by-employee chart reads the existing/api/reports/leaves-dataendpoint once and groups clients-side instead of fanning out per user; the hours-tracked chart fetches its six monthly buckets in parallel instead of awaiting each in turn. Typical admin loads drop from double-digit seconds to sub-2s on a 50-user org, and the fan-out no longer scales with team size. - Approvals overview uses one counts endpoint — The
/approvalspage used to mount three hidden tab components purely to compute the "N pending" numbers on its stat cards — three full queue fetches for three integers. A newGET /api/approvals/countsendpoint returns{ leave, expense, time }via DB counts in one hop; the hidden tabs are gone. Drill-down pages still own their own detailed fetches when the user actually clicks through. - Faster time approvals — Approving or rejecting a timesheet entry on
/approvals/timeused to re-fetch the whole queue just to see the row disappear. It now removes the row locally on success and only reverts on error, so the click feels instant regardless of how many pending entries there are./approvals/leaveand/approvals/expensesalready worked this way; this brings timesheets in line. - Dashboard reads in parallel — The home dashboard's user-row query used to walk through five independent reads in sequence (leaves for the visible range, the year-to-date balance, previous-year leaves for carry-forward, holiday overrides, org holidays). They now run in a single
Promise.all. No query shape changes — the cold-render latency on large orgs drops proportionally to the slowest query instead of the sum. - Schedule auto-generation cut from hundreds of queries to one — When a team has the default-schedule feature turned on, loading
/scheduleswas doing a per-employee-per-dayfindFirstto decide whether to create a shift. A typical 50-person, 5-day week ran 250+ individual queries. A singlefindManynow covers the whole range and the existence check happens against an in-memoryMap. Unrelated but in the same file: three other sequential reads (leaves, public holidays, schedule publications) now run alongside the main shifts query, and a duplicate org lookup further down the handler has been merged with the one at the top. - Leave balance handler parallelizes lookups — The three independent reads at the top of
GET /api/leaves/balance(current user, organization settings, target user) now fire together. Permission checks still gate access to the target user's data for non-self callers. - People grid on
/time-tracking/peopleuses DB aggregates — The endpoint was pulling every time entry for the week and for today and then looping in memory to compute per-user totals. It now usesprisma.timeEntry.groupByfor the week and day sums plus one narrow fetch for the handful of live active timers, which is all the UI needs for the running-clock badge. Wire payload drops from "all closed entries for the week" to "one sum per user". - Department filter on time reports pushed to the DB — The
/time-tracking/reportsendpoint had the same department filter expressed both in thewhereclause and again as an in-memoryArray.filterafter the fetch. The redundant in-memory pass is gone; admin-supplied?departmentId=…now goes to the DB too so it doesn't scan rows just to throw them away. - Composable fetches in parallel — Department-head paths in
useTimesheetCalendaranduseScheduleCalendarused toawait /api/usersand thenawait /api/users/{id}in sequence when both can start immediately. Same on the cold-bootuseDashboardcall for departments + leave types. All three now fire in parallel viaPromise.all. The tour/timesheet calendar also gets a microtask-level debounce aroundfetchEntries()so the cascade of watchers that wake up together when you toggle a range filter coalesces into one request instead of 2-3 races. Preferences load once per session instead of refetching/api/users/{id}on every calendar navigation. - Cross-tab notification dedup — The unread-count poller in
useNotificationsruns in every open tab. Each tab still polls (so a backgrounded tab can't go stale), but the result is now broadcast over aBroadcastChannelso sibling tabs update their local unread ref from the broadcast instead of all hitting the server independently. User with three tabs open → same load on the server as one. - Domain middleware merged into a single query —
server/middleware/domain.tsused to find theCustomDomainrow and then, on a hit, do a secondfindUniqueon the owningOrganizationto check plan/features. The org is now loaded via a relationincludeon the first call, so the cache-miss path is a single DB round-trip per unique host. - Request-scoped plan cache in feature-gate — Endpoints that run two or more feature checks in a single request (e.g. an expense-create that checks
checkReceiptLimitandcheckFeatureAccess) used to re-query the organization row for each check. A new internalgetOrgBasicCached(event, organizationId)helper stashes the plan/features/limits shape onevent.contextfor the duration of the request, so subsequent checks in the same handler are free. Callers that don't passeventkeep the old behaviour — nothing breaks. - Reports charts consolidated — The
/reportspage's four chart fetchers (expenses, time, project, schedule) now all hit a single/api/reports/chart-dataendpoint that runs everything as Postgresdate_truncGROUP BYs. The wire carries ~6 rows per chart no matter how much underlying data exists. On mount the four charts share one call; each chart's settings gear still refetches independently when its period changes. - Reports stats explainer — A single "?" icon in the
/reportspage header reveals a short card explaining what each stat card measures (Leave/Expense/Time are all-time, Projects is ACTIVE-only, Scheduling is current calendar year) and that charts default to six months. No per-card clutter. - Admin home dashboard — A new
/api/admin/summaryendpoint returns the platform-wide KPI strip (organizations, users, MRR, etc.) via Postgres aggregates in one call instead of a handful of per-metric reads. Caches in Redis for 60s; charts paint in tens of milliseconds. - Admin list endpoints —
/api/admin/organizations,/api/admin/refund-requests, and/api/admin/audit-logsnow Redis-cache their list responses for 30s. Thestatusfilter on refunds is allow-list validated (unknown values used to silently return the full list); the org search string is length-capped at 100 characters. - Users list —
/api/usersnow acceptspage,pageSize,search,departmentId,role,status, andsortByquery params. All filtering, sorting, and pagination is pushed to Postgres. The response includes atotalcount alongside the page of users. DEPARTMENT_HEAD callers are forced to their own department server-side even if they pass a differentdepartmentId(gate preserved). Role-awareselect— employees never receive HR-sensitive fields. Two new composite indexes ((organizationId, isActive, firstName)and(organizationId, departmentId, isActive)) back the common sort/filter paths. - Documents list —
/api/documentsnow uses Prisma_countrelation aggregates for per-document signer counts instead of looping. Server-side pagination, filter, and sort. Response is Redis-cached for 30s keyed by(organizationId, userId, role, params)so an admin's cached view never serves an employee's scoped view. Two new composite indexes back the common filter combinations. - Calendar list endpoint —
/api/time-tracking/entries(used by the timesheets calendar + flat list) now has a short-lived Redis cache scoped by organization + role + department/user and the date window. Theavatarfield was dropped from the userselectsince the calendar never renders it — one less join, one less field to serialize. Date-range validation added to reject malformed inputs early. - Approvals detail pages —
/api/leaves/pending,/api/expenses/approvals, and/api/time-tracking/approvals/indexall got: Redis-cached list responses, slim selects, server-side pagination bounds, and count + list + summary aggregated in onePromise.all(expenses queue). Thestatusfilter on time-tracking approvals is now allow-list validated. - QuickBooks dashboards —
/api/integrations/quickbooks/status,/api/integrations/quickbooks/employees/mappings, and/api/integrations/quickbooks/sync/historyall got: Redis-cached responses with conservative TTL (10-60s),groupBy-based status summaries instead of looped counts, and explicit exclusion of OAuth credentials + raw QBO request/response payload blobs from the cached response. Admins see fresh status without every poll hitting the DB. - Cache-Control headers everywhere — Every new read endpoint sets
Cache-Control: private, max-age=10-30and anX-Cache: HIT|MISStelemetry header so ops can see cache effectiveness in DevTools. - Home dashboard cache —
/api/dashboard/users(the endpoint every user hits on login) now Redis-caches its full response for 30 seconds. The cache key scopes by organization, role, and — for dept heads and employees — the caller's department / user ID, so an admin's cached org-wide view can never be served to someone who should see less. The external Nager.Date public-holiday lookup that ran on every dashboard load is now memoized per(country, year)in Redis for 7 days — same holidays for every tenant with users in that country, fetched once per week. - Expenses list pagination —
/expensesused to request a thousand expense reports with full line-item / per-diem / mileage / receipt relations on page mount, even though the table only renders a page of ~50 rows. The endpoint now takespage/pageSize/search/status/reportType/userId/sort/startDate/endDatequery params and does all pagination / filtering / sorting in Postgres. Heavy relations are opt-in viaincludeItems=true. OptionalincludeStats=trueadds a count + total aggregate in two parallel queries. Response is Redis-cached for 30s with role + viewer-scoped keys. - Reverse-geocode cache —
/api/maps/geocodeis hit on every location-aware clock-in / clock-out. External geocoding calls (~900ms each) are now cached in Redis for 7 days, keyed by coordinates bucketed to 3 decimal places (~111m cells). Clock-ins from the same office share a cache entry; clock-ins from different neighborhoods don't. No tenant prefix on the key — reverse-geocode responses contain only public address strings with no per-user data. Rate limiting and auth checks still run before every lookup; null results are intentionally not cached to avoid pinning a transient provider outage. - Billing subscription dedupe — Every component that called
useBilling()used to get its own local state and fire its own/api/billing/subscriptionrequest, so the/billingpage was making three or more parallel identical requests on mount.useBilling()now shares module-level state + an in-flight promise (same pattern asuseBootstrap), so concurrent callers all await the same single request. One network round-trip per page load instead of three.