[{"data":1,"prerenderedAt":4280},["ShallowReactive",2],{"$fo25bqj26zy9p":3,"mdc-8ddzgk-key":15},{"release":4,"newer":10,"older":12,"page":14},{"tag":5,"name":5,"publishedAt":6,"url":7,"prerelease":8,"markdown":9},"v1.0.10","2026-04-20T16:40:11Z","https:\u002F\u002Fgithub.com\u002Fanhourtec\u002FBookYourPTO-SaaS\u002Freleases\u002Ftag\u002Fv1.0.10",false,"### New Features\r\n\r\n#### Geofencing\r\n\r\n- **Allowed clock-in locations** — New `\u002Fsettings\u002Fgeofencing` area lets Administrators and Executives define the sites where employees can clock in \u002F out. Department heads can also manage geofences — scoped to their own department's users and projects. Every fence carries a name, type (Site \u002F Project Site \u002F Client Site \u002F Home Office \u002F Other), optional address, and a description field visible only to admins. Fences can be toggled Active \u002F Inactive without losing history.\r\n\r\n- **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\u002Foutside 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.\r\n\r\n- **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_KEY` is missing the editor gracefully falls back to coordinate inputs.\r\n\r\n- **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.\r\n\r\n- **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.\r\n\r\n- **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.\r\n\r\n- **Three enforcement modes** — `REQUIRED` blocks clock-ins that fall outside an applicable fence, `OPTIONAL` lets them through but flags the entry on the compliance dashboard, and `LOG_ONLY` silently 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 `\u002Fsettings\u002Ftimetracking`.\r\n\r\n- **Live status on the clock-in screen** — A full-width status banner above the Clock In \u002F Clock Out buttons on `\u002Ftime-tracking` shows 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.\r\n\r\n- **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.\r\n\r\n- **Compliance dashboard + CSV export** — Administrators \u002F 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.\r\n\r\n- **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\u002Fafter payload, so compliance has a reviewable trail of every geofence policy decision.\r\n\r\n- **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 \u002F MEDIUM \u002F LOW \u002F UNVERIFIED) are stored on the entry. Historical entries retain this information even if the fence is later renamed, deactivated, or deleted.\r\n\r\n- **Mobile-ready clock API** — Clock-in \u002F clock-out endpoints emit an `X-API-Version` response header and return structured `data.error` codes so future native mobile clients can match on stable identifiers instead of parsing human messages. An offline-queue timestamp protocol (`clockInAt` \u002F `clockOutAt` body fields with a ±15 min future \u002F 24 h past tolerance) lets mobile apps replay clock events queued while the device was offline.\r\n\r\n- **Shared row-action pattern** — The `\u002Fsettings\u002Fgeofencing` list and the Assignments table both use the same ellipsis-menu row action (Edit \u002F Manage assignments \u002F 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.\r\n\r\n#### Time Tracking & Project Management\r\n\r\n- **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.\r\n\r\n- **Configurable clock time rounding** — New time-tracking setting to snap clock-in\u002Fout to a configured interval (1–60 minutes) with UP \u002F DOWN \u002F NEAREST direction. Disabled by default. Applied uniformly to live clock-in\u002Fout, manual entry, and edits so billable hours line up with the org's rounding policy regardless of how precisely the user clicks.\r\n\r\n- **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.\r\n\r\n- **Project money budgets** — New `budgetAmount` field 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\u002F100% colour thresholds. Currency inherits from the organization's default.\r\n\r\n- **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.\r\n\r\n- **Restart-timer (duplicate)** — One-click duplicate on any past entry clones the project \u002F task \u002F description \u002F notes and starts it as a fresh active timer. Stops any currently-running timer first so the \"one active timer per user\" invariant holds.\r\n\r\n- **Branded PDF export for timesheets** — `GET \u002Fapi\u002Freports\u002Ftimesheets.pdf` returns a per-employee branded breakdown with summary totals, billable split, and an optional money column. Same filters as the JSON report plus `projectId`. Admins\u002FExecutives get org-wide; department heads are scoped to their own department; employees see their own. New \"Export PDF\" button on the timesheet top bar.\r\n\r\n- **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 \u002F project, so a manager can go from \"this person logged 40h\" straight to the underlying entries.\r\n\r\n- **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.\r\n\r\n- **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\u002Fyear; 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.\r\n\r\n#### Team Matrix View for Schedules and Timesheets\r\n\r\nThe 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\u002FKronos, 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 `\u002Fschedules` and `\u002Ftime-tracking\u002Ftimesheets`, and makes it the default.\r\n\r\n- **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 \u002F amber pending \u002F 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.\r\n\r\n- **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 \u002F 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.\r\n\r\n- **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 `localStorage` so 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.\r\n\r\n- **Density toggle** — Compact (36–40px rows) \u002F Normal (52–60px) \u002F 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.\r\n\r\n- **Sort and filter** — Sort by name, week hours ↑\u002F↓, pending count, or missing days (timesheets) \u002F unscheduled days (schedules). Filter chips at the top toggle between All \u002F Missing \u002F Pending \u002F Approved \u002F Rejected (timesheets) or All \u002F Unscheduled \u002F Scheduled \u002F 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.\r\n\r\n- **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.\r\n\r\n- **Calendar and List views still available** — The top bar's toggle is now three buttons instead of two: Team (default) \u002F Calendar \u002F 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.\r\n\r\n#### Approvals & Reports UX Redesign\r\n\r\n- **Approvals dashboard overview** — `\u002Fapprovals` is 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.\r\n\r\n- **Per-type approval pages** — Three new dedicated routes: `\u002Fapprovals\u002Fleave`, `\u002Fapprovals\u002Fexpenses`, `\u002Fapprovals\u002Ftime`. Each follows a consistent layout: breadcrumb + page header, search + filter dropdowns + export toolbar, a data table with `\u003Ccolgroup>` column widths, employee avatar cell, colored status \u002F type badges, row-click detail slideover, action dropdown menu per row, reject modal with a dedicated header \u002F body \u002F footer + live character counter + keyboard shortcut hint, and a bottom-pinned pagination footer with rows-per-page selector and prev\u002Fnext navigation.\r\n\r\n- **Reports dashboard overview** — `\u002Freports` is 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\u002F10\u002F15\u002F20, 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\u002F5\u002F7\u002F10), 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.\r\n\r\n- **Per-type report pages** — Five new dedicated routes: `\u002Freports\u002Fleave`, `\u002Freports\u002Fexpenses`, `\u002Freports\u002Ftime`, `\u002Freports\u002Fprojects`, `\u002Freports\u002Fscheduling`. Each wraps the existing tab component inside a consistent shell (breadcrumb + page header). Permission gates and time-tracking redirects preserved.\r\n\r\n- **Tab → query-param navigation** — Hash-based tab navigation on `\u002Fapprovals` and `\u002Freports` (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 shared `useTabParam` composable handles the state, URL sync, and the async-validation edge case where `validTabs` depends on settings (e.g. `isTimeTrackingEnabled`) that load after the initial render.\r\n\r\n- **Shared UI primitives for approvals and reports** — New `components\u002Fcommon\u002F` directory: `PageHeader`, `PageTabs`, `PageToolbar`, `StatCard`, `StatusBadge`, `SearchInput`, `FilterSelect`, `FilterBar`, `SlideOver`, `ActionMenu`. The stat card uses an edge-joined `first:rounded-l-lg last:rounded-r-lg` strip pattern so a row of cards reads as a single unit. The action menu teleports its dropdown to `\u003Cbody>` with fixed positioning so it escapes any clipping ancestor (table wrappers, slideovers). The slideover is a right-aligned 400px drawer with backdrop click \u002F Esc to close. All built in raw Tailwind against the existing CSS variables so they match the rest of the product's design system.\r\n\r\n- **Cost flow fix in time reports** — The Reports Time tab was dropping `cost`, `totalCost`, and `entriesMissingRate` between the API response and the chart\u002Fcard render paths on the previous `by_employee` merge. Refactored the merge logic out into `utils\u002Ftime-report-shape.ts` with 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.\r\n\r\n- **Full-width page treatment** — `\u002Fapprovals`, `\u002Freports`, and all 8 sub-pages drop the `max-w-7xl` cap and stretch to the full content-area width, matching the `\u002Fschedules` layout so wide tables don't leave empty side margins on large monitors.\r\n\r\n- **Pass-through of avatar role colour + badges** — Every new approval\u002Freport surface uses the existing `\u003CUserAvatar>` component, so an employee's initials circle, role-tinted colour, and Administrator\u002FExecutive crown\u002Fstar badge render identically across `\u002Fusers`, `\u002Fcalendar`, `\u002Fapprovals`, and `\u002Freports`. No new palette, no drift.\r\n\r\n#### Settings Search\r\n\r\n- **Global settings search bar in the top header** — On every `\u002Fsettings\u002F*` route, the top toolbar renders a compact Stripe-style search input (left-aligned with the page H1). Press `\u002F` anywhere on a settings page to focus it. Hidden on mobile to keep the toolbar usable.\r\n\r\n- **Deep-linkable individual settings** — Search results resolve to `path#anchor` (e.g. \"time zone\" → `\u002Fsettings\u002Fgeneral#time-zone`, \"auto clock out\" → `\u002Fsettings\u002Ftimetracking#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\u002FOffboarding, 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.\r\n\r\n- **Hand-curated search index with keyword aliases** — `composables\u002FuseSettingsSearch.ts` defines 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\u002Fdescription\u002Fkeywords.\r\n\r\n- **Billing-aware lock badges in results** — Each result carries `locked`, `planLabel`, `isAddOn`. Locked entries render a `🔒 Pro` \u002F `🔒 Business` \u002F `🔒 Enterprise` \u002F `🔒 Add-on` pill matching the badge style on `\u002Fsettings` overview 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 `-25` score penalty. Uses the same `isFeatureLocked` + `getLockedPlanLabel` helpers as `\u002Fbilling` so the labels stay consistent across the product. The bar calls `loadSubscription()` on mount to ensure badges reflect the org's actual plan.\r\n\r\n- **Role-aware visibility** — The search only indexes `visibleItems` from `useSettingsNavigation`, so an employee searching for \"audit logs\" gets no result (the page is admin\u002Fexec only), while an administrator does. Department heads see onboarding\u002Foffboarding entries.\r\n\r\n- **Tabbed-page auto-switch** — On pages with tabs (Security, Connected Apps), navigating to `#audit-logs` \u002F `#personal-webhooks` \u002F etc. now auto-selects the matching tab via a `watch(route.hash)` wired to a static anchor-to-tab map, so a result click lands on actual content rather than the default tab.\r\n\r\n- **Keyboard navigation** — Arrow keys move the active row, Enter commits, Esc closes, `\u002F` global shortcut focuses the input (ignored when the user is already typing in another field).\r\n\r\n- **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\u002Fright for a split second on every refresh. Suppressed the transition on the very first paint via a `layoutReady` flag so the layout snaps instantly; sidebar-toggle animation still works for later user interactions.\r\n\r\n#### Guided Tours Overhaul\r\n\r\n- **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. `\u002F`, `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 (respects `prefers-reduced-motion`).\r\n\r\n- **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.\r\n\r\n- **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 \u002F OSM provider switch, and the assignments step that most admins miss.\r\n\r\n- **\"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.\r\n\r\n- **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.\r\n\r\n#### Page Redesigns & Data Export\r\n\r\n- **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\u002Fpage default) keeps the API efficient. Checkbox multi-select supports bulk edit and bulk delete. The entries API endpoint now returns summary aggregates (approved\u002Fpending\u002Frejected minutes via `groupBy`) alongside the page so header stats stay accurate across pagination. View mode (calendar\u002Flist) persists in the URL query param (`?view=calendar` \u002F `?view=list`) — no flash on refresh.\r\n\r\n- **AG Grid export page for timesheets** — Clicking the export button navigates to `\u002Ftime-tracking\u002Ftimesheets\u002Fexport` with the active date range and filters as query params. The page uses AG Grid Community (`ag-grid-vue3`) with: four theme variants (Quartz \u002F Alpine \u002F Balham \u002F Material) auto-switching to dark mode, custom Excel-style checkbox set filter per column (Select All \u002F Deselect All with search — replaces the Enterprise-only Set Filter), Columns dropdown to show\u002Fhide 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.\r\n\r\n- **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.\r\n\r\n- **List view + export for schedules** — All timesheet list-view features ported to `\u002Fschedules`: flat table (Date, Employee, Department, Shift Type badge, Time, Break, Duration, Notes, Actions), calendar\u002Flist toggle persisted in URL, AG Grid export page at `\u002Fschedules\u002Fexport` with 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.\r\n\r\n- **Users page redesign** — `\u002Fusers` redesigned 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 `\u003Ctable>` 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\u002F20\u002F50). CSV export of filtered users. Full-width layout matching `\u002Fapprovals`. AG Grid export at `\u002Fusers-export`.\r\n\r\n- **Uniform stat chip styling** — Timesheet and schedule top bars use consistent neutral-tone stat chips (`bg-muted\u002F60 ring-1 ring-border`) instead of rainbow-colored pills, matching the admin dashboard pattern across all pages.\r\n\r\n- **Responsive top bar wrapping** — Both timesheet and schedule top bars use `flex-wrap gap-y-2` so stat chips, view toggle, and action buttons wrap gracefully on narrow viewports instead of overflowing.\r\n\r\n- **Sidebar visibility gating** — Employee sidebar toggle button hidden in list view on both timesheets and schedules (sidebar is only relevant for the calendar grid).\r\n\r\n- **Date range visibility gating** — Date range filter row hidden in calendar view on both timesheets and schedules (calendar is fixed at 7-day columns).\r\n\r\n- **Unified Export dropdown** — New `CommonExportDropdown` component replaces separate CSV\u002FPDF\u002FExcel 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.\r\n\r\n- **AG Grid export pages for approvals** — Three new export pages: `\u002Fapprovals\u002Fleave-export`, `\u002Fapprovals\u002Fexpense-export`, `\u002Fapprovals\u002Ftime-export` — each with the full AG Grid tooling (theme picker, column visibility, custom set filters, quick filter, pinned totals).\r\n\r\n- **Client-side branded PDF export** — New shared utility `utils\u002Fexport-pdf.ts` generates branded PDFs client-side using pdf-lib. Fetches org logo + company name from `\u002Fapi\u002Fbranding` (which now falls back to `SMTP_LOGO_URL` env 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.\r\n\r\n- **Manage Departments modal redesign** — Compact header\u002Fbody\u002Ffooter 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.\r\n\r\n- **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 `\u002Fexpenses-export`.\r\n\r\n- **Expense approvals redesign** — Admin-style table replacing grid-cols-12 layout. CommonActionMenu per row. Reject modal with header\u002Fbody\u002Ffooter sections, contextual chip, character counter. Pagination footer.\r\n\r\n- **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.\r\n\r\n- **Reports overview fixes** — All 5 charts use settings gear overlay with period options (3 months \u002F 6 months \u002F 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\".\r\n\r\n- **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.\r\n\r\n- **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.\r\n\r\n- **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.\r\n\r\n- **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.\r\n\r\n- **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.\r\n\r\n- **Projects list page redesign** — Full-width, admin-style table with project color dots, status badges, progress bars, CommonActionMenu, pagination footer, export dropdown.\r\n\r\n- **Documents page redesign** — Full-width, admin-style table with CommonActionMenu and CommonStatusBadge. New `CommonSegmentedTabs` component replaces underline tab bar — pill-shaped toggle group with sliding primary-color indicator, ResizeObserver for responsive positioning. URL sync changed from `#hash` to `?view=` query params.\r\n\r\n### Improvements & Fixes\r\n\r\n#### UX Refinements\r\n\r\n- **Org chart list view matches the \u002Fusers table styling** — The `\u002Fusers\u002Forgchart?view=list` People page has been realigned with the admin-dashboard `\u003Ctable>` treatment used on `\u002Fusers`: a single shared `CommonPageToolbar` with search + Department \u002F Employment Type \u002F Status inline filters, a proper `\u003Ctable>` with `\u003Ccolgroup>` column widths and rounded header cells, inline `UserAvatar` cells, `CommonStatusBadge` pills for the employment-type status (Full-time \u002F Part-time \u002F Contract \u002F Intern \u002F Temporary \u002F Seasonal), and the standard rows-per-page pagination footer (10 \u002F 20 \u002F 50). Employment-type tones are mapped through the shared StatusBadge palette so colors stay consistent with the rest of the product. Column visibility (Employee # \u002F Department \u002F Location) is preserved as a trailing toolbar action. Mobile switches to a card layout.\r\n\r\n- **Current-session indicator on \u002Fsettings\u002Ftrusted-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.\r\n\r\n- **Persisted day-row heights on the schedule grid** — When a user drags the row-resize handle on `\u002Fschedules` to 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.\r\n\r\n- **Leave history no longer shows phantom -1 day deductions for non-deducting types** — The History table on `\u002Fusers\u002F:id?tab=timeoff` was 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 `\u002Fcalendar\u002F:id`. A new shared `utils\u002FleaveBucket.ts` helper mirrors the same ANNUAL \u002F SICK \u002F NONE classification used server-side (including the legacy fallback for rows that predate the `deductionBucket` column), and the History cell now shows \"—\" with an explanatory tooltip for leaves that don't deduct.\r\n\r\n- **Mark all tours completed in one click** — `\u002Fsettings\u002Ftours` gains 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.\r\n\r\n#### Bug Fixes\r\n\r\n- **Notification settings (`\u002Fsettings\u002Fnotifications`) crashed for orgs with legacy defaults** — When an organization's saved notification defaults were stored in an older shape, the page would throw `Cannot read properties of undefined (reading 'enabled')` instead of rendering. The server endpoint now normalises partial payloads through the shared `resolveNotificationPreferences` helper, and the client merges fetched values over the form's defaults so a missing nested key can no longer blow up the view.\r\n\r\n- **Password manager hints on settings** — Added the right `autocomplete` attributes to every password \u002F 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.\r\n\r\n- **Due-date calculation ignored BEFORE\u002FAFTER 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\u002Fusers\u002F[id]\u002Findex.vue` was always adding the offset; it now defers to the server which honors `dueDaysDirection: 'BEFORE' | 'AFTER'` with UTC date arithmetic.\r\n\r\n- **\"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.\r\n\r\n- **\"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 \u002Fapi\u002Fonboarding\u002Finstances` with the template ID, creating a separate `OnboardingInstance` with its own `templateName`\u002F`templateIcon` snapshots so each imported list renders as its own card.\r\n\r\n- **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` \u002F `availableTemplatesForStart` computeds that filter out templates already present in the user's instances.\r\n\r\n- **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\u002FHR 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.\r\n\r\n- **Signed documents did not auto-complete their linked tasks** — When an employee signed a document attached to an onboarding task, the task stayed in `PENDING` status until someone manually ticked it. Added `autoCompleteTasksForSignedDocument` which fires from the document sign handler, matches tasks by `(documentId, instance.userId)`, flips status to `COMPLETED`, rolls up instance status, and dispatches the task-completed notification. Works for both the direct per-user clone and the source-template document cases.\r\n\r\n- **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 to `max-width=\"2xl\"` with a two-column layout (Name + Department side-by-side) and the icon picker given full breathing room below.\r\n\r\n- **Shrine icon rendered as a blank square** — `lucide:shrine` is 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).\r\n\r\n- **Department heads could not manage their own team's task lists** — Only ADMINISTRATOR and EXECUTIVE could CRUD onboarding templates, forcing every HR\u002FIT\u002FSales 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 with `departmentId: null` stay admin\u002Fexec only. Enforced via the new `server\u002Futils\u002Fonboarding-access.ts` helper across all template, taskdef, instance, and per-user-task endpoints.\r\n\r\n- **Department heads could not see Onboarding \u002F Offboarding in \u002Fsettings** — The settings sidebar and page wrappers had hard `adminOnly` gates. Introduced a `deptHeadAllowed` flag on `SettingsNavItem` and `SettingsPageWrapper`. Onboarding and Offboarding now opt in; department heads see both entries in their settings sidebar and can open either page.\r\n\r\n- **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=true` to `GET \u002Fapi\u002Fonboarding\u002Ftemplates`. The settings page passes this flag, so a DH only sees their own department's templates. The user-page Import\u002FStart flow still fetches the full visible set (org-wide + dept) because DHs should be able to import admin-built lists for their team.\r\n\r\n- **403 on \u002Fapi\u002Fonboarding\u002Fpackets for department heads** — Opening `\u002Fsettings\u002Fonboarding-templates` as 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=packets` directly, it coerces to `tasks`.\r\n\r\n- **Misleading \"All departments (admin-only)\" dropdown label** — The department selector in the New\u002FEdit 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.\r\n\r\n- **Task list cards had no spacing between them** — Multiple onboarding\u002Foffboarding instances rendered flush against each other with no gap. Added `mb-4 last:mb-0` to each instance card on both the Onboarding and Offboarding tabs.\r\n\r\n- **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.\r\n\r\n- **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\u002FSome)\" and \"Update existing employee tasks\" fields because per-user tasks only ever apply to one user.\r\n\r\n- **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.\r\n\r\n- **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\u002Fmanagement learned about it. Added `sendOnboardingTaskCompletedNotification` which emails administrators, executives, AND the task assignee (deduped). Includes the employee name in the subject line and body.\r\n\r\n- **Due-date reminders only went to one person** — The 1-day-before \u002F 3-day \u002F 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.\r\n\r\n- **Task removed → no notification** — Deleting a task from an active instance silently disappeared the row. Admins\u002Fexecs\u002FDH 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.\r\n\r\n- **Instance removed → no notification** — Same for removing an entire task list from a user's profile. Admins\u002Fexecs\u002FDH are now notified with the list name, task count deleted, and who removed it.\r\n\r\n- **No audit trail for onboarding\u002Foffboarding settings changes** — Template and task mutations were not appearing in `\u002Fsettings\u002Fsecurity → Audit Logs`. Added `logOnboardingAudit` helper that writes `AuditLog` entries with IP + user-agent for every CREATE\u002FUPDATE\u002FDELETE on `ONBOARDING_TEMPLATE`, `ONBOARDING_TASK_DEF`, `ONBOARDING_INSTANCE`, and `ONBOARDING_TASK`. UPDATEs include before\u002Fafter snapshots. Task PATCHes distinguish `reason: STATUS` (marking complete\u002Fincomplete) from `reason: EDIT` (field change) so the audit feed is actionable.\r\n\r\n- **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 `\u002Fsettings\u002Fonboarding-templates` and `\u002Fsettings\u002Foffboarding-templates` now has a grip handle and is `draggable=\"true\"`. Drag to reorder; `sortOrder` is renumbered locally and each changed row is PATCHed. Optimistic UI — reverts on error.\r\n\r\n- **Email CTA buttons used a hardcoded purple** — All onboarding\u002Foffboarding email action buttons (\"View My Tasks\", \"View Task\", \"View Onboarding\", \"View Profile\") rendered `#4f46e5` regardless of the organization's white-label branding. Added `primaryColor` to `EmailConfig` which reads `CustomDomain.primaryColor` (falling back to `#3B82F6`). All eight CTA buttons across the onboarding email templates now render the organization's brand color.\r\n\r\n- **`OnboardingInstance` lost 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. Added `templateName` + `templateIcon` snapshots on `OnboardingInstance`. Each imported\u002Fstarted list keeps its own identity even after the source template changes. The UI prefers the snapshot (`inst.templateName`) over the live relation.\r\n\r\n- **`canCompleteTask` in the frontend allowed the instance owner to complete their own tasks** — Mirrored the backend fix in the `taskDetail` completion gate. The modal's \"Mark Complete\" button now requires admin\u002Fexec, the explicit assignee, or a matching department head — not just \"is this your own instance\".\r\n\r\n- **Saving a time-tracking setting could fail with a 400 after the page was reloaded** — Toggling any switch on `\u002Fsettings\u002Ftimetracking` (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 as `null` on save — which a strict type check was then rejecting. The save handler now treats `null` the 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.\r\n\r\n#### Security & Compliance\r\n\r\n- **Department-head scope enforcement** — A department head cannot use a template scoped to another department (enforced on `POST \u002Fapi\u002Fonboarding\u002Finstances` when they try to pass a `templateId` belonging to a different department). They also cannot re-scope a template between departments via PATCH.\r\n\r\n- **Template-move authorization** — Moving a task definition between templates (via `PATCH \u002Fapi\u002Fonboarding\u002Ftemplates\u002F[id]\u002Ftasks\u002F[taskDefId]` with a new `templateId`) now re-runs `assertCanManageTemplate` against the target template's department. A DH cannot move a task into a template they don't own.\r\n\r\n- **Document auto-complete cross-user safety** — `autoCompleteTasksForSignedDocument` filters by `instance.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.\r\n\r\n- **DH departmentId self-pin on create** — When a department head creates a new task list, the backend always pins `departmentId` to their own department regardless of what the request sent, closing the gap if the UI-level dropdown is manipulated.\r\n\r\n- **Tenant isolation on time-entry and project mutations** — Foreign-key fields written on time-entry create\u002Fedit (`projectId`, `taskId`) and project create\u002Fedit (`managerId`) are validated against the caller's organization at every write path.\r\n\r\n- **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 `userId` filter returns 403 instead of an empty report so the caller knows the request was denied.\r\n\r\n- **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.\r\n\r\n- **Lockdown-override audit trail** — Every admin\u002Fexec 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.\r\n\r\n- **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.\r\n\r\n- **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 \u002F clock-out ranges are rejected up front so downstream billing math never sees a negative duration.\r\n\r\n- **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.\r\n\r\n- **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.\r\n\r\n- **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.\r\n\r\n- **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\u002Fmax bounds.\r\n\r\n- **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.\r\n\r\n- **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.\r\n\r\n- **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), and `hono` (4.12.12 → 4.12.14) via npm overrides to resolve Dependabot security advisories.\r\n\r\n#### Performance\r\n\r\n- **Search debounce (300ms)** — `CommonSearchInput` now debounces emit by 300ms, preventing re-filtering on every keystroke across all pages. Single fix, global impact.\r\n\r\n- **Leaves endpoint server-side pagination** — `GET \u002Fapi\u002Freports\u002Fleaves-data` now supports `page`\u002F`limit` params. Returns `pagination` metadata + `summary` aggregates (approved\u002Fpending\u002Frejected counts + totalDays via `groupBy`). Runs `count`, `groupBy`, and `findMany` in `Promise.all` for zero extra latency.\r\n\r\n- **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.\r\n\r\n- **Projects N+1 query eliminated** — Replaced per-project `Promise.all(projects.map(aggregate))` (N individual queries) with a single `timeEntry.groupBy({ by: ['projectId'] })` query mapped back by project ID. O(N) → O(1).\r\n\r\n- **TimeTab server-side pagination** — Uses `serverTotal` for real entry count and `serverSummary` for accurate hours across all pages (not just current page of 50).\r\n\r\n- **Branding endpoint SMTP fallback** — `\u002Fapi\u002Fbranding` now falls back to `SMTP_LOGO_URL` env var when no white-label logo is configured, ensuring PDF exports always have a logo.\r\n\r\n- **Reports overview now loads in one pass** — `\u002Freports` was firing roughly seventy requests on mount to paint five stat cards and six charts. The summary strip now calls a single new `GET \u002Fapi\u002Freports\u002Fsummary-stats` endpoint that returns all five KPI numbers via DB aggregates; the leave-by-employee chart reads the existing `\u002Fapi\u002Freports\u002Fleaves-data` endpoint 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.\r\n\r\n- **Approvals overview uses one counts endpoint** — The `\u002Fapprovals` page 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 new `GET \u002Fapi\u002Fapprovals\u002Fcounts` endpoint 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.\r\n\r\n- **Faster time approvals** — Approving or rejecting a timesheet entry on `\u002Fapprovals\u002Ftime` used 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. `\u002Fapprovals\u002Fleave` and `\u002Fapprovals\u002Fexpenses` already worked this way; this brings timesheets in line.\r\n\r\n- **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.\r\n\r\n- **Schedule auto-generation cut from hundreds of queries to one** — When a team has the default-schedule feature turned on, loading `\u002Fschedules` was doing a per-employee-per-day `findFirst` to decide whether to create a shift. A typical 50-person, 5-day week ran 250+ individual queries. A single `findMany` now covers the whole range and the existence check happens against an in-memory `Map`. 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.\r\n\r\n- **Leave balance handler parallelizes lookups** — The three independent reads at the top of `GET \u002Fapi\u002Fleaves\u002Fbalance` (current user, organization settings, target user) now fire together. Permission checks still gate access to the target user's data for non-self callers.\r\n\r\n- **People grid on `\u002Ftime-tracking\u002Fpeople` uses 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 uses `prisma.timeEntry.groupBy` for 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\".\r\n\r\n- **Department filter on time reports pushed to the DB** — The `\u002Ftime-tracking\u002Freports` endpoint had the same department filter expressed both in the `where` clause and again as an in-memory `Array.filter` after 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.\r\n\r\n- **Composable fetches in parallel** — Department-head paths in `useTimesheetCalendar` and `useScheduleCalendar` used to `await \u002Fapi\u002Fusers` and then `await \u002Fapi\u002Fusers\u002F{id}` in sequence when both can start immediately. Same on the cold-boot `useDashboard` call for departments + leave types. All three now fire in parallel via `Promise.all`. The tour\u002Ftimesheet calendar also gets a microtask-level debounce around `fetchEntries()` 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 `\u002Fapi\u002Fusers\u002F{id}` on every calendar navigation.\r\n\r\n- **Cross-tab notification dedup** — The unread-count poller in `useNotifications` runs in every open tab. Each tab still polls (so a backgrounded tab can't go stale), but the result is now broadcast over a `BroadcastChannel` so 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.\r\n\r\n- **Domain middleware merged into a single query** — `server\u002Fmiddleware\u002Fdomain.ts` used to find the `CustomDomain` row and then, on a hit, do a second `findUnique` on the owning `Organization` to check plan\u002Ffeatures. The org is now loaded via a relation `include` on the first call, so the cache-miss path is a single DB round-trip per unique host.\r\n\r\n- **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 `checkReceiptLimit` and `checkFeatureAccess`) used to re-query the organization row for each check. A new internal `getOrgBasicCached(event, organizationId)` helper stashes the plan\u002Ffeatures\u002Flimits shape on `event.context` for the duration of the request, so subsequent checks in the same handler are free. Callers that don't pass `event` keep the old behaviour — nothing breaks.\r\n\r\n- **Reports charts consolidated** — The `\u002Freports` page's four chart fetchers (expenses, time, project, schedule) now all hit a single `\u002Fapi\u002Freports\u002Fchart-data` endpoint that runs everything as Postgres `date_trunc` GROUP 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.\r\n\r\n- **Reports stats explainer** — A single \"?\" icon in the `\u002Freports` page header reveals a short card explaining what each stat card measures (Leave\u002FExpense\u002FTime are all-time, Projects is ACTIVE-only, Scheduling is current calendar year) and that charts default to six months. No per-card clutter.\r\n\r\n- **Admin home dashboard** — A new `\u002Fapi\u002Fadmin\u002Fsummary` endpoint 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.\r\n\r\n- **Admin list endpoints** — `\u002Fapi\u002Fadmin\u002Forganizations`, `\u002Fapi\u002Fadmin\u002Frefund-requests`, and `\u002Fapi\u002Fadmin\u002Faudit-logs` now Redis-cache their list responses for 30s. The `status` filter 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.\r\n\r\n- **Users list** — `\u002Fapi\u002Fusers` now accepts `page`, `pageSize`, `search`, `departmentId`, `role`, `status`, and `sortBy` query params. All filtering, sorting, and pagination is pushed to Postgres. The response includes a `total` count alongside the page of users. DEPARTMENT_HEAD callers are forced to their own department server-side even if they pass a different `departmentId` (gate preserved). Role-aware `select` — employees never receive HR-sensitive fields. Two new composite indexes (`(organizationId, isActive, firstName)` and `(organizationId, departmentId, isActive)`) back the common sort\u002Ffilter paths.\r\n\r\n- **Documents list** — `\u002Fapi\u002Fdocuments` now uses Prisma `_count` relation 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.\r\n\r\n- **Calendar list endpoint** — `\u002Fapi\u002Ftime-tracking\u002Fentries` (used by the timesheets calendar + flat list) now has a short-lived Redis cache scoped by organization + role + department\u002Fuser and the date window. The `avatar` field was dropped from the user `select` since the calendar never renders it — one less join, one less field to serialize. Date-range validation added to reject malformed inputs early.\r\n\r\n- **Approvals detail pages** — `\u002Fapi\u002Fleaves\u002Fpending`, `\u002Fapi\u002Fexpenses\u002Fapprovals`, and `\u002Fapi\u002Ftime-tracking\u002Fapprovals\u002Findex` all got: Redis-cached list responses, slim selects, server-side pagination bounds, and count + list + summary aggregated in one `Promise.all` (expenses queue). The `status` filter on time-tracking approvals is now allow-list validated.\r\n\r\n- **QuickBooks dashboards** — `\u002Fapi\u002Fintegrations\u002Fquickbooks\u002Fstatus`, `\u002Fapi\u002Fintegrations\u002Fquickbooks\u002Femployees\u002Fmappings`, and `\u002Fapi\u002Fintegrations\u002Fquickbooks\u002Fsync\u002Fhistory` all 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\u002Fresponse payload blobs from the cached response. Admins see fresh status without every poll hitting the DB.\r\n\r\n- **Cache-Control headers everywhere** — Every new read endpoint sets `Cache-Control: private, max-age=10-30` and an `X-Cache: HIT|MISS` telemetry header so ops can see cache effectiveness in DevTools.\r\n\r\n- **Home dashboard cache** — `\u002Fapi\u002Fdashboard\u002Fusers` (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 \u002F 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.\r\n\r\n- **Expenses list pagination** — `\u002Fexpenses` used to request a thousand expense reports with full line-item \u002F per-diem \u002F mileage \u002F receipt relations on page mount, even though the table only renders a page of ~50 rows. The endpoint now takes `page` \u002F `pageSize` \u002F `search` \u002F `status` \u002F `reportType` \u002F `userId` \u002F `sort` \u002F `startDate` \u002F `endDate` query params and does all pagination \u002F filtering \u002F sorting in Postgres. Heavy relations are opt-in via `includeItems=true`. Optional `includeStats=true` adds a count + total aggregate in two parallel queries. Response is Redis-cached for 30s with role + viewer-scoped keys.\r\n\r\n- **Reverse-geocode cache** — `\u002Fapi\u002Fmaps\u002Fgeocode` is hit on every location-aware clock-in \u002F 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.\r\n\r\n- **Billing subscription dedupe** — Every component that called `useBilling()` used to get its own local state and fire its own `\u002Fapi\u002Fbilling\u002Fsubscription` request, so the `\u002Fbilling` page was making three or more parallel identical requests on mount. `useBilling()` now shares module-level state + an in-flight promise (same pattern as `useBootstrap`), so concurrent callers all await the same single request. One network round-trip per page load instead of three.\r\n",{"tag":11,"name":11},"v1.0.11",{"tag":13,"name":13},"v1.0.9",2,{"data":16,"body":17},{},{"type":18,"children":19},"root",[20,29,36,286,292,417,423,445,534,540,970,976,1275,1281,1363,1369,1793,1799,1805,1979,1985,2788,2794,3089,3095],{"type":21,"tag":22,"props":23,"children":25},"element","h3",{"id":24},"new-features",[26],{"type":27,"value":28},"text","New Features",{"type":21,"tag":30,"props":31,"children":33},"h4",{"id":32},"geofencing",[34],{"type":27,"value":35},"Geofencing",{"type":21,"tag":37,"props":38,"children":39},"ul",{},[40,61,71,89,99,117,127,169,187,197,207,217,227,269],{"type":21,"tag":41,"props":42,"children":43},"li",{},[44,50,52,59],{"type":21,"tag":45,"props":46,"children":47},"strong",{},[48],{"type":27,"value":49},"Allowed clock-in locations",{"type":27,"value":51}," — New ",{"type":21,"tag":53,"props":54,"children":56},"code",{"className":55},[],[57],{"type":27,"value":58},"\u002Fsettings\u002Fgeofencing",{"type":27,"value":60}," area lets Administrators and Executives define the sites where employees can clock in \u002F out. Department heads can also manage geofences — scoped to their own department's users and projects. Every fence carries a name, type (Site \u002F Project Site \u002F Client Site \u002F Home Office \u002F Other), optional address, and a description field visible only to admins. Fences can be toggled Active \u002F Inactive without losing history.",{"type":21,"tag":41,"props":62,"children":63},{},[64,69],{"type":21,"tag":45,"props":65,"children":66},{},[67],{"type":27,"value":68},"Circle and polygon shapes",{"type":27,"value":70}," — 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\u002Foutside 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.",{"type":21,"tag":41,"props":72,"children":73},{},[74,79,81,87],{"type":21,"tag":45,"props":75,"children":76},{},[77],{"type":27,"value":78},"Interactive map editor",{"type":27,"value":80}," — 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 ",{"type":21,"tag":53,"props":82,"children":84},{"className":83},[],[85],{"type":27,"value":86},"GOOGLE_MAPS_API_KEY",{"type":27,"value":88}," is missing the editor gracefully falls back to coordinate inputs.",{"type":21,"tag":41,"props":90,"children":91},{},[92,97],{"type":21,"tag":45,"props":93,"children":94},{},[95],{"type":27,"value":96},"Google Maps + OpenStreetMap",{"type":27,"value":98}," — 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.",{"type":21,"tag":41,"props":100,"children":101},{},[102,107,109,115],{"type":21,"tag":45,"props":103,"children":104},{},[105],{"type":27,"value":106},"Configuration + Assignments tabs",{"type":27,"value":108}," — 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 (",{"type":21,"tag":53,"props":110,"children":112},{"className":111},[],[113],{"type":27,"value":114},"?tab=assignments",{"type":27,"value":116},") for bookmarks and back-button navigation. Newly-created fences redirect straight to the Assignments tab so the admin never forgets the follow-up step.",{"type":21,"tag":41,"props":118,"children":119},{},[120,125],{"type":21,"tag":45,"props":121,"children":122},{},[123],{"type":27,"value":124},"Assignment scopes",{"type":27,"value":126}," — 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.",{"type":21,"tag":41,"props":128,"children":129},{},[130,135,137,143,145,151,153,159,161,167],{"type":21,"tag":45,"props":131,"children":132},{},[133],{"type":27,"value":134},"Three enforcement modes",{"type":27,"value":136}," — ",{"type":21,"tag":53,"props":138,"children":140},{"className":139},[],[141],{"type":27,"value":142},"REQUIRED",{"type":27,"value":144}," blocks clock-ins that fall outside an applicable fence, ",{"type":21,"tag":53,"props":146,"children":148},{"className":147},[],[149],{"type":27,"value":150},"OPTIONAL",{"type":27,"value":152}," lets them through but flags the entry on the compliance dashboard, and ",{"type":21,"tag":53,"props":154,"children":156},{"className":155},[],[157],{"type":27,"value":158},"LOG_ONLY",{"type":27,"value":160}," silently 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 ",{"type":21,"tag":53,"props":162,"children":164},{"className":163},[],[165],{"type":27,"value":166},"\u002Fsettings\u002Ftimetracking",{"type":27,"value":168},".",{"type":21,"tag":41,"props":170,"children":171},{},[172,177,179,185],{"type":21,"tag":45,"props":173,"children":174},{},[175],{"type":27,"value":176},"Live status on the clock-in screen",{"type":27,"value":178}," — A full-width status banner above the Clock In \u002F Clock Out buttons on ",{"type":21,"tag":53,"props":180,"children":182},{"className":181},[],[183],{"type":27,"value":184},"\u002Ftime-tracking",{"type":27,"value":186}," shows 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.",{"type":21,"tag":41,"props":188,"children":189},{},[190,195],{"type":21,"tag":45,"props":191,"children":192},{},[193],{"type":27,"value":194},"Rejection modal with nearest-fence guidance",{"type":27,"value":196}," — 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.",{"type":21,"tag":41,"props":198,"children":199},{},[200,205],{"type":21,"tag":45,"props":201,"children":202},{},[203],{"type":27,"value":204},"Compliance dashboard + CSV export",{"type":27,"value":206}," — Administrators \u002F 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.",{"type":21,"tag":41,"props":208,"children":209},{},[210,215],{"type":21,"tag":45,"props":211,"children":212},{},[213],{"type":27,"value":214},"Audit trail on every change",{"type":27,"value":216}," — 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\u002Fafter payload, so compliance has a reviewable trail of every geofence policy decision.",{"type":21,"tag":41,"props":218,"children":219},{},[220,225],{"type":21,"tag":45,"props":221,"children":222},{},[223],{"type":27,"value":224},"Geofenced time entries record their fence context",{"type":27,"value":226}," — When an entry is created inside a fence, the fence id, the GPS accuracy at the time, and a confidence classification (HIGH \u002F MEDIUM \u002F LOW \u002F UNVERIFIED) are stored on the entry. Historical entries retain this information even if the fence is later renamed, deactivated, or deleted.",{"type":21,"tag":41,"props":228,"children":229},{},[230,235,237,243,245,251,253,259,261,267],{"type":21,"tag":45,"props":231,"children":232},{},[233],{"type":27,"value":234},"Mobile-ready clock API",{"type":27,"value":236}," — Clock-in \u002F clock-out endpoints emit an ",{"type":21,"tag":53,"props":238,"children":240},{"className":239},[],[241],{"type":27,"value":242},"X-API-Version",{"type":27,"value":244}," response header and return structured ",{"type":21,"tag":53,"props":246,"children":248},{"className":247},[],[249],{"type":27,"value":250},"data.error",{"type":27,"value":252}," codes so future native mobile clients can match on stable identifiers instead of parsing human messages. An offline-queue timestamp protocol (",{"type":21,"tag":53,"props":254,"children":256},{"className":255},[],[257],{"type":27,"value":258},"clockInAt",{"type":27,"value":260}," \u002F ",{"type":21,"tag":53,"props":262,"children":264},{"className":263},[],[265],{"type":27,"value":266},"clockOutAt",{"type":27,"value":268}," body fields with a ±15 min future \u002F 24 h past tolerance) lets mobile apps replay clock events queued while the device was offline.",{"type":21,"tag":41,"props":270,"children":271},{},[272,277,279,284],{"type":21,"tag":45,"props":273,"children":274},{},[275],{"type":27,"value":276},"Shared row-action pattern",{"type":27,"value":278}," — The ",{"type":21,"tag":53,"props":280,"children":282},{"className":281},[],[283],{"type":27,"value":58},{"type":27,"value":285}," list and the Assignments table both use the same ellipsis-menu row action (Edit \u002F Manage assignments \u002F 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.",{"type":21,"tag":30,"props":287,"children":289},{"id":288},"time-tracking-project-management",[290],{"type":27,"value":291},"Time Tracking & Project Management",{"type":21,"tag":37,"props":293,"children":294},{},[295,305,315,325,342,352,362,387,397,407],{"type":21,"tag":41,"props":296,"children":297},{},[298,303],{"type":21,"tag":45,"props":299,"children":300},{},[301],{"type":27,"value":302},"Pay-period lockdown",{"type":27,"value":304}," — 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.",{"type":21,"tag":41,"props":306,"children":307},{},[308,313],{"type":21,"tag":45,"props":309,"children":310},{},[311],{"type":27,"value":312},"Configurable clock time rounding",{"type":27,"value":314}," — New time-tracking setting to snap clock-in\u002Fout to a configured interval (1–60 minutes) with UP \u002F DOWN \u002F NEAREST direction. Disabled by default. Applied uniformly to live clock-in\u002Fout, manual entry, and edits so billable hours line up with the org's rounding policy regardless of how precisely the user clicks.",{"type":21,"tag":41,"props":316,"children":317},{},[318,323],{"type":21,"tag":45,"props":319,"children":320},{},[321],{"type":27,"value":322},"Free-form tags on time entries",{"type":27,"value":324}," — 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.",{"type":21,"tag":41,"props":326,"children":327},{},[328,333,334,340],{"type":21,"tag":45,"props":329,"children":330},{},[331],{"type":27,"value":332},"Project money budgets",{"type":27,"value":51},{"type":21,"tag":53,"props":335,"children":337},{"className":336},[],[338],{"type":27,"value":339},"budgetAmount",{"type":27,"value":341}," field 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\u002F100% colour thresholds. Currency inherits from the organization's default.",{"type":21,"tag":41,"props":343,"children":344},{},[345,350],{"type":21,"tag":45,"props":346,"children":347},{},[348],{"type":27,"value":349},"Bulk delete time entries",{"type":27,"value":351}," — 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.",{"type":21,"tag":41,"props":353,"children":354},{},[355,360],{"type":21,"tag":45,"props":356,"children":357},{},[358],{"type":27,"value":359},"Restart-timer (duplicate)",{"type":27,"value":361}," — One-click duplicate on any past entry clones the project \u002F task \u002F description \u002F notes and starts it as a fresh active timer. Stops any currently-running timer first so the \"one active timer per user\" invariant holds.",{"type":21,"tag":41,"props":363,"children":364},{},[365,370,371,377,379,385],{"type":21,"tag":45,"props":366,"children":367},{},[368],{"type":27,"value":369},"Branded PDF export for timesheets",{"type":27,"value":136},{"type":21,"tag":53,"props":372,"children":374},{"className":373},[],[375],{"type":27,"value":376},"GET \u002Fapi\u002Freports\u002Ftimesheets.pdf",{"type":27,"value":378}," returns a per-employee branded breakdown with summary totals, billable split, and an optional money column. Same filters as the JSON report plus ",{"type":21,"tag":53,"props":380,"children":382},{"className":381},[],[383],{"type":27,"value":384},"projectId",{"type":27,"value":386},". Admins\u002FExecutives get org-wide; department heads are scoped to their own department; employees see their own. New \"Export PDF\" button on the timesheet top bar.",{"type":21,"tag":41,"props":388,"children":389},{},[390,395],{"type":21,"tag":45,"props":391,"children":392},{},[393],{"type":27,"value":394},"Expanded timesheet filters and report drill-down",{"type":27,"value":396}," — 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 \u002F project, so a manager can go from \"this person logged 40h\" straight to the underlying entries.",{"type":21,"tag":41,"props":398,"children":399},{},[400,405],{"type":21,"tag":45,"props":401,"children":402},{},[403],{"type":27,"value":404},"Bulk edit on timesheets",{"type":27,"value":406}," — 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.",{"type":21,"tag":41,"props":408,"children":409},{},[410,415],{"type":21,"tag":45,"props":411,"children":412},{},[413],{"type":27,"value":414},"Cost aggregation in time reports",{"type":27,"value":416}," — 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\u002Fyear; 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.",{"type":21,"tag":30,"props":418,"children":420},{"id":419},"team-matrix-view-for-schedules-and-timesheets",[421],{"type":27,"value":422},"Team Matrix View for Schedules and Timesheets",{"type":21,"tag":424,"props":425,"children":426},"p",{},[427,429,435,437,443],{"type":27,"value":428},"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\u002FKronos, 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 ",{"type":21,"tag":53,"props":430,"children":432},{"className":431},[],[433],{"type":27,"value":434},"\u002Fschedules",{"type":27,"value":436}," and ",{"type":21,"tag":53,"props":438,"children":440},{"className":439},[],[441],{"type":27,"value":442},"\u002Ftime-tracking\u002Ftimesheets",{"type":27,"value":444},", and makes it the default.",{"type":21,"tag":37,"props":446,"children":447},{},[448,458,468,486,496,506,516],{"type":21,"tag":41,"props":449,"children":450},{},[451,456],{"type":21,"tag":45,"props":452,"children":453},{},[454],{"type":27,"value":455},"Employee × day matrix",{"type":27,"value":457}," — 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 \u002F amber pending \u002F 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.",{"type":21,"tag":41,"props":459,"children":460},{},[461,466],{"type":21,"tag":45,"props":462,"children":463},{},[464],{"type":27,"value":465},"Click-to-drill",{"type":27,"value":467}," — 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 \u002F 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.",{"type":21,"tag":41,"props":469,"children":470},{},[471,476,478,484],{"type":21,"tag":45,"props":472,"children":473},{},[474],{"type":27,"value":475},"Drag-resizable columns",{"type":27,"value":477}," — 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 ",{"type":21,"tag":53,"props":479,"children":481},{"className":480},[],[482],{"type":27,"value":483},"localStorage",{"type":27,"value":485}," so 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.",{"type":21,"tag":41,"props":487,"children":488},{},[489,494],{"type":21,"tag":45,"props":490,"children":491},{},[492],{"type":27,"value":493},"Density toggle",{"type":27,"value":495}," — Compact (36–40px rows) \u002F Normal (52–60px) \u002F 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.",{"type":21,"tag":41,"props":497,"children":498},{},[499,504],{"type":21,"tag":45,"props":500,"children":501},{},[502],{"type":27,"value":503},"Sort and filter",{"type":27,"value":505}," — Sort by name, week hours ↑\u002F↓, pending count, or missing days (timesheets) \u002F unscheduled days (schedules). Filter chips at the top toggle between All \u002F Missing \u002F Pending \u002F Approved \u002F Rejected (timesheets) or All \u002F Unscheduled \u002F Scheduled \u002F 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.",{"type":21,"tag":41,"props":507,"children":508},{},[509,514],{"type":21,"tag":45,"props":510,"children":511},{},[512],{"type":27,"value":513},"Holidays and leave aware",{"type":27,"value":515}," — 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.",{"type":21,"tag":41,"props":517,"children":518},{},[519,524,526,532],{"type":21,"tag":45,"props":520,"children":521},{},[522],{"type":27,"value":523},"Calendar and List views still available",{"type":27,"value":525}," — The top bar's toggle is now three buttons instead of two: Team (default) \u002F Calendar \u002F List. The URL ",{"type":21,"tag":53,"props":527,"children":529},{"className":528},[],[530],{"type":27,"value":531},"?view=",{"type":27,"value":533}," 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.",{"type":21,"tag":30,"props":535,"children":537},{"id":536},"approvals-reports-ux-redesign",[538],{"type":27,"value":539},"Approvals & Reports UX Redesign",{"type":21,"tag":37,"props":541,"children":542},{},[543,560,601,618,664,735,839,888,925],{"type":21,"tag":41,"props":544,"children":545},{},[546,551,552,558],{"type":21,"tag":45,"props":547,"children":548},{},[549],{"type":27,"value":550},"Approvals dashboard overview",{"type":27,"value":136},{"type":21,"tag":53,"props":553,"children":555},{"className":554},[],[556],{"type":27,"value":557},"\u002Fapprovals",{"type":27,"value":559}," is 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.",{"type":21,"tag":41,"props":561,"children":562},{},[563,568,570,576,578,584,585,591,593,599],{"type":21,"tag":45,"props":564,"children":565},{},[566],{"type":27,"value":567},"Per-type approval pages",{"type":27,"value":569}," — Three new dedicated routes: ",{"type":21,"tag":53,"props":571,"children":573},{"className":572},[],[574],{"type":27,"value":575},"\u002Fapprovals\u002Fleave",{"type":27,"value":577},", ",{"type":21,"tag":53,"props":579,"children":581},{"className":580},[],[582],{"type":27,"value":583},"\u002Fapprovals\u002Fexpenses",{"type":27,"value":577},{"type":21,"tag":53,"props":586,"children":588},{"className":587},[],[589],{"type":27,"value":590},"\u002Fapprovals\u002Ftime",{"type":27,"value":592},". Each follows a consistent layout: breadcrumb + page header, search + filter dropdowns + export toolbar, a data table with ",{"type":21,"tag":53,"props":594,"children":596},{"className":595},[],[597],{"type":27,"value":598},"\u003Ccolgroup>",{"type":27,"value":600}," column widths, employee avatar cell, colored status \u002F type badges, row-click detail slideover, action dropdown menu per row, reject modal with a dedicated header \u002F body \u002F footer + live character counter + keyboard shortcut hint, and a bottom-pinned pagination footer with rows-per-page selector and prev\u002Fnext navigation.",{"type":21,"tag":41,"props":602,"children":603},{},[604,609,610,616],{"type":21,"tag":45,"props":605,"children":606},{},[607],{"type":27,"value":608},"Reports dashboard overview",{"type":27,"value":136},{"type":21,"tag":53,"props":611,"children":613},{"className":612},[],[614],{"type":27,"value":615},"\u002Freports",{"type":27,"value":617}," is 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\u002F10\u002F15\u002F20, 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\u002F5\u002F7\u002F10), 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.",{"type":21,"tag":41,"props":619,"children":620},{},[621,626,628,634,635,641,642,648,649,655,656,662],{"type":21,"tag":45,"props":622,"children":623},{},[624],{"type":27,"value":625},"Per-type report pages",{"type":27,"value":627}," — Five new dedicated routes: ",{"type":21,"tag":53,"props":629,"children":631},{"className":630},[],[632],{"type":27,"value":633},"\u002Freports\u002Fleave",{"type":27,"value":577},{"type":21,"tag":53,"props":636,"children":638},{"className":637},[],[639],{"type":27,"value":640},"\u002Freports\u002Fexpenses",{"type":27,"value":577},{"type":21,"tag":53,"props":643,"children":645},{"className":644},[],[646],{"type":27,"value":647},"\u002Freports\u002Ftime",{"type":27,"value":577},{"type":21,"tag":53,"props":650,"children":652},{"className":651},[],[653],{"type":27,"value":654},"\u002Freports\u002Fprojects",{"type":27,"value":577},{"type":21,"tag":53,"props":657,"children":659},{"className":658},[],[660],{"type":27,"value":661},"\u002Freports\u002Fscheduling",{"type":27,"value":663},". Each wraps the existing tab component inside a consistent shell (breadcrumb + page header). Permission gates and time-tracking redirects preserved.",{"type":21,"tag":41,"props":665,"children":666},{},[667,672,674,679,680,685,687,693,695,701,703,709,711,717,719,725,727,733],{"type":21,"tag":45,"props":668,"children":669},{},[670],{"type":27,"value":671},"Tab → query-param navigation",{"type":27,"value":673}," — Hash-based tab navigation on ",{"type":21,"tag":53,"props":675,"children":677},{"className":676},[],[678],{"type":27,"value":557},{"type":27,"value":436},{"type":21,"tag":53,"props":681,"children":683},{"className":682},[],[684],{"type":27,"value":615},{"type":27,"value":686}," (e.g. ",{"type":21,"tag":53,"props":688,"children":690},{"className":689},[],[691],{"type":27,"value":692},"#leave",{"type":27,"value":694},") replaced with query params (",{"type":21,"tag":53,"props":696,"children":698},{"className":697},[],[699],{"type":27,"value":700},"?tab=leave",{"type":27,"value":702},"). URLs are shareable, round-trip through Vue Router, and cooperate with existing drill-down query params (e.g. ",{"type":21,"tag":53,"props":704,"children":706},{"className":705},[],[707],{"type":27,"value":708},"?tab=time&userId=…",{"type":27,"value":710},"). A shared ",{"type":21,"tag":53,"props":712,"children":714},{"className":713},[],[715],{"type":27,"value":716},"useTabParam",{"type":27,"value":718}," composable handles the state, URL sync, and the async-validation edge case where ",{"type":21,"tag":53,"props":720,"children":722},{"className":721},[],[723],{"type":27,"value":724},"validTabs",{"type":27,"value":726}," depends on settings (e.g. ",{"type":21,"tag":53,"props":728,"children":730},{"className":729},[],[731],{"type":27,"value":732},"isTimeTrackingEnabled",{"type":27,"value":734},") that load after the initial render.",{"type":21,"tag":41,"props":736,"children":737},{},[738,743,744,750,752,758,759,765,766,772,773,779,780,786,787,793,794,800,801,807,808,814,815,821,823,829,831,837],{"type":21,"tag":45,"props":739,"children":740},{},[741],{"type":27,"value":742},"Shared UI primitives for approvals and reports",{"type":27,"value":51},{"type":21,"tag":53,"props":745,"children":747},{"className":746},[],[748],{"type":27,"value":749},"components\u002Fcommon\u002F",{"type":27,"value":751}," directory: ",{"type":21,"tag":53,"props":753,"children":755},{"className":754},[],[756],{"type":27,"value":757},"PageHeader",{"type":27,"value":577},{"type":21,"tag":53,"props":760,"children":762},{"className":761},[],[763],{"type":27,"value":764},"PageTabs",{"type":27,"value":577},{"type":21,"tag":53,"props":767,"children":769},{"className":768},[],[770],{"type":27,"value":771},"PageToolbar",{"type":27,"value":577},{"type":21,"tag":53,"props":774,"children":776},{"className":775},[],[777],{"type":27,"value":778},"StatCard",{"type":27,"value":577},{"type":21,"tag":53,"props":781,"children":783},{"className":782},[],[784],{"type":27,"value":785},"StatusBadge",{"type":27,"value":577},{"type":21,"tag":53,"props":788,"children":790},{"className":789},[],[791],{"type":27,"value":792},"SearchInput",{"type":27,"value":577},{"type":21,"tag":53,"props":795,"children":797},{"className":796},[],[798],{"type":27,"value":799},"FilterSelect",{"type":27,"value":577},{"type":21,"tag":53,"props":802,"children":804},{"className":803},[],[805],{"type":27,"value":806},"FilterBar",{"type":27,"value":577},{"type":21,"tag":53,"props":809,"children":811},{"className":810},[],[812],{"type":27,"value":813},"SlideOver",{"type":27,"value":577},{"type":21,"tag":53,"props":816,"children":818},{"className":817},[],[819],{"type":27,"value":820},"ActionMenu",{"type":27,"value":822},". The stat card uses an edge-joined ",{"type":21,"tag":53,"props":824,"children":826},{"className":825},[],[827],{"type":27,"value":828},"first:rounded-l-lg last:rounded-r-lg",{"type":27,"value":830}," strip pattern so a row of cards reads as a single unit. The action menu teleports its dropdown to ",{"type":21,"tag":53,"props":832,"children":834},{"className":833},[],[835],{"type":27,"value":836},"\u003Cbody>",{"type":27,"value":838}," with fixed positioning so it escapes any clipping ancestor (table wrappers, slideovers). The slideover is a right-aligned 400px drawer with backdrop click \u002F Esc to close. All built in raw Tailwind against the existing CSS variables so they match the rest of the product's design system.",{"type":21,"tag":41,"props":840,"children":841},{},[842,847,849,855,856,862,864,870,872,878,880,886],{"type":21,"tag":45,"props":843,"children":844},{},[845],{"type":27,"value":846},"Cost flow fix in time reports",{"type":27,"value":848}," — The Reports Time tab was dropping ",{"type":21,"tag":53,"props":850,"children":852},{"className":851},[],[853],{"type":27,"value":854},"cost",{"type":27,"value":577},{"type":21,"tag":53,"props":857,"children":859},{"className":858},[],[860],{"type":27,"value":861},"totalCost",{"type":27,"value":863},", and ",{"type":21,"tag":53,"props":865,"children":867},{"className":866},[],[868],{"type":27,"value":869},"entriesMissingRate",{"type":27,"value":871}," between the API response and the chart\u002Fcard render paths on the previous ",{"type":21,"tag":53,"props":873,"children":875},{"className":874},[],[876],{"type":27,"value":877},"by_employee",{"type":27,"value":879}," merge. Refactored the merge logic out into ",{"type":21,"tag":53,"props":881,"children":883},{"className":882},[],[884],{"type":27,"value":885},"utils\u002Ftime-report-shape.ts",{"type":27,"value":887}," with 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.",{"type":21,"tag":41,"props":889,"children":890},{},[891,896,897,902,903,908,910,916,918,923],{"type":21,"tag":45,"props":892,"children":893},{},[894],{"type":27,"value":895},"Full-width page treatment",{"type":27,"value":136},{"type":21,"tag":53,"props":898,"children":900},{"className":899},[],[901],{"type":27,"value":557},{"type":27,"value":577},{"type":21,"tag":53,"props":904,"children":906},{"className":905},[],[907],{"type":27,"value":615},{"type":27,"value":909},", and all 8 sub-pages drop the ",{"type":21,"tag":53,"props":911,"children":913},{"className":912},[],[914],{"type":27,"value":915},"max-w-7xl",{"type":27,"value":917}," cap and stretch to the full content-area width, matching the ",{"type":21,"tag":53,"props":919,"children":921},{"className":920},[],[922],{"type":27,"value":434},{"type":27,"value":924}," layout so wide tables don't leave empty side margins on large monitors.",{"type":21,"tag":41,"props":926,"children":927},{},[928,933,935,941,943,949,950,956,957,962,963,968],{"type":21,"tag":45,"props":929,"children":930},{},[931],{"type":27,"value":932},"Pass-through of avatar role colour + badges",{"type":27,"value":934}," — Every new approval\u002Freport surface uses the existing ",{"type":21,"tag":53,"props":936,"children":938},{"className":937},[],[939],{"type":27,"value":940},"\u003CUserAvatar>",{"type":27,"value":942}," component, so an employee's initials circle, role-tinted colour, and Administrator\u002FExecutive crown\u002Fstar badge render identically across ",{"type":21,"tag":53,"props":944,"children":946},{"className":945},[],[947],{"type":27,"value":948},"\u002Fusers",{"type":27,"value":577},{"type":21,"tag":53,"props":951,"children":953},{"className":952},[],[954],{"type":27,"value":955},"\u002Fcalendar",{"type":27,"value":577},{"type":21,"tag":53,"props":958,"children":960},{"className":959},[],[961],{"type":27,"value":557},{"type":27,"value":863},{"type":21,"tag":53,"props":964,"children":966},{"className":965},[],[967],{"type":27,"value":615},{"type":27,"value":969},". No new palette, no drift.",{"type":21,"tag":30,"props":971,"children":973},{"id":972},"settings-search",[974],{"type":27,"value":975},"Settings Search",{"type":21,"tag":37,"props":977,"children":978},{},[979,1005,1039,1056,1165,1191,1224,1241],{"type":21,"tag":41,"props":980,"children":981},{},[982,987,989,995,997,1003],{"type":21,"tag":45,"props":983,"children":984},{},[985],{"type":27,"value":986},"Global settings search bar in the top header",{"type":27,"value":988}," — On every ",{"type":21,"tag":53,"props":990,"children":992},{"className":991},[],[993],{"type":27,"value":994},"\u002Fsettings\u002F*",{"type":27,"value":996}," route, the top toolbar renders a compact Stripe-style search input (left-aligned with the page H1). Press ",{"type":21,"tag":53,"props":998,"children":1000},{"className":999},[],[1001],{"type":27,"value":1002},"\u002F",{"type":27,"value":1004}," anywhere on a settings page to focus it. Hidden on mobile to keep the toolbar usable.",{"type":21,"tag":41,"props":1006,"children":1007},{},[1008,1013,1015,1021,1023,1029,1031,1037],{"type":21,"tag":45,"props":1009,"children":1010},{},[1011],{"type":27,"value":1012},"Deep-linkable individual settings",{"type":27,"value":1014}," — Search results resolve to ",{"type":21,"tag":53,"props":1016,"children":1018},{"className":1017},[],[1019],{"type":27,"value":1020},"path#anchor",{"type":27,"value":1022}," (e.g. \"time zone\" → ",{"type":21,"tag":53,"props":1024,"children":1026},{"className":1025},[],[1027],{"type":27,"value":1028},"\u002Fsettings\u002Fgeneral#time-zone",{"type":27,"value":1030},", \"auto clock out\" → ",{"type":21,"tag":53,"props":1032,"children":1034},{"className":1033},[],[1035],{"type":27,"value":1036},"\u002Fsettings\u002Ftimetracking#auto-clock-out",{"type":27,"value":1038},"). Anchor IDs added across ~30 settings components covering General, Time Tracking, Expenses, Carry Forward, Leave Types, Departments, Holidays, Email, Policies, Documents, Training, Performance, Onboarding\u002FOffboarding, 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.",{"type":21,"tag":41,"props":1040,"children":1041},{},[1042,1047,1048,1054],{"type":21,"tag":45,"props":1043,"children":1044},{},[1045],{"type":27,"value":1046},"Hand-curated search index with keyword aliases",{"type":27,"value":136},{"type":21,"tag":53,"props":1049,"children":1051},{"className":1050},[],[1052],{"type":27,"value":1053},"composables\u002FuseSettingsSearch.ts",{"type":27,"value":1055}," defines 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\u002Fdescription\u002Fkeywords.",{"type":21,"tag":41,"props":1057,"children":1058},{},[1059,1064,1066,1072,1073,1079,1080,1086,1088,1094,1095,1101,1102,1108,1109,1115,1117,1123,1125,1131,1133,1139,1141,1147,1149,1155,1157,1163],{"type":21,"tag":45,"props":1060,"children":1061},{},[1062],{"type":27,"value":1063},"Billing-aware lock badges in results",{"type":27,"value":1065}," — Each result carries ",{"type":21,"tag":53,"props":1067,"children":1069},{"className":1068},[],[1070],{"type":27,"value":1071},"locked",{"type":27,"value":577},{"type":21,"tag":53,"props":1074,"children":1076},{"className":1075},[],[1077],{"type":27,"value":1078},"planLabel",{"type":27,"value":577},{"type":21,"tag":53,"props":1081,"children":1083},{"className":1082},[],[1084],{"type":27,"value":1085},"isAddOn",{"type":27,"value":1087},". Locked entries render a ",{"type":21,"tag":53,"props":1089,"children":1091},{"className":1090},[],[1092],{"type":27,"value":1093},"🔒 Pro",{"type":27,"value":260},{"type":21,"tag":53,"props":1096,"children":1098},{"className":1097},[],[1099],{"type":27,"value":1100},"🔒 Business",{"type":27,"value":260},{"type":21,"tag":53,"props":1103,"children":1105},{"className":1104},[],[1106],{"type":27,"value":1107},"🔒 Enterprise",{"type":27,"value":260},{"type":21,"tag":53,"props":1110,"children":1112},{"className":1111},[],[1113],{"type":27,"value":1114},"🔒 Add-on",{"type":27,"value":1116}," pill matching the badge style on ",{"type":21,"tag":53,"props":1118,"children":1120},{"className":1119},[],[1121],{"type":27,"value":1122},"\u002Fsettings",{"type":27,"value":1124}," overview 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 ",{"type":21,"tag":53,"props":1126,"children":1128},{"className":1127},[],[1129],{"type":27,"value":1130},"-25",{"type":27,"value":1132}," score penalty. Uses the same ",{"type":21,"tag":53,"props":1134,"children":1136},{"className":1135},[],[1137],{"type":27,"value":1138},"isFeatureLocked",{"type":27,"value":1140}," + ",{"type":21,"tag":53,"props":1142,"children":1144},{"className":1143},[],[1145],{"type":27,"value":1146},"getLockedPlanLabel",{"type":27,"value":1148}," helpers as ",{"type":21,"tag":53,"props":1150,"children":1152},{"className":1151},[],[1153],{"type":27,"value":1154},"\u002Fbilling",{"type":27,"value":1156}," so the labels stay consistent across the product. The bar calls ",{"type":21,"tag":53,"props":1158,"children":1160},{"className":1159},[],[1161],{"type":27,"value":1162},"loadSubscription()",{"type":27,"value":1164}," on mount to ensure badges reflect the org's actual plan.",{"type":21,"tag":41,"props":1166,"children":1167},{},[1168,1173,1175,1181,1183,1189],{"type":21,"tag":45,"props":1169,"children":1170},{},[1171],{"type":27,"value":1172},"Role-aware visibility",{"type":27,"value":1174}," — The search only indexes ",{"type":21,"tag":53,"props":1176,"children":1178},{"className":1177},[],[1179],{"type":27,"value":1180},"visibleItems",{"type":27,"value":1182}," from ",{"type":21,"tag":53,"props":1184,"children":1186},{"className":1185},[],[1187],{"type":27,"value":1188},"useSettingsNavigation",{"type":27,"value":1190},", so an employee searching for \"audit logs\" gets no result (the page is admin\u002Fexec only), while an administrator does. Department heads see onboarding\u002Foffboarding entries.",{"type":21,"tag":41,"props":1192,"children":1193},{},[1194,1199,1201,1207,1208,1214,1216,1222],{"type":21,"tag":45,"props":1195,"children":1196},{},[1197],{"type":27,"value":1198},"Tabbed-page auto-switch",{"type":27,"value":1200}," — On pages with tabs (Security, Connected Apps), navigating to ",{"type":21,"tag":53,"props":1202,"children":1204},{"className":1203},[],[1205],{"type":27,"value":1206},"#audit-logs",{"type":27,"value":260},{"type":21,"tag":53,"props":1209,"children":1211},{"className":1210},[],[1212],{"type":27,"value":1213},"#personal-webhooks",{"type":27,"value":1215}," \u002F etc. now auto-selects the matching tab via a ",{"type":21,"tag":53,"props":1217,"children":1219},{"className":1218},[],[1220],{"type":27,"value":1221},"watch(route.hash)",{"type":27,"value":1223}," wired to a static anchor-to-tab map, so a result click lands on actual content rather than the default tab.",{"type":21,"tag":41,"props":1225,"children":1226},{},[1227,1232,1234,1239],{"type":21,"tag":45,"props":1228,"children":1229},{},[1230],{"type":27,"value":1231},"Keyboard navigation",{"type":27,"value":1233}," — Arrow keys move the active row, Enter commits, Esc closes, ",{"type":21,"tag":53,"props":1235,"children":1237},{"className":1236},[],[1238],{"type":27,"value":1002},{"type":27,"value":1240}," global shortcut focuses the input (ignored when the user is already typing in another field).",{"type":21,"tag":41,"props":1242,"children":1243},{},[1244,1249,1251,1257,1259,1265,1267,1273],{"type":21,"tag":45,"props":1245,"children":1246},{},[1247],{"type":27,"value":1248},"First-paint layout stability",{"type":27,"value":1250}," — The authenticated layout's outer wrapper animated ",{"type":21,"tag":53,"props":1252,"children":1254},{"className":1253},[],[1255],{"type":27,"value":1256},"lg:pl-[68px]",{"type":27,"value":1258}," ↔ ",{"type":21,"tag":53,"props":1260,"children":1262},{"className":1261},[],[1263],{"type":27,"value":1264},"lg:pl-[260px]",{"type":27,"value":1266}," when the sidebar collapsed state hydrated from localStorage, dragging the header — including the search bar — left\u002Fright for a split second on every refresh. Suppressed the transition on the very first paint via a ",{"type":21,"tag":53,"props":1268,"children":1270},{"className":1269},[],[1271],{"type":27,"value":1272},"layoutReady",{"type":27,"value":1274}," flag so the layout snaps instantly; sidebar-toggle animation still works for later user interactions.",{"type":21,"tag":30,"props":1276,"children":1278},{"id":1277},"guided-tours-overhaul",[1279],{"type":27,"value":1280},"Guided Tours Overhaul",{"type":21,"tag":37,"props":1282,"children":1283},{},[1284,1323,1333,1343,1353],{"type":21,"tag":41,"props":1285,"children":1286},{},[1287,1292,1294,1299,1300,1306,1307,1313,1315,1321],{"type":21,"tag":45,"props":1288,"children":1289},{},[1290],{"type":27,"value":1291},"Redesigned popover",{"type":27,"value":1293}," — 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. ",{"type":21,"tag":53,"props":1295,"children":1297},{"className":1296},[],[1298],{"type":27,"value":1002},{"type":27,"value":577},{"type":21,"tag":53,"props":1301,"children":1303},{"className":1302},[],[1304],{"type":27,"value":1305},"A",{"type":27,"value":577},{"type":21,"tag":53,"props":1308,"children":1310},{"className":1309},[],[1311],{"type":27,"value":1312},"R",{"type":27,"value":1314},") 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 (respects ",{"type":21,"tag":53,"props":1316,"children":1318},{"className":1317},[],[1319],{"type":27,"value":1320},"prefers-reduced-motion",{"type":27,"value":1322},").",{"type":21,"tag":41,"props":1324,"children":1325},{},[1326,1331],{"type":21,"tag":45,"props":1327,"children":1328},{},[1329],{"type":27,"value":1330},"Rewritten tour copy across the app",{"type":27,"value":1332}," — 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.",{"type":21,"tag":41,"props":1334,"children":1335},{},[1336,1341],{"type":21,"tag":45,"props":1337,"children":1338},{},[1339],{"type":27,"value":1340},"Five new tours",{"type":27,"value":1342}," — 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 \u002F OSM provider switch, and the assignments step that most admins miss.",{"type":21,"tag":41,"props":1344,"children":1345},{},[1346,1351],{"type":21,"tag":45,"props":1347,"children":1348},{},[1349],{"type":27,"value":1350},"\"Don't show tours\" opt-out inside the popover",{"type":27,"value":1352}," — 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.",{"type":21,"tag":41,"props":1354,"children":1355},{},[1356,1361],{"type":21,"tag":45,"props":1357,"children":1358},{},[1359],{"type":27,"value":1360},"Smarter placement",{"type":27,"value":1362}," — 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.",{"type":21,"tag":30,"props":1364,"children":1366},{"id":1365},"page-redesigns-data-export",[1367],{"type":27,"value":1368},"Page Redesigns & Data Export",{"type":21,"tag":37,"props":1370,"children":1371},{},[1372,1405,1431,1441,1466,1504,1522,1540,1550,1560,1577,1609,1643,1653,1670,1680,1690,1700,1710,1720,1730,1740,1750,1760],{"type":21,"tag":41,"props":1373,"children":1374},{},[1375,1380,1382,1388,1390,1396,1397,1403],{"type":21,"tag":45,"props":1376,"children":1377},{},[1378],{"type":27,"value":1379},"Clean flat list view for timesheets",{"type":27,"value":1381}," — 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\u002Fpage default) keeps the API efficient. Checkbox multi-select supports bulk edit and bulk delete. The entries API endpoint now returns summary aggregates (approved\u002Fpending\u002Frejected minutes via ",{"type":21,"tag":53,"props":1383,"children":1385},{"className":1384},[],[1386],{"type":27,"value":1387},"groupBy",{"type":27,"value":1389},") alongside the page so header stats stay accurate across pagination. View mode (calendar\u002Flist) persists in the URL query param (",{"type":21,"tag":53,"props":1391,"children":1393},{"className":1392},[],[1394],{"type":27,"value":1395},"?view=calendar",{"type":27,"value":260},{"type":21,"tag":53,"props":1398,"children":1400},{"className":1399},[],[1401],{"type":27,"value":1402},"?view=list",{"type":27,"value":1404},") — no flash on refresh.",{"type":21,"tag":41,"props":1406,"children":1407},{},[1408,1413,1415,1421,1423,1429],{"type":21,"tag":45,"props":1409,"children":1410},{},[1411],{"type":27,"value":1412},"AG Grid export page for timesheets",{"type":27,"value":1414}," — Clicking the export button navigates to ",{"type":21,"tag":53,"props":1416,"children":1418},{"className":1417},[],[1419],{"type":27,"value":1420},"\u002Ftime-tracking\u002Ftimesheets\u002Fexport",{"type":27,"value":1422}," with the active date range and filters as query params. The page uses AG Grid Community (",{"type":21,"tag":53,"props":1424,"children":1426},{"className":1425},[],[1427],{"type":27,"value":1428},"ag-grid-vue3",{"type":27,"value":1430},") with: four theme variants (Quartz \u002F Alpine \u002F Balham \u002F Material) auto-switching to dark mode, custom Excel-style checkbox set filter per column (Select All \u002F Deselect All with search — replaces the Enterprise-only Set Filter), Columns dropdown to show\u002Fhide 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.",{"type":21,"tag":41,"props":1432,"children":1433},{},[1434,1439],{"type":21,"tag":45,"props":1435,"children":1436},{},[1437],{"type":27,"value":1438},"PDF preview in new tab",{"type":27,"value":1440}," — 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.",{"type":21,"tag":41,"props":1442,"children":1443},{},[1444,1449,1451,1456,1458,1464],{"type":21,"tag":45,"props":1445,"children":1446},{},[1447],{"type":27,"value":1448},"List view + export for schedules",{"type":27,"value":1450}," — All timesheet list-view features ported to ",{"type":21,"tag":53,"props":1452,"children":1454},{"className":1453},[],[1455],{"type":27,"value":434},{"type":27,"value":1457},": flat table (Date, Employee, Department, Shift Type badge, Time, Break, Duration, Notes, Actions), calendar\u002Flist toggle persisted in URL, AG Grid export page at ",{"type":21,"tag":53,"props":1459,"children":1461},{"className":1460},[],[1462],{"type":27,"value":1463},"\u002Fschedules\u002Fexport",{"type":27,"value":1465}," with 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.",{"type":21,"tag":41,"props":1467,"children":1468},{},[1469,1474,1475,1480,1482,1488,1490,1495,1497,1503],{"type":21,"tag":45,"props":1470,"children":1471},{},[1472],{"type":27,"value":1473},"Users page redesign",{"type":27,"value":136},{"type":21,"tag":53,"props":1476,"children":1478},{"className":1477},[],[1479],{"type":27,"value":948},{"type":27,"value":1481}," redesigned 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 ",{"type":21,"tag":53,"props":1483,"children":1485},{"className":1484},[],[1486],{"type":27,"value":1487},"\u003Ctable>",{"type":27,"value":1489}," 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\u002F20\u002F50). CSV export of filtered users. Full-width layout matching ",{"type":21,"tag":53,"props":1491,"children":1493},{"className":1492},[],[1494],{"type":27,"value":557},{"type":27,"value":1496},". AG Grid export at ",{"type":21,"tag":53,"props":1498,"children":1500},{"className":1499},[],[1501],{"type":27,"value":1502},"\u002Fusers-export",{"type":27,"value":168},{"type":21,"tag":41,"props":1505,"children":1506},{},[1507,1512,1514,1520],{"type":21,"tag":45,"props":1508,"children":1509},{},[1510],{"type":27,"value":1511},"Uniform stat chip styling",{"type":27,"value":1513}," — Timesheet and schedule top bars use consistent neutral-tone stat chips (",{"type":21,"tag":53,"props":1515,"children":1517},{"className":1516},[],[1518],{"type":27,"value":1519},"bg-muted\u002F60 ring-1 ring-border",{"type":27,"value":1521},") instead of rainbow-colored pills, matching the admin dashboard pattern across all pages.",{"type":21,"tag":41,"props":1523,"children":1524},{},[1525,1530,1532,1538],{"type":21,"tag":45,"props":1526,"children":1527},{},[1528],{"type":27,"value":1529},"Responsive top bar wrapping",{"type":27,"value":1531}," — Both timesheet and schedule top bars use ",{"type":21,"tag":53,"props":1533,"children":1535},{"className":1534},[],[1536],{"type":27,"value":1537},"flex-wrap gap-y-2",{"type":27,"value":1539}," so stat chips, view toggle, and action buttons wrap gracefully on narrow viewports instead of overflowing.",{"type":21,"tag":41,"props":1541,"children":1542},{},[1543,1548],{"type":21,"tag":45,"props":1544,"children":1545},{},[1546],{"type":27,"value":1547},"Sidebar visibility gating",{"type":27,"value":1549}," — Employee sidebar toggle button hidden in list view on both timesheets and schedules (sidebar is only relevant for the calendar grid).",{"type":21,"tag":41,"props":1551,"children":1552},{},[1553,1558],{"type":21,"tag":45,"props":1554,"children":1555},{},[1556],{"type":27,"value":1557},"Date range visibility gating",{"type":27,"value":1559}," — Date range filter row hidden in calendar view on both timesheets and schedules (calendar is fixed at 7-day columns).",{"type":21,"tag":41,"props":1561,"children":1562},{},[1563,1568,1569,1575],{"type":21,"tag":45,"props":1564,"children":1565},{},[1566],{"type":27,"value":1567},"Unified Export dropdown",{"type":27,"value":51},{"type":21,"tag":53,"props":1570,"children":1572},{"className":1571},[],[1573],{"type":27,"value":1574},"CommonExportDropdown",{"type":27,"value":1576}," component replaces separate CSV\u002FPDF\u002FExcel 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.",{"type":21,"tag":41,"props":1578,"children":1579},{},[1580,1585,1587,1593,1594,1600,1601,1607],{"type":21,"tag":45,"props":1581,"children":1582},{},[1583],{"type":27,"value":1584},"AG Grid export pages for approvals",{"type":27,"value":1586}," — Three new export pages: ",{"type":21,"tag":53,"props":1588,"children":1590},{"className":1589},[],[1591],{"type":27,"value":1592},"\u002Fapprovals\u002Fleave-export",{"type":27,"value":577},{"type":21,"tag":53,"props":1595,"children":1597},{"className":1596},[],[1598],{"type":27,"value":1599},"\u002Fapprovals\u002Fexpense-export",{"type":27,"value":577},{"type":21,"tag":53,"props":1602,"children":1604},{"className":1603},[],[1605],{"type":27,"value":1606},"\u002Fapprovals\u002Ftime-export",{"type":27,"value":1608}," — each with the full AG Grid tooling (theme picker, column visibility, custom set filters, quick filter, pinned totals).",{"type":21,"tag":41,"props":1610,"children":1611},{},[1612,1617,1619,1625,1627,1633,1635,1641],{"type":21,"tag":45,"props":1613,"children":1614},{},[1615],{"type":27,"value":1616},"Client-side branded PDF export",{"type":27,"value":1618}," — New shared utility ",{"type":21,"tag":53,"props":1620,"children":1622},{"className":1621},[],[1623],{"type":27,"value":1624},"utils\u002Fexport-pdf.ts",{"type":27,"value":1626}," generates branded PDFs client-side using pdf-lib. Fetches org logo + company name from ",{"type":21,"tag":53,"props":1628,"children":1630},{"className":1629},[],[1631],{"type":27,"value":1632},"\u002Fapi\u002Fbranding",{"type":27,"value":1634}," (which now falls back to ",{"type":21,"tag":53,"props":1636,"children":1638},{"className":1637},[],[1639],{"type":27,"value":1640},"SMTP_LOGO_URL",{"type":27,"value":1642}," env 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.",{"type":21,"tag":41,"props":1644,"children":1645},{},[1646,1651],{"type":21,"tag":45,"props":1647,"children":1648},{},[1649],{"type":27,"value":1650},"Manage Departments modal redesign",{"type":27,"value":1652}," — Compact header\u002Fbody\u002Ffooter 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.",{"type":21,"tag":41,"props":1654,"children":1655},{},[1656,1661,1663,1669],{"type":21,"tag":45,"props":1657,"children":1658},{},[1659],{"type":27,"value":1660},"Expenses page redesign",{"type":27,"value":1662}," — 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 ",{"type":21,"tag":53,"props":1664,"children":1666},{"className":1665},[],[1667],{"type":27,"value":1668},"\u002Fexpenses-export",{"type":27,"value":168},{"type":21,"tag":41,"props":1671,"children":1672},{},[1673,1678],{"type":21,"tag":45,"props":1674,"children":1675},{},[1676],{"type":27,"value":1677},"Expense approvals redesign",{"type":27,"value":1679}," — Admin-style table replacing grid-cols-12 layout. CommonActionMenu per row. Reject modal with header\u002Fbody\u002Ffooter sections, contextual chip, character counter. Pagination footer.",{"type":21,"tag":41,"props":1681,"children":1682},{},[1683,1688],{"type":21,"tag":45,"props":1684,"children":1685},{},[1686],{"type":27,"value":1687},"Expense reports redesign",{"type":27,"value":1689}," — 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.",{"type":21,"tag":41,"props":1691,"children":1692},{},[1693,1698],{"type":21,"tag":45,"props":1694,"children":1695},{},[1696],{"type":27,"value":1697},"Reports overview fixes",{"type":27,"value":1699}," — All 5 charts use settings gear overlay with period options (3 months \u002F 6 months \u002F 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\".",{"type":21,"tag":41,"props":1701,"children":1702},{},[1703,1708],{"type":21,"tag":45,"props":1704,"children":1705},{},[1706],{"type":27,"value":1707},"Leave report tab redesign",{"type":27,"value":1709}," — 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.",{"type":21,"tag":41,"props":1711,"children":1712},{},[1713,1718],{"type":21,"tag":45,"props":1714,"children":1715},{},[1716],{"type":27,"value":1717},"Expense report tab redesign",{"type":27,"value":1719}," — 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.",{"type":21,"tag":41,"props":1721,"children":1722},{},[1723,1728],{"type":21,"tag":45,"props":1724,"children":1725},{},[1726],{"type":27,"value":1727},"Time report tab redesign",{"type":27,"value":1729}," — Removed all charts (Area, Bar, Line, Donut). Admin-style data table with individual time entries, server-side pagination, CommonStatusBadge, CommonActionMenu. Stats use server aggregates.",{"type":21,"tag":41,"props":1731,"children":1732},{},[1733,1738],{"type":21,"tag":45,"props":1734,"children":1735},{},[1736],{"type":27,"value":1737},"Projects report tab redesign",{"type":27,"value":1739}," — Stat card strip, CommonPageToolbar with search and inline filters, admin-style table with project stats and progress bars, CommonActionMenu, pagination, export.",{"type":21,"tag":41,"props":1741,"children":1742},{},[1743,1748],{"type":21,"tag":45,"props":1744,"children":1745},{},[1746],{"type":27,"value":1747},"Scheduling report tab redesign",{"type":27,"value":1749}," — 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.",{"type":21,"tag":41,"props":1751,"children":1752},{},[1753,1758],{"type":21,"tag":45,"props":1754,"children":1755},{},[1756],{"type":27,"value":1757},"Projects list page redesign",{"type":27,"value":1759}," — Full-width, admin-style table with project color dots, status badges, progress bars, CommonActionMenu, pagination footer, export dropdown.",{"type":21,"tag":41,"props":1761,"children":1762},{},[1763,1768,1770,1776,1778,1784,1786,1791],{"type":21,"tag":45,"props":1764,"children":1765},{},[1766],{"type":27,"value":1767},"Documents page redesign",{"type":27,"value":1769}," — Full-width, admin-style table with CommonActionMenu and CommonStatusBadge. New ",{"type":21,"tag":53,"props":1771,"children":1773},{"className":1772},[],[1774],{"type":27,"value":1775},"CommonSegmentedTabs",{"type":27,"value":1777}," component replaces underline tab bar — pill-shaped toggle group with sliding primary-color indicator, ResizeObserver for responsive positioning. URL sync changed from ",{"type":21,"tag":53,"props":1779,"children":1781},{"className":1780},[],[1782],{"type":27,"value":1783},"#hash",{"type":27,"value":1785}," to ",{"type":21,"tag":53,"props":1787,"children":1789},{"className":1788},[],[1790],{"type":27,"value":531},{"type":27,"value":1792}," query params.",{"type":21,"tag":22,"props":1794,"children":1796},{"id":1795},"improvements-fixes",[1797],{"type":27,"value":1798},"Improvements & Fixes",{"type":21,"tag":30,"props":1800,"children":1802},{"id":1801},"ux-refinements",[1803],{"type":27,"value":1804},"UX Refinements",{"type":21,"tag":37,"props":1806,"children":1807},{},[1808,1877,1895,1912,1954],{"type":21,"tag":41,"props":1809,"children":1810},{},[1811,1816,1817,1823,1825,1830,1832,1837,1839,1845,1847,1852,1854,1859,1861,1867,1869,1875],{"type":21,"tag":45,"props":1812,"children":1813},{},[1814],{"type":27,"value":1815},"Org chart list view matches the \u002Fusers table styling",{"type":27,"value":278},{"type":21,"tag":53,"props":1818,"children":1820},{"className":1819},[],[1821],{"type":27,"value":1822},"\u002Fusers\u002Forgchart?view=list",{"type":27,"value":1824}," People page has been realigned with the admin-dashboard ",{"type":21,"tag":53,"props":1826,"children":1828},{"className":1827},[],[1829],{"type":27,"value":1487},{"type":27,"value":1831}," treatment used on ",{"type":21,"tag":53,"props":1833,"children":1835},{"className":1834},[],[1836],{"type":27,"value":948},{"type":27,"value":1838},": a single shared ",{"type":21,"tag":53,"props":1840,"children":1842},{"className":1841},[],[1843],{"type":27,"value":1844},"CommonPageToolbar",{"type":27,"value":1846}," with search + Department \u002F Employment Type \u002F Status inline filters, a proper ",{"type":21,"tag":53,"props":1848,"children":1850},{"className":1849},[],[1851],{"type":27,"value":1487},{"type":27,"value":1853}," with ",{"type":21,"tag":53,"props":1855,"children":1857},{"className":1856},[],[1858],{"type":27,"value":598},{"type":27,"value":1860}," column widths and rounded header cells, inline ",{"type":21,"tag":53,"props":1862,"children":1864},{"className":1863},[],[1865],{"type":27,"value":1866},"UserAvatar",{"type":27,"value":1868}," cells, ",{"type":21,"tag":53,"props":1870,"children":1872},{"className":1871},[],[1873],{"type":27,"value":1874},"CommonStatusBadge",{"type":27,"value":1876}," pills for the employment-type status (Full-time \u002F Part-time \u002F Contract \u002F Intern \u002F Temporary \u002F Seasonal), and the standard rows-per-page pagination footer (10 \u002F 20 \u002F 50). Employment-type tones are mapped through the shared StatusBadge palette so colors stay consistent with the rest of the product. Column visibility (Employee # \u002F Department \u002F Location) is preserved as a trailing toolbar action. Mobile switches to a card layout.",{"type":21,"tag":41,"props":1878,"children":1879},{},[1880,1885,1887,1893],{"type":21,"tag":45,"props":1881,"children":1882},{},[1883],{"type":27,"value":1884},"Current-session indicator on \u002Fsettings\u002Ftrusted-devices",{"type":27,"value":1886}," — 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 ",{"type":21,"tag":1888,"props":1889,"children":1890},"em",{},[1891],{"type":27,"value":1892},"and",{"type":27,"value":1894}," 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.",{"type":21,"tag":41,"props":1896,"children":1897},{},[1898,1903,1905,1910],{"type":21,"tag":45,"props":1899,"children":1900},{},[1901],{"type":27,"value":1902},"Persisted day-row heights on the schedule grid",{"type":27,"value":1904}," — When a user drags the row-resize handle on ",{"type":21,"tag":53,"props":1906,"children":1908},{"className":1907},[],[1909],{"type":27,"value":434},{"type":27,"value":1911}," to 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.",{"type":21,"tag":41,"props":1913,"children":1914},{},[1915,1920,1922,1928,1930,1936,1938,1944,1946,1952],{"type":21,"tag":45,"props":1916,"children":1917},{},[1918],{"type":27,"value":1919},"Leave history no longer shows phantom -1 day deductions for non-deducting types",{"type":27,"value":1921}," — The History table on ",{"type":21,"tag":53,"props":1923,"children":1925},{"className":1924},[],[1926],{"type":27,"value":1927},"\u002Fusers\u002F:id?tab=timeoff",{"type":27,"value":1929}," was 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 ",{"type":21,"tag":53,"props":1931,"children":1933},{"className":1932},[],[1934],{"type":27,"value":1935},"\u002Fcalendar\u002F:id",{"type":27,"value":1937},". A new shared ",{"type":21,"tag":53,"props":1939,"children":1941},{"className":1940},[],[1942],{"type":27,"value":1943},"utils\u002FleaveBucket.ts",{"type":27,"value":1945}," helper mirrors the same ANNUAL \u002F SICK \u002F NONE classification used server-side (including the legacy fallback for rows that predate the ",{"type":21,"tag":53,"props":1947,"children":1949},{"className":1948},[],[1950],{"type":27,"value":1951},"deductionBucket",{"type":27,"value":1953}," column), and the History cell now shows \"—\" with an explanatory tooltip for leaves that don't deduct.",{"type":21,"tag":41,"props":1955,"children":1956},{},[1957,1962,1963,1969,1971,1977],{"type":21,"tag":45,"props":1958,"children":1959},{},[1960],{"type":27,"value":1961},"Mark all tours completed in one click",{"type":27,"value":136},{"type":21,"tag":53,"props":1964,"children":1966},{"className":1965},[],[1967],{"type":27,"value":1968},"\u002Fsettings\u002Ftours",{"type":27,"value":1970}," gains 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. ",{"type":21,"tag":53,"props":1972,"children":1974},{"className":1973},[],[1975],{"type":27,"value":1976},"useGuidedTour.completeTours(ids)",{"type":27,"value":1978}," is the underlying helper for any future flow that needs to mark tours complete in bulk.",{"type":21,"tag":30,"props":1980,"children":1982},{"id":1981},"bug-fixes",[1983],{"type":27,"value":1984},"Bug Fixes",{"type":21,"tag":37,"props":1986,"children":1987},{},[1988,2022,2040,2073,2083,2124,2149,2159,2201,2227,2244,2278,2319,2344,2385,2402,2420,2438,2448,2466,2484,2501,2511,2521,2600,2640,2689,2732,2756],{"type":21,"tag":41,"props":1989,"children":1990},{},[1991,2004,2006,2012,2014,2020],{"type":21,"tag":45,"props":1992,"children":1993},{},[1994,1996,2002],{"type":27,"value":1995},"Notification settings (",{"type":21,"tag":53,"props":1997,"children":1999},{"className":1998},[],[2000],{"type":27,"value":2001},"\u002Fsettings\u002Fnotifications",{"type":27,"value":2003},") crashed for orgs with legacy defaults",{"type":27,"value":2005}," — When an organization's saved notification defaults were stored in an older shape, the page would throw ",{"type":21,"tag":53,"props":2007,"children":2009},{"className":2008},[],[2010],{"type":27,"value":2011},"Cannot read properties of undefined (reading 'enabled')",{"type":27,"value":2013}," instead of rendering. The server endpoint now normalises partial payloads through the shared ",{"type":21,"tag":53,"props":2015,"children":2017},{"className":2016},[],[2018],{"type":27,"value":2019},"resolveNotificationPreferences",{"type":27,"value":2021}," helper, and the client merges fetched values over the form's defaults so a missing nested key can no longer blow up the view.",{"type":21,"tag":41,"props":2023,"children":2024},{},[2025,2030,2032,2038],{"type":21,"tag":45,"props":2026,"children":2027},{},[2028],{"type":27,"value":2029},"Password manager hints on settings",{"type":27,"value":2031}," — Added the right ",{"type":21,"tag":53,"props":2033,"children":2035},{"className":2034},[],[2036],{"type":27,"value":2037},"autocomplete",{"type":27,"value":2039}," attributes to every password \u002F 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.",{"type":21,"tag":41,"props":2041,"children":2042},{},[2043,2048,2050,2055,2057,2063,2065,2071],{"type":21,"tag":45,"props":2044,"children":2045},{},[2046],{"type":27,"value":2047},"Due-date calculation ignored BEFORE\u002FAFTER direction",{"type":27,"value":2049}," — Template tasks configured as \"7 days before hire date\" were being created with due dates ",{"type":21,"tag":1888,"props":2051,"children":2052},{},[2053],{"type":27,"value":2054},"after",{"type":27,"value":2056}," the hire date (e.g. hire May 1 → laptop task due May 8 instead of Apr 24). The frontend import path in ",{"type":21,"tag":53,"props":2058,"children":2060},{"className":2059},[],[2061],{"type":27,"value":2062},"pages\u002Fusers\u002F[id]\u002Findex.vue",{"type":27,"value":2064}," was always adding the offset; it now defers to the server which honors ",{"type":21,"tag":53,"props":2066,"children":2068},{"className":2067},[],[2069],{"type":27,"value":2070},"dueDaysDirection: 'BEFORE' | 'AFTER'",{"type":27,"value":2072}," with UTC date arithmetic.",{"type":21,"tag":41,"props":2074,"children":2075},{},[2076,2081],{"type":21,"tag":45,"props":2077,"children":2078},{},[2079],{"type":27,"value":2080},"\"Start Onboarding\" silently picked the first template",{"type":27,"value":2082}," — 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.",{"type":21,"tag":41,"props":2084,"children":2085},{},[2086,2091,2093,2099,2101,2107,2109,2115,2116,2122],{"type":21,"tag":45,"props":2087,"children":2088},{},[2089],{"type":27,"value":2090},"\"Import Task List\" merged into an existing list",{"type":27,"value":2092}," — Selecting IT Setup when HR was already attached appended IT tasks into the HR card, erasing the grouping. Import now calls ",{"type":21,"tag":53,"props":2094,"children":2096},{"className":2095},[],[2097],{"type":27,"value":2098},"POST \u002Fapi\u002Fonboarding\u002Finstances",{"type":27,"value":2100}," with the template ID, creating a separate ",{"type":21,"tag":53,"props":2102,"children":2104},{"className":2103},[],[2105],{"type":27,"value":2106},"OnboardingInstance",{"type":27,"value":2108}," with its own ",{"type":21,"tag":53,"props":2110,"children":2112},{"className":2111},[],[2113],{"type":27,"value":2114},"templateName",{"type":27,"value":1002},{"type":21,"tag":53,"props":2117,"children":2119},{"className":2118},[],[2120],{"type":27,"value":2121},"templateIcon",{"type":27,"value":2123}," snapshots so each imported list renders as its own card.",{"type":21,"tag":41,"props":2125,"children":2126},{},[2127,2132,2134,2140,2141,2147],{"type":21,"tag":45,"props":2128,"children":2129},{},[2130],{"type":27,"value":2131},"Already-imported templates were re-selectable",{"type":27,"value":2133}," — The Import and Start modals showed every template every time, allowing duplicate imports of the same list onto one user. Both modals now use ",{"type":21,"tag":53,"props":2135,"children":2137},{"className":2136},[],[2138],{"type":27,"value":2139},"availableTemplatesForType",{"type":27,"value":260},{"type":21,"tag":53,"props":2142,"children":2144},{"className":2143},[],[2145],{"type":27,"value":2146},"availableTemplatesForStart",{"type":27,"value":2148}," computeds that filter out templates already present in the user's instances.",{"type":21,"tag":41,"props":2150,"children":2151},{},[2152,2157],{"type":21,"tag":45,"props":2153,"children":2154},{},[2155],{"type":27,"value":2156},"Employees could mark their own onboarding tasks complete",{"type":27,"value":2158}," — The subject user (the employee being onboarded) was able to tick off their own checklist items, including completing compliance tasks that require admin\u002FHR 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.",{"type":21,"tag":41,"props":2160,"children":2161},{},[2162,2167,2169,2175,2177,2183,2185,2191,2193,2199],{"type":21,"tag":45,"props":2163,"children":2164},{},[2165],{"type":27,"value":2166},"Signed documents did not auto-complete their linked tasks",{"type":27,"value":2168}," — When an employee signed a document attached to an onboarding task, the task stayed in ",{"type":21,"tag":53,"props":2170,"children":2172},{"className":2171},[],[2173],{"type":27,"value":2174},"PENDING",{"type":27,"value":2176}," status until someone manually ticked it. Added ",{"type":21,"tag":53,"props":2178,"children":2180},{"className":2179},[],[2181],{"type":27,"value":2182},"autoCompleteTasksForSignedDocument",{"type":27,"value":2184}," which fires from the document sign handler, matches tasks by ",{"type":21,"tag":53,"props":2186,"children":2188},{"className":2187},[],[2189],{"type":27,"value":2190},"(documentId, instance.userId)",{"type":27,"value":2192},", flips status to ",{"type":21,"tag":53,"props":2194,"children":2196},{"className":2195},[],[2197],{"type":27,"value":2198},"COMPLETED",{"type":27,"value":2200},", rolls up instance status, and dispatches the task-completed notification. Works for both the direct per-user clone and the source-template document cases.",{"type":21,"tag":41,"props":2202,"children":2203},{},[2204,2209,2211,2217,2219,2225],{"type":21,"tag":45,"props":2205,"children":2206},{},[2207],{"type":27,"value":2208},"Task list modal was too small and cut off the icon grid",{"type":27,"value":2210}," — The \"New Task List\" modal was ",{"type":21,"tag":53,"props":2212,"children":2214},{"className":2213},[],[2215],{"type":27,"value":2216},"max-width=\"sm\"",{"type":27,"value":2218}," which made picking an icon require scrolling a 47-item grid inside a 256px container. Bumped to ",{"type":21,"tag":53,"props":2220,"children":2222},{"className":2221},[],[2223],{"type":27,"value":2224},"max-width=\"2xl\"",{"type":27,"value":2226}," with a two-column layout (Name + Department side-by-side) and the icon picker given full breathing room below.",{"type":21,"tag":41,"props":2228,"children":2229},{},[2230,2235,2236,2242],{"type":21,"tag":45,"props":2231,"children":2232},{},[2233],{"type":27,"value":2234},"Shrine icon rendered as a blank square",{"type":27,"value":136},{"type":21,"tag":53,"props":2237,"children":2239},{"className":2238},[],[2240],{"type":27,"value":2241},"lucide:shrine",{"type":27,"value":2243}," is 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).",{"type":21,"tag":41,"props":2245,"children":2246},{},[2247,2252,2254,2260,2262,2268,2270,2276],{"type":21,"tag":45,"props":2248,"children":2249},{},[2250],{"type":27,"value":2251},"Department heads could not manage their own team's task lists",{"type":27,"value":2253}," — Only ADMINISTRATOR and EXECUTIVE could CRUD onboarding templates, forcing every HR\u002FIT\u002FSales lead to route changes through an admin. Added ",{"type":21,"tag":53,"props":2255,"children":2257},{"className":2256},[],[2258],{"type":27,"value":2259},"OnboardingTemplate.departmentId",{"type":27,"value":2261}," (nullable). Department heads can now create, edit, delete, and manage task lists scoped to their own department. Templates with ",{"type":21,"tag":53,"props":2263,"children":2265},{"className":2264},[],[2266],{"type":27,"value":2267},"departmentId: null",{"type":27,"value":2269}," stay admin\u002Fexec only. Enforced via the new ",{"type":21,"tag":53,"props":2271,"children":2273},{"className":2272},[],[2274],{"type":27,"value":2275},"server\u002Futils\u002Fonboarding-access.ts",{"type":27,"value":2277}," helper across all template, taskdef, instance, and per-user-task endpoints.",{"type":21,"tag":41,"props":2279,"children":2280},{},[2281,2286,2288,2294,2296,2302,2304,2310,2311,2317],{"type":21,"tag":45,"props":2282,"children":2283},{},[2284],{"type":27,"value":2285},"Department heads could not see Onboarding \u002F Offboarding in \u002Fsettings",{"type":27,"value":2287}," — The settings sidebar and page wrappers had hard ",{"type":21,"tag":53,"props":2289,"children":2291},{"className":2290},[],[2292],{"type":27,"value":2293},"adminOnly",{"type":27,"value":2295}," gates. Introduced a ",{"type":21,"tag":53,"props":2297,"children":2299},{"className":2298},[],[2300],{"type":27,"value":2301},"deptHeadAllowed",{"type":27,"value":2303}," flag on ",{"type":21,"tag":53,"props":2305,"children":2307},{"className":2306},[],[2308],{"type":27,"value":2309},"SettingsNavItem",{"type":27,"value":436},{"type":21,"tag":53,"props":2312,"children":2314},{"className":2313},[],[2315],{"type":27,"value":2316},"SettingsPageWrapper",{"type":27,"value":2318},". Onboarding and Offboarding now opt in; department heads see both entries in their settings sidebar and can open either page.",{"type":21,"tag":41,"props":2320,"children":2321},{},[2322,2327,2329,2335,2336,2342],{"type":21,"tag":45,"props":2323,"children":2324},{},[2325],{"type":27,"value":2326},"DH saw all templates on the settings page",{"type":27,"value":2328}," — After unlocking the page for DH, they were seeing org-wide (admin-only) task lists they couldn't edit. Added ",{"type":21,"tag":53,"props":2330,"children":2332},{"className":2331},[],[2333],{"type":27,"value":2334},"?manageableOnly=true",{"type":27,"value":1785},{"type":21,"tag":53,"props":2337,"children":2339},{"className":2338},[],[2340],{"type":27,"value":2341},"GET \u002Fapi\u002Fonboarding\u002Ftemplates",{"type":27,"value":2343},". The settings page passes this flag, so a DH only sees their own department's templates. The user-page Import\u002FStart flow still fetches the full visible set (org-wide + dept) because DHs should be able to import admin-built lists for their team.",{"type":21,"tag":41,"props":2345,"children":2346},{},[2347,2352,2354,2360,2362,2368,2370,2376,2378,2384],{"type":21,"tag":45,"props":2348,"children":2349},{},[2350],{"type":27,"value":2351},"403 on \u002Fapi\u002Fonboarding\u002Fpackets for department heads",{"type":27,"value":2353}," — Opening ",{"type":21,"tag":53,"props":2355,"children":2357},{"className":2356},[],[2358],{"type":27,"value":2359},"\u002Fsettings\u002Fonboarding-templates",{"type":27,"value":2361}," as a DH crashed the data load because the packets endpoint is admin-only. ",{"type":21,"tag":53,"props":2363,"children":2365},{"className":2364},[],[2366],{"type":27,"value":2367},"loadData()",{"type":27,"value":2369}," now skips the packets fetch entirely for non-admins and hides the \"New Hire Packet Templates\" tab. If a DH lands on ",{"type":21,"tag":53,"props":2371,"children":2373},{"className":2372},[],[2374],{"type":27,"value":2375},"?tab=packets",{"type":27,"value":2377}," directly, it coerces to ",{"type":21,"tag":53,"props":2379,"children":2381},{"className":2380},[],[2382],{"type":27,"value":2383},"tasks",{"type":27,"value":168},{"type":21,"tag":41,"props":2386,"children":2387},{},[2388,2393,2395,2400],{"type":21,"tag":45,"props":2389,"children":2390},{},[2391],{"type":27,"value":2392},"Misleading \"All departments (admin-only)\" dropdown label",{"type":27,"value":2394}," — The department selector in the New\u002FEdit Task List modal didn't make clear which ",{"type":21,"tag":1888,"props":2396,"children":2397},{},[2398],{"type":27,"value":2399},"roles",{"type":27,"value":2401}," 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.",{"type":21,"tag":41,"props":2403,"children":2404},{},[2405,2410,2412,2418],{"type":21,"tag":45,"props":2406,"children":2407},{},[2408],{"type":27,"value":2409},"Task list cards had no spacing between them",{"type":27,"value":2411}," — Multiple onboarding\u002Foffboarding instances rendered flush against each other with no gap. Added ",{"type":21,"tag":53,"props":2413,"children":2415},{"className":2414},[],[2416],{"type":27,"value":2417},"mb-4 last:mb-0",{"type":27,"value":2419}," to each instance card on both the Onboarding and Offboarding tabs.",{"type":21,"tag":41,"props":2421,"children":2422},{},[2423,2428,2430,2436],{"type":21,"tag":45,"props":2424,"children":2425},{},[2426],{"type":27,"value":2427},"Add Task button disappeared from the user's onboarding tab",{"type":27,"value":2429}," — 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 ",{"type":21,"tag":53,"props":2431,"children":2433},{"className":2432},[],[2434],{"type":27,"value":2435},"+",{"type":27,"value":2437}," 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.",{"type":21,"tag":41,"props":2439,"children":2440},{},[2441,2446],{"type":21,"tag":45,"props":2442,"children":2443},{},[2444],{"type":27,"value":2445},"Per-user Add Task modal missed fields that existed in the template modal",{"type":27,"value":2447}," — 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\u002FSome)\" and \"Update existing employee tasks\" fields because per-user tasks only ever apply to one user.",{"type":21,"tag":41,"props":2449,"children":2450},{},[2451,2456,2458,2464],{"type":21,"tag":45,"props":2452,"children":2453},{},[2454],{"type":27,"value":2455},"Assigned Role dropdown in the per-user Add Task modal was redundant",{"type":27,"value":2457}," — 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 ",{"type":21,"tag":53,"props":2459,"children":2461},{"className":2460},[],[2462],{"type":27,"value":2463},"'HR'",{"type":27,"value":2465}," when not provided, and the template flow keeps the full role picker since template tasks may be authored without a specific user in mind.",{"type":21,"tag":41,"props":2467,"children":2468},{},[2469,2474,2476,2482],{"type":21,"tag":45,"props":2470,"children":2471},{},[2472],{"type":27,"value":2473},"Task completion emails only went to the assignee",{"type":27,"value":2475}," — When a task was marked complete (or auto-completed by a document signing), no one in HR\u002Fmanagement learned about it. Added ",{"type":21,"tag":53,"props":2477,"children":2479},{"className":2478},[],[2480],{"type":27,"value":2481},"sendOnboardingTaskCompletedNotification",{"type":27,"value":2483}," which emails administrators, executives, AND the task assignee (deduped). Includes the employee name in the subject line and body.",{"type":21,"tag":41,"props":2485,"children":2486},{},[2487,2492,2494,2499],{"type":21,"tag":45,"props":2488,"children":2489},{},[2490],{"type":27,"value":2491},"Due-date reminders only went to one person",{"type":27,"value":2493}," — The 1-day-before \u002F 3-day \u002F 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 ",{"type":21,"tag":1888,"props":2495,"children":2496},{},[2497],{"type":27,"value":2498},"not",{"type":27,"value":2500}," reminded because they can't mark their own tasks complete.",{"type":21,"tag":41,"props":2502,"children":2503},{},[2504,2509],{"type":21,"tag":45,"props":2505,"children":2506},{},[2507],{"type":27,"value":2508},"Task removed → no notification",{"type":27,"value":2510}," — Deleting a task from an active instance silently disappeared the row. Admins\u002Fexecs\u002FDH 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.",{"type":21,"tag":41,"props":2512,"children":2513},{},[2514,2519],{"type":21,"tag":45,"props":2515,"children":2516},{},[2517],{"type":27,"value":2518},"Instance removed → no notification",{"type":27,"value":2520}," — Same for removing an entire task list from a user's profile. Admins\u002Fexecs\u002FDH are now notified with the list name, task count deleted, and who removed it.",{"type":21,"tag":41,"props":2522,"children":2523},{},[2524,2529,2531,2537,2539,2545,2547,2553,2555,2561,2562,2568,2569,2575,2576,2582,2584,2590,2592,2598],{"type":21,"tag":45,"props":2525,"children":2526},{},[2527],{"type":27,"value":2528},"No audit trail for onboarding\u002Foffboarding settings changes",{"type":27,"value":2530}," — Template and task mutations were not appearing in ",{"type":21,"tag":53,"props":2532,"children":2534},{"className":2533},[],[2535],{"type":27,"value":2536},"\u002Fsettings\u002Fsecurity → Audit Logs",{"type":27,"value":2538},". Added ",{"type":21,"tag":53,"props":2540,"children":2542},{"className":2541},[],[2543],{"type":27,"value":2544},"logOnboardingAudit",{"type":27,"value":2546}," helper that writes ",{"type":21,"tag":53,"props":2548,"children":2550},{"className":2549},[],[2551],{"type":27,"value":2552},"AuditLog",{"type":27,"value":2554}," entries with IP + user-agent for every CREATE\u002FUPDATE\u002FDELETE on ",{"type":21,"tag":53,"props":2556,"children":2558},{"className":2557},[],[2559],{"type":27,"value":2560},"ONBOARDING_TEMPLATE",{"type":27,"value":577},{"type":21,"tag":53,"props":2563,"children":2565},{"className":2564},[],[2566],{"type":27,"value":2567},"ONBOARDING_TASK_DEF",{"type":27,"value":577},{"type":21,"tag":53,"props":2570,"children":2572},{"className":2571},[],[2573],{"type":27,"value":2574},"ONBOARDING_INSTANCE",{"type":27,"value":863},{"type":21,"tag":53,"props":2577,"children":2579},{"className":2578},[],[2580],{"type":27,"value":2581},"ONBOARDING_TASK",{"type":27,"value":2583},". UPDATEs include before\u002Fafter snapshots. Task PATCHes distinguish ",{"type":21,"tag":53,"props":2585,"children":2587},{"className":2586},[],[2588],{"type":27,"value":2589},"reason: STATUS",{"type":27,"value":2591}," (marking complete\u002Fincomplete) from ",{"type":21,"tag":53,"props":2593,"children":2595},{"className":2594},[],[2596],{"type":27,"value":2597},"reason: EDIT",{"type":27,"value":2599}," (field change) so the audit feed is actionable.",{"type":21,"tag":41,"props":2601,"children":2602},{},[2603,2608,2610,2615,2616,2622,2624,2630,2632,2638],{"type":21,"tag":45,"props":2604,"children":2605},{},[2606],{"type":27,"value":2607},"No way to reorder tasks within a task list",{"type":27,"value":2609}," — Once a task was added, its sort order was locked unless you deleted and recreated it. Each task row in ",{"type":21,"tag":53,"props":2611,"children":2613},{"className":2612},[],[2614],{"type":27,"value":2359},{"type":27,"value":436},{"type":21,"tag":53,"props":2617,"children":2619},{"className":2618},[],[2620],{"type":27,"value":2621},"\u002Fsettings\u002Foffboarding-templates",{"type":27,"value":2623}," now has a grip handle and is ",{"type":21,"tag":53,"props":2625,"children":2627},{"className":2626},[],[2628],{"type":27,"value":2629},"draggable=\"true\"",{"type":27,"value":2631},". Drag to reorder; ",{"type":21,"tag":53,"props":2633,"children":2635},{"className":2634},[],[2636],{"type":27,"value":2637},"sortOrder",{"type":27,"value":2639}," is renumbered locally and each changed row is PATCHed. Optimistic UI — reverts on error.",{"type":21,"tag":41,"props":2641,"children":2642},{},[2643,2648,2650,2656,2658,2664,2665,2671,2673,2679,2681,2687],{"type":21,"tag":45,"props":2644,"children":2645},{},[2646],{"type":27,"value":2647},"Email CTA buttons used a hardcoded purple",{"type":27,"value":2649}," — All onboarding\u002Foffboarding email action buttons (\"View My Tasks\", \"View Task\", \"View Onboarding\", \"View Profile\") rendered ",{"type":21,"tag":53,"props":2651,"children":2653},{"className":2652},[],[2654],{"type":27,"value":2655},"#4f46e5",{"type":27,"value":2657}," regardless of the organization's white-label branding. Added ",{"type":21,"tag":53,"props":2659,"children":2661},{"className":2660},[],[2662],{"type":27,"value":2663},"primaryColor",{"type":27,"value":1785},{"type":21,"tag":53,"props":2666,"children":2668},{"className":2667},[],[2669],{"type":27,"value":2670},"EmailConfig",{"type":27,"value":2672}," which reads ",{"type":21,"tag":53,"props":2674,"children":2676},{"className":2675},[],[2677],{"type":27,"value":2678},"CustomDomain.primaryColor",{"type":27,"value":2680}," (falling back to ",{"type":21,"tag":53,"props":2682,"children":2684},{"className":2683},[],[2685],{"type":27,"value":2686},"#3B82F6",{"type":27,"value":2688},"). All eight CTA buttons across the onboarding email templates now render the organization's brand color.",{"type":21,"tag":41,"props":2690,"children":2691},{},[2692,2702,2704,2709,2710,2715,2717,2722,2724,2730],{"type":21,"tag":45,"props":2693,"children":2694},{},[2695,2700],{"type":21,"tag":53,"props":2696,"children":2698},{"className":2697},[],[2699],{"type":27,"value":2106},{"type":27,"value":2701}," lost its identity if the template was edited or deleted",{"type":27,"value":2703}," — When an admin renamed or deleted a template, every instance using it suddenly showed \"Onboarding\" as its card title or crashed on dereference. Added ",{"type":21,"tag":53,"props":2705,"children":2707},{"className":2706},[],[2708],{"type":27,"value":2114},{"type":27,"value":1140},{"type":21,"tag":53,"props":2711,"children":2713},{"className":2712},[],[2714],{"type":27,"value":2121},{"type":27,"value":2716}," snapshots on ",{"type":21,"tag":53,"props":2718,"children":2720},{"className":2719},[],[2721],{"type":27,"value":2106},{"type":27,"value":2723},". Each imported\u002Fstarted list keeps its own identity even after the source template changes. The UI prefers the snapshot (",{"type":21,"tag":53,"props":2725,"children":2727},{"className":2726},[],[2728],{"type":27,"value":2729},"inst.templateName",{"type":27,"value":2731},") over the live relation.",{"type":21,"tag":41,"props":2733,"children":2734},{},[2735,2746,2748,2754],{"type":21,"tag":45,"props":2736,"children":2737},{},[2738,2744],{"type":21,"tag":53,"props":2739,"children":2741},{"className":2740},[],[2742],{"type":27,"value":2743},"canCompleteTask",{"type":27,"value":2745}," in the frontend allowed the instance owner to complete their own tasks",{"type":27,"value":2747}," — Mirrored the backend fix in the ",{"type":21,"tag":53,"props":2749,"children":2751},{"className":2750},[],[2752],{"type":27,"value":2753},"taskDetail",{"type":27,"value":2755}," completion gate. The modal's \"Mark Complete\" button now requires admin\u002Fexec, the explicit assignee, or a matching department head — not just \"is this your own instance\".",{"type":21,"tag":41,"props":2757,"children":2758},{},[2759,2764,2766,2771,2773,2779,2781,2786],{"type":21,"tag":45,"props":2760,"children":2761},{},[2762],{"type":27,"value":2763},"Saving a time-tracking setting could fail with a 400 after the page was reloaded",{"type":27,"value":2765}," — Toggling any switch on ",{"type":21,"tag":53,"props":2767,"children":2769},{"className":2768},[],[2770],{"type":27,"value":166},{"type":27,"value":2772}," (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 as ",{"type":21,"tag":53,"props":2774,"children":2776},{"className":2775},[],[2777],{"type":27,"value":2778},"null",{"type":27,"value":2780}," on save — which a strict type check was then rejecting. The save handler now treats ",{"type":21,"tag":53,"props":2782,"children":2784},{"className":2783},[],[2785],{"type":27,"value":2778},{"type":27,"value":2787}," the 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.",{"type":21,"tag":30,"props":2789,"children":2791},{"id":2790},"security-compliance",[2792],{"type":27,"value":2793},"Security & Compliance",{"type":21,"tag":37,"props":2795,"children":2796},{},[2797,2822,2855,2879,2897,2929,2947,2957,2967,2977,2987,2997,3007,3017,3027,3037,3047],{"type":21,"tag":41,"props":2798,"children":2799},{},[2800,2805,2807,2812,2814,2820],{"type":21,"tag":45,"props":2801,"children":2802},{},[2803],{"type":27,"value":2804},"Department-head scope enforcement",{"type":27,"value":2806}," — A department head cannot use a template scoped to another department (enforced on ",{"type":21,"tag":53,"props":2808,"children":2810},{"className":2809},[],[2811],{"type":27,"value":2098},{"type":27,"value":2813}," when they try to pass a ",{"type":21,"tag":53,"props":2815,"children":2817},{"className":2816},[],[2818],{"type":27,"value":2819},"templateId",{"type":27,"value":2821}," belonging to a different department). They also cannot re-scope a template between departments via PATCH.",{"type":21,"tag":41,"props":2823,"children":2824},{},[2825,2830,2832,2838,2840,2845,2847,2853],{"type":21,"tag":45,"props":2826,"children":2827},{},[2828],{"type":27,"value":2829},"Template-move authorization",{"type":27,"value":2831}," — Moving a task definition between templates (via ",{"type":21,"tag":53,"props":2833,"children":2835},{"className":2834},[],[2836],{"type":27,"value":2837},"PATCH \u002Fapi\u002Fonboarding\u002Ftemplates\u002F[id]\u002Ftasks\u002F[taskDefId]",{"type":27,"value":2839}," with a new ",{"type":21,"tag":53,"props":2841,"children":2843},{"className":2842},[],[2844],{"type":27,"value":2819},{"type":27,"value":2846},") now re-runs ",{"type":21,"tag":53,"props":2848,"children":2850},{"className":2849},[],[2851],{"type":27,"value":2852},"assertCanManageTemplate",{"type":27,"value":2854}," against the target template's department. A DH cannot move a task into a template they don't own.",{"type":21,"tag":41,"props":2856,"children":2857},{},[2858,2863,2864,2869,2871,2877],{"type":21,"tag":45,"props":2859,"children":2860},{},[2861],{"type":27,"value":2862},"Document auto-complete cross-user safety",{"type":27,"value":136},{"type":21,"tag":53,"props":2865,"children":2867},{"className":2866},[],[2868],{"type":27,"value":2182},{"type":27,"value":2870}," filters by ",{"type":21,"tag":53,"props":2872,"children":2874},{"className":2873},[],[2875],{"type":27,"value":2876},"instance.userId === signerUserId",{"type":27,"value":2878},", so signing a document only completes the signer's own tasks — never another user's task that happens to reference the same source document.",{"type":21,"tag":41,"props":2880,"children":2881},{},[2882,2887,2889,2895],{"type":21,"tag":45,"props":2883,"children":2884},{},[2885],{"type":27,"value":2886},"DH departmentId self-pin on create",{"type":27,"value":2888}," — When a department head creates a new task list, the backend always pins ",{"type":21,"tag":53,"props":2890,"children":2892},{"className":2891},[],[2893],{"type":27,"value":2894},"departmentId",{"type":27,"value":2896}," to their own department regardless of what the request sent, closing the gap if the UI-level dropdown is manipulated.",{"type":21,"tag":41,"props":2898,"children":2899},{},[2900,2905,2907,2912,2913,2919,2921,2927],{"type":21,"tag":45,"props":2901,"children":2902},{},[2903],{"type":27,"value":2904},"Tenant isolation on time-entry and project mutations",{"type":27,"value":2906}," — Foreign-key fields written on time-entry create\u002Fedit (",{"type":21,"tag":53,"props":2908,"children":2910},{"className":2909},[],[2911],{"type":27,"value":384},{"type":27,"value":577},{"type":21,"tag":53,"props":2914,"children":2916},{"className":2915},[],[2917],{"type":27,"value":2918},"taskId",{"type":27,"value":2920},") and project create\u002Fedit (",{"type":21,"tag":53,"props":2922,"children":2924},{"className":2923},[],[2925],{"type":27,"value":2926},"managerId",{"type":27,"value":2928},") are validated against the caller's organization at every write path.",{"type":21,"tag":41,"props":2930,"children":2931},{},[2932,2937,2939,2945],{"type":21,"tag":45,"props":2933,"children":2934},{},[2935],{"type":27,"value":2936},"Department-head scope on timesheet PDF export",{"type":27,"value":2938}," — When a department head runs the timesheet PDF, results are constrained to their department's users on every code path; an out-of-department ",{"type":21,"tag":53,"props":2940,"children":2942},{"className":2941},[],[2943],{"type":27,"value":2944},"userId",{"type":27,"value":2946}," filter returns 403 instead of an empty report so the caller knows the request was denied.",{"type":21,"tag":41,"props":2948,"children":2949},{},[2950,2955],{"type":21,"tag":45,"props":2951,"children":2952},{},[2953],{"type":27,"value":2954},"Project financials visibility",{"type":27,"value":2956}," — 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.",{"type":21,"tag":41,"props":2958,"children":2959},{},[2960,2965],{"type":21,"tag":45,"props":2961,"children":2962},{},[2963],{"type":27,"value":2964},"Lockdown-override audit trail",{"type":27,"value":2966}," — Every admin\u002Fexec 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.",{"type":21,"tag":41,"props":2968,"children":2969},{},[2970,2975],{"type":21,"tag":45,"props":2971,"children":2972},{},[2973],{"type":27,"value":2974},"Hardened external fetch in PDF generator",{"type":27,"value":2976}," — 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.",{"type":21,"tag":41,"props":2978,"children":2979},{},[2980,2985],{"type":21,"tag":45,"props":2981,"children":2982},{},[2983],{"type":27,"value":2984},"Stricter input validation on time-entry edits",{"type":27,"value":2986}," — Break duration must be a non-negative finite number and cannot exceed the entry's total span. Invalid clock-in \u002F clock-out ranges are rejected up front so downstream billing math never sees a negative duration.",{"type":21,"tag":41,"props":2988,"children":2989},{},[2990,2995],{"type":21,"tag":45,"props":2991,"children":2992},{},[2993],{"type":27,"value":2994},"Strengthened input validation across all time-entry endpoints",{"type":27,"value":2996}," — 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.",{"type":21,"tag":41,"props":2998,"children":2999},{},[3000,3005],{"type":21,"tag":45,"props":3001,"children":3002},{},[3003],{"type":27,"value":3004},"Improved multi-tenant isolation on write paths",{"type":27,"value":3006}," — 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.",{"type":21,"tag":41,"props":3008,"children":3009},{},[3010,3015],{"type":21,"tag":45,"props":3011,"children":3012},{},[3013],{"type":27,"value":3014},"Tighter scoping for department-level roles",{"type":27,"value":3016}," — 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.",{"type":21,"tag":41,"props":3018,"children":3019},{},[3020,3025],{"type":21,"tag":45,"props":3021,"children":3022},{},[3023],{"type":27,"value":3024},"Hardened settings update endpoint",{"type":27,"value":3026}," — Boolean, numeric, and enum fields on the time-tracking settings endpoint are validated by type and range before persistence. Numeric settings enforce documented min\u002Fmax bounds.",{"type":21,"tag":41,"props":3028,"children":3029},{},[3030,3035],{"type":21,"tag":45,"props":3031,"children":3032},{},[3033],{"type":27,"value":3034},"Soft-deleted entries excluded from aggregations",{"type":27,"value":3036}," — Report endpoints and project statistics queries now consistently exclude soft-deleted records, ensuring accurate totals and preventing stale data from surfacing in dashboards.",{"type":21,"tag":41,"props":3038,"children":3039},{},[3040,3045],{"type":21,"tag":45,"props":3041,"children":3042},{},[3043],{"type":27,"value":3044},"Sanitized custom CSS in branding",{"type":27,"value":3046}," — The branding endpoint strips potentially dangerous CSS constructs (dynamic URLs, imports, expressions, script bindings) from custom CSS before serving it to clients.",{"type":21,"tag":41,"props":3048,"children":3049},{},[3050,3055,3057,3063,3065,3071,3073,3079,3081,3087],{"type":21,"tag":45,"props":3051,"children":3052},{},[3053],{"type":27,"value":3054},"Bumped vulnerable transitive dependencies",{"type":27,"value":3056}," — Updated ",{"type":21,"tag":53,"props":3058,"children":3060},{"className":3059},[],[3061],{"type":27,"value":3062},"follow-redirects",{"type":27,"value":3064}," (1.15.11 → 1.16.0), ",{"type":21,"tag":53,"props":3066,"children":3068},{"className":3067},[],[3069],{"type":27,"value":3070},"protocol-buffers-schema",{"type":27,"value":3072}," (3.6.0 → 3.6.1), ",{"type":21,"tag":53,"props":3074,"children":3076},{"className":3075},[],[3077],{"type":27,"value":3078},"dompurify",{"type":27,"value":3080}," (3.3.3 → 3.4.0), and ",{"type":21,"tag":53,"props":3082,"children":3084},{"className":3083},[],[3085],{"type":27,"value":3086},"hono",{"type":27,"value":3088}," (4.12.12 → 4.12.14) via npm overrides to resolve Dependabot security advisories.",{"type":21,"tag":30,"props":3090,"children":3092},{"id":3091},"performance",[3093],{"type":27,"value":3094},"Performance",{"type":21,"tag":37,"props":3096,"children":3097},{},[3098,3115,3199,3209,3235,3261,3284,3316,3348,3378,3395,3435,3453,3479,3520,3592,3618,3667,3716,3748,3765,3783,3822,3924,3957,3989,4034,4072,4098,4123,4215,4232],{"type":21,"tag":41,"props":3099,"children":3100},{},[3101,3106,3107,3113],{"type":21,"tag":45,"props":3102,"children":3103},{},[3104],{"type":27,"value":3105},"Search debounce (300ms)",{"type":27,"value":136},{"type":21,"tag":53,"props":3108,"children":3110},{"className":3109},[],[3111],{"type":27,"value":3112},"CommonSearchInput",{"type":27,"value":3114}," now debounces emit by 300ms, preventing re-filtering on every keystroke across all pages. Single fix, global impact.",{"type":21,"tag":41,"props":3116,"children":3117},{},[3118,3123,3124,3130,3132,3138,3139,3145,3147,3153,3155,3161,3163,3168,3170,3176,3177,3182,3183,3189,3191,3197],{"type":21,"tag":45,"props":3119,"children":3120},{},[3121],{"type":27,"value":3122},"Leaves endpoint server-side pagination",{"type":27,"value":136},{"type":21,"tag":53,"props":3125,"children":3127},{"className":3126},[],[3128],{"type":27,"value":3129},"GET \u002Fapi\u002Freports\u002Fleaves-data",{"type":27,"value":3131}," now supports ",{"type":21,"tag":53,"props":3133,"children":3135},{"className":3134},[],[3136],{"type":27,"value":3137},"page",{"type":27,"value":1002},{"type":21,"tag":53,"props":3140,"children":3142},{"className":3141},[],[3143],{"type":27,"value":3144},"limit",{"type":27,"value":3146}," params. Returns ",{"type":21,"tag":53,"props":3148,"children":3150},{"className":3149},[],[3151],{"type":27,"value":3152},"pagination",{"type":27,"value":3154}," metadata + ",{"type":21,"tag":53,"props":3156,"children":3158},{"className":3157},[],[3159],{"type":27,"value":3160},"summary",{"type":27,"value":3162}," aggregates (approved\u002Fpending\u002Frejected counts + totalDays via ",{"type":21,"tag":53,"props":3164,"children":3166},{"className":3165},[],[3167],{"type":27,"value":1387},{"type":27,"value":3169},"). Runs ",{"type":21,"tag":53,"props":3171,"children":3173},{"className":3172},[],[3174],{"type":27,"value":3175},"count",{"type":27,"value":577},{"type":21,"tag":53,"props":3178,"children":3180},{"className":3179},[],[3181],{"type":27,"value":1387},{"type":27,"value":863},{"type":21,"tag":53,"props":3184,"children":3186},{"className":3185},[],[3187],{"type":27,"value":3188},"findMany",{"type":27,"value":3190}," in ",{"type":21,"tag":53,"props":3192,"children":3194},{"className":3193},[],[3195],{"type":27,"value":3196},"Promise.all",{"type":27,"value":3198}," for zero extra latency.",{"type":21,"tag":41,"props":3200,"children":3201},{},[3202,3207],{"type":21,"tag":45,"props":3203,"children":3204},{},[3205],{"type":27,"value":3206},"LeaveTab server-side pagination",{"type":27,"value":3208}," — 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.",{"type":21,"tag":41,"props":3210,"children":3211},{},[3212,3217,3219,3225,3227,3233],{"type":21,"tag":45,"props":3213,"children":3214},{},[3215],{"type":27,"value":3216},"Projects N+1 query eliminated",{"type":27,"value":3218}," — Replaced per-project ",{"type":21,"tag":53,"props":3220,"children":3222},{"className":3221},[],[3223],{"type":27,"value":3224},"Promise.all(projects.map(aggregate))",{"type":27,"value":3226}," (N individual queries) with a single ",{"type":21,"tag":53,"props":3228,"children":3230},{"className":3229},[],[3231],{"type":27,"value":3232},"timeEntry.groupBy({ by: ['projectId'] })",{"type":27,"value":3234}," query mapped back by project ID. O(N) → O(1).",{"type":21,"tag":41,"props":3236,"children":3237},{},[3238,3243,3245,3251,3253,3259],{"type":21,"tag":45,"props":3239,"children":3240},{},[3241],{"type":27,"value":3242},"TimeTab server-side pagination",{"type":27,"value":3244}," — Uses ",{"type":21,"tag":53,"props":3246,"children":3248},{"className":3247},[],[3249],{"type":27,"value":3250},"serverTotal",{"type":27,"value":3252}," for real entry count and ",{"type":21,"tag":53,"props":3254,"children":3256},{"className":3255},[],[3257],{"type":27,"value":3258},"serverSummary",{"type":27,"value":3260}," for accurate hours across all pages (not just current page of 50).",{"type":21,"tag":41,"props":3262,"children":3263},{},[3264,3269,3270,3275,3277,3282],{"type":21,"tag":45,"props":3265,"children":3266},{},[3267],{"type":27,"value":3268},"Branding endpoint SMTP fallback",{"type":27,"value":136},{"type":21,"tag":53,"props":3271,"children":3273},{"className":3272},[],[3274],{"type":27,"value":1632},{"type":27,"value":3276}," now falls back to ",{"type":21,"tag":53,"props":3278,"children":3280},{"className":3279},[],[3281],{"type":27,"value":1640},{"type":27,"value":3283}," env var when no white-label logo is configured, ensuring PDF exports always have a logo.",{"type":21,"tag":41,"props":3285,"children":3286},{},[3287,3292,3293,3298,3300,3306,3308,3314],{"type":21,"tag":45,"props":3288,"children":3289},{},[3290],{"type":27,"value":3291},"Reports overview now loads in one pass",{"type":27,"value":136},{"type":21,"tag":53,"props":3294,"children":3296},{"className":3295},[],[3297],{"type":27,"value":615},{"type":27,"value":3299}," was firing roughly seventy requests on mount to paint five stat cards and six charts. The summary strip now calls a single new ",{"type":21,"tag":53,"props":3301,"children":3303},{"className":3302},[],[3304],{"type":27,"value":3305},"GET \u002Fapi\u002Freports\u002Fsummary-stats",{"type":27,"value":3307}," endpoint that returns all five KPI numbers via DB aggregates; the leave-by-employee chart reads the existing ",{"type":21,"tag":53,"props":3309,"children":3311},{"className":3310},[],[3312],{"type":27,"value":3313},"\u002Fapi\u002Freports\u002Fleaves-data",{"type":27,"value":3315}," endpoint 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.",{"type":21,"tag":41,"props":3317,"children":3318},{},[3319,3324,3325,3330,3332,3338,3340,3346],{"type":21,"tag":45,"props":3320,"children":3321},{},[3322],{"type":27,"value":3323},"Approvals overview uses one counts endpoint",{"type":27,"value":278},{"type":21,"tag":53,"props":3326,"children":3328},{"className":3327},[],[3329],{"type":27,"value":557},{"type":27,"value":3331}," page 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 new ",{"type":21,"tag":53,"props":3333,"children":3335},{"className":3334},[],[3336],{"type":27,"value":3337},"GET \u002Fapi\u002Fapprovals\u002Fcounts",{"type":27,"value":3339}," endpoint returns ",{"type":21,"tag":53,"props":3341,"children":3343},{"className":3342},[],[3344],{"type":27,"value":3345},"{ leave, expense, time }",{"type":27,"value":3347}," 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.",{"type":21,"tag":41,"props":3349,"children":3350},{},[3351,3356,3358,3363,3365,3370,3371,3376],{"type":21,"tag":45,"props":3352,"children":3353},{},[3354],{"type":27,"value":3355},"Faster time approvals",{"type":27,"value":3357}," — Approving or rejecting a timesheet entry on ",{"type":21,"tag":53,"props":3359,"children":3361},{"className":3360},[],[3362],{"type":27,"value":590},{"type":27,"value":3364}," used 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. ",{"type":21,"tag":53,"props":3366,"children":3368},{"className":3367},[],[3369],{"type":27,"value":575},{"type":27,"value":436},{"type":21,"tag":53,"props":3372,"children":3374},{"className":3373},[],[3375],{"type":27,"value":583},{"type":27,"value":3377}," already worked this way; this brings timesheets in line.",{"type":21,"tag":41,"props":3379,"children":3380},{},[3381,3386,3388,3393],{"type":21,"tag":45,"props":3382,"children":3383},{},[3384],{"type":27,"value":3385},"Dashboard reads in parallel",{"type":27,"value":3387}," — 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 ",{"type":21,"tag":53,"props":3389,"children":3391},{"className":3390},[],[3392],{"type":27,"value":3196},{"type":27,"value":3394},". No query shape changes — the cold-render latency on large orgs drops proportionally to the slowest query instead of the sum.",{"type":21,"tag":41,"props":3396,"children":3397},{},[3398,3403,3405,3410,3412,3418,3420,3425,3427,3433],{"type":21,"tag":45,"props":3399,"children":3400},{},[3401],{"type":27,"value":3402},"Schedule auto-generation cut from hundreds of queries to one",{"type":27,"value":3404}," — When a team has the default-schedule feature turned on, loading ",{"type":21,"tag":53,"props":3406,"children":3408},{"className":3407},[],[3409],{"type":27,"value":434},{"type":27,"value":3411}," was doing a per-employee-per-day ",{"type":21,"tag":53,"props":3413,"children":3415},{"className":3414},[],[3416],{"type":27,"value":3417},"findFirst",{"type":27,"value":3419}," to decide whether to create a shift. A typical 50-person, 5-day week ran 250+ individual queries. A single ",{"type":21,"tag":53,"props":3421,"children":3423},{"className":3422},[],[3424],{"type":27,"value":3188},{"type":27,"value":3426}," now covers the whole range and the existence check happens against an in-memory ",{"type":21,"tag":53,"props":3428,"children":3430},{"className":3429},[],[3431],{"type":27,"value":3432},"Map",{"type":27,"value":3434},". 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.",{"type":21,"tag":41,"props":3436,"children":3437},{},[3438,3443,3445,3451],{"type":21,"tag":45,"props":3439,"children":3440},{},[3441],{"type":27,"value":3442},"Leave balance handler parallelizes lookups",{"type":27,"value":3444}," — The three independent reads at the top of ",{"type":21,"tag":53,"props":3446,"children":3448},{"className":3447},[],[3449],{"type":27,"value":3450},"GET \u002Fapi\u002Fleaves\u002Fbalance",{"type":27,"value":3452}," (current user, organization settings, target user) now fire together. Permission checks still gate access to the target user's data for non-self callers.",{"type":21,"tag":41,"props":3454,"children":3455},{},[3456,3469,3471,3477],{"type":21,"tag":45,"props":3457,"children":3458},{},[3459,3461,3467],{"type":27,"value":3460},"People grid on ",{"type":21,"tag":53,"props":3462,"children":3464},{"className":3463},[],[3465],{"type":27,"value":3466},"\u002Ftime-tracking\u002Fpeople",{"type":27,"value":3468}," uses DB aggregates",{"type":27,"value":3470}," — 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 uses ",{"type":21,"tag":53,"props":3472,"children":3474},{"className":3473},[],[3475],{"type":27,"value":3476},"prisma.timeEntry.groupBy",{"type":27,"value":3478}," for 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\".",{"type":21,"tag":41,"props":3480,"children":3481},{},[3482,3487,3488,3494,3496,3502,3504,3510,3512,3518],{"type":21,"tag":45,"props":3483,"children":3484},{},[3485],{"type":27,"value":3486},"Department filter on time reports pushed to the DB",{"type":27,"value":278},{"type":21,"tag":53,"props":3489,"children":3491},{"className":3490},[],[3492],{"type":27,"value":3493},"\u002Ftime-tracking\u002Freports",{"type":27,"value":3495}," endpoint had the same department filter expressed both in the ",{"type":21,"tag":53,"props":3497,"children":3499},{"className":3498},[],[3500],{"type":27,"value":3501},"where",{"type":27,"value":3503}," clause and again as an in-memory ",{"type":21,"tag":53,"props":3505,"children":3507},{"className":3506},[],[3508],{"type":27,"value":3509},"Array.filter",{"type":27,"value":3511}," after the fetch. The redundant in-memory pass is gone; admin-supplied ",{"type":21,"tag":53,"props":3513,"children":3515},{"className":3514},[],[3516],{"type":27,"value":3517},"?departmentId=…",{"type":27,"value":3519}," now goes to the DB too so it doesn't scan rows just to throw them away.",{"type":21,"tag":41,"props":3521,"children":3522},{},[3523,3528,3530,3536,3537,3543,3545,3551,3553,3559,3561,3567,3569,3574,3576,3582,3584,3590],{"type":21,"tag":45,"props":3524,"children":3525},{},[3526],{"type":27,"value":3527},"Composable fetches in parallel",{"type":27,"value":3529}," — Department-head paths in ",{"type":21,"tag":53,"props":3531,"children":3533},{"className":3532},[],[3534],{"type":27,"value":3535},"useTimesheetCalendar",{"type":27,"value":436},{"type":21,"tag":53,"props":3538,"children":3540},{"className":3539},[],[3541],{"type":27,"value":3542},"useScheduleCalendar",{"type":27,"value":3544}," used to ",{"type":21,"tag":53,"props":3546,"children":3548},{"className":3547},[],[3549],{"type":27,"value":3550},"await \u002Fapi\u002Fusers",{"type":27,"value":3552}," and then ",{"type":21,"tag":53,"props":3554,"children":3556},{"className":3555},[],[3557],{"type":27,"value":3558},"await \u002Fapi\u002Fusers\u002F{id}",{"type":27,"value":3560}," in sequence when both can start immediately. Same on the cold-boot ",{"type":21,"tag":53,"props":3562,"children":3564},{"className":3563},[],[3565],{"type":27,"value":3566},"useDashboard",{"type":27,"value":3568}," call for departments + leave types. All three now fire in parallel via ",{"type":21,"tag":53,"props":3570,"children":3572},{"className":3571},[],[3573],{"type":27,"value":3196},{"type":27,"value":3575},". The tour\u002Ftimesheet calendar also gets a microtask-level debounce around ",{"type":21,"tag":53,"props":3577,"children":3579},{"className":3578},[],[3580],{"type":27,"value":3581},"fetchEntries()",{"type":27,"value":3583}," 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 ",{"type":21,"tag":53,"props":3585,"children":3587},{"className":3586},[],[3588],{"type":27,"value":3589},"\u002Fapi\u002Fusers\u002F{id}",{"type":27,"value":3591}," on every calendar navigation.",{"type":21,"tag":41,"props":3593,"children":3594},{},[3595,3600,3602,3608,3610,3616],{"type":21,"tag":45,"props":3596,"children":3597},{},[3598],{"type":27,"value":3599},"Cross-tab notification dedup",{"type":27,"value":3601}," — The unread-count poller in ",{"type":21,"tag":53,"props":3603,"children":3605},{"className":3604},[],[3606],{"type":27,"value":3607},"useNotifications",{"type":27,"value":3609}," runs in every open tab. Each tab still polls (so a backgrounded tab can't go stale), but the result is now broadcast over a ",{"type":21,"tag":53,"props":3611,"children":3613},{"className":3612},[],[3614],{"type":27,"value":3615},"BroadcastChannel",{"type":27,"value":3617}," so 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.",{"type":21,"tag":41,"props":3619,"children":3620},{},[3621,3626,3627,3633,3635,3641,3643,3649,3651,3657,3659,3665],{"type":21,"tag":45,"props":3622,"children":3623},{},[3624],{"type":27,"value":3625},"Domain middleware merged into a single query",{"type":27,"value":136},{"type":21,"tag":53,"props":3628,"children":3630},{"className":3629},[],[3631],{"type":27,"value":3632},"server\u002Fmiddleware\u002Fdomain.ts",{"type":27,"value":3634}," used to find the ",{"type":21,"tag":53,"props":3636,"children":3638},{"className":3637},[],[3639],{"type":27,"value":3640},"CustomDomain",{"type":27,"value":3642}," row and then, on a hit, do a second ",{"type":21,"tag":53,"props":3644,"children":3646},{"className":3645},[],[3647],{"type":27,"value":3648},"findUnique",{"type":27,"value":3650}," on the owning ",{"type":21,"tag":53,"props":3652,"children":3654},{"className":3653},[],[3655],{"type":27,"value":3656},"Organization",{"type":27,"value":3658}," to check plan\u002Ffeatures. The org is now loaded via a relation ",{"type":21,"tag":53,"props":3660,"children":3662},{"className":3661},[],[3663],{"type":27,"value":3664},"include",{"type":27,"value":3666}," on the first call, so the cache-miss path is a single DB round-trip per unique host.",{"type":21,"tag":41,"props":3668,"children":3669},{},[3670,3675,3677,3683,3684,3690,3692,3698,3700,3706,3708,3714],{"type":21,"tag":45,"props":3671,"children":3672},{},[3673],{"type":27,"value":3674},"Request-scoped plan cache in feature-gate",{"type":27,"value":3676}," — Endpoints that run two or more feature checks in a single request (e.g. an expense-create that checks ",{"type":21,"tag":53,"props":3678,"children":3680},{"className":3679},[],[3681],{"type":27,"value":3682},"checkReceiptLimit",{"type":27,"value":436},{"type":21,"tag":53,"props":3685,"children":3687},{"className":3686},[],[3688],{"type":27,"value":3689},"checkFeatureAccess",{"type":27,"value":3691},") used to re-query the organization row for each check. A new internal ",{"type":21,"tag":53,"props":3693,"children":3695},{"className":3694},[],[3696],{"type":27,"value":3697},"getOrgBasicCached(event, organizationId)",{"type":27,"value":3699}," helper stashes the plan\u002Ffeatures\u002Flimits shape on ",{"type":21,"tag":53,"props":3701,"children":3703},{"className":3702},[],[3704],{"type":27,"value":3705},"event.context",{"type":27,"value":3707}," for the duration of the request, so subsequent checks in the same handler are free. Callers that don't pass ",{"type":21,"tag":53,"props":3709,"children":3711},{"className":3710},[],[3712],{"type":27,"value":3713},"event",{"type":27,"value":3715}," keep the old behaviour — nothing breaks.",{"type":21,"tag":41,"props":3717,"children":3718},{},[3719,3724,3725,3730,3732,3738,3740,3746],{"type":21,"tag":45,"props":3720,"children":3721},{},[3722],{"type":27,"value":3723},"Reports charts consolidated",{"type":27,"value":278},{"type":21,"tag":53,"props":3726,"children":3728},{"className":3727},[],[3729],{"type":27,"value":615},{"type":27,"value":3731}," page's four chart fetchers (expenses, time, project, schedule) now all hit a single ",{"type":21,"tag":53,"props":3733,"children":3735},{"className":3734},[],[3736],{"type":27,"value":3737},"\u002Fapi\u002Freports\u002Fchart-data",{"type":27,"value":3739}," endpoint that runs everything as Postgres ",{"type":21,"tag":53,"props":3741,"children":3743},{"className":3742},[],[3744],{"type":27,"value":3745},"date_trunc",{"type":27,"value":3747}," GROUP 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.",{"type":21,"tag":41,"props":3749,"children":3750},{},[3751,3756,3758,3763],{"type":21,"tag":45,"props":3752,"children":3753},{},[3754],{"type":27,"value":3755},"Reports stats explainer",{"type":27,"value":3757}," — A single \"?\" icon in the ",{"type":21,"tag":53,"props":3759,"children":3761},{"className":3760},[],[3762],{"type":27,"value":615},{"type":27,"value":3764}," page header reveals a short card explaining what each stat card measures (Leave\u002FExpense\u002FTime are all-time, Projects is ACTIVE-only, Scheduling is current calendar year) and that charts default to six months. No per-card clutter.",{"type":21,"tag":41,"props":3766,"children":3767},{},[3768,3773,3775,3781],{"type":21,"tag":45,"props":3769,"children":3770},{},[3771],{"type":27,"value":3772},"Admin home dashboard",{"type":27,"value":3774}," — A new ",{"type":21,"tag":53,"props":3776,"children":3778},{"className":3777},[],[3779],{"type":27,"value":3780},"\u002Fapi\u002Fadmin\u002Fsummary",{"type":27,"value":3782}," endpoint 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.",{"type":21,"tag":41,"props":3784,"children":3785},{},[3786,3791,3792,3798,3799,3805,3806,3812,3814,3820],{"type":21,"tag":45,"props":3787,"children":3788},{},[3789],{"type":27,"value":3790},"Admin list endpoints",{"type":27,"value":136},{"type":21,"tag":53,"props":3793,"children":3795},{"className":3794},[],[3796],{"type":27,"value":3797},"\u002Fapi\u002Fadmin\u002Forganizations",{"type":27,"value":577},{"type":21,"tag":53,"props":3800,"children":3802},{"className":3801},[],[3803],{"type":27,"value":3804},"\u002Fapi\u002Fadmin\u002Frefund-requests",{"type":27,"value":863},{"type":21,"tag":53,"props":3807,"children":3809},{"className":3808},[],[3810],{"type":27,"value":3811},"\u002Fapi\u002Fadmin\u002Faudit-logs",{"type":27,"value":3813}," now Redis-cache their list responses for 30s. The ",{"type":21,"tag":53,"props":3815,"children":3817},{"className":3816},[],[3818],{"type":27,"value":3819},"status",{"type":27,"value":3821}," filter 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.",{"type":21,"tag":41,"props":3823,"children":3824},{},[3825,3830,3831,3837,3839,3844,3845,3851,3852,3858,3859,3864,3865,3871,3872,3877,3878,3884,3886,3892,3894,3899,3901,3907,3909,3915,3916,3922],{"type":21,"tag":45,"props":3826,"children":3827},{},[3828],{"type":27,"value":3829},"Users list",{"type":27,"value":136},{"type":21,"tag":53,"props":3832,"children":3834},{"className":3833},[],[3835],{"type":27,"value":3836},"\u002Fapi\u002Fusers",{"type":27,"value":3838}," now accepts ",{"type":21,"tag":53,"props":3840,"children":3842},{"className":3841},[],[3843],{"type":27,"value":3137},{"type":27,"value":577},{"type":21,"tag":53,"props":3846,"children":3848},{"className":3847},[],[3849],{"type":27,"value":3850},"pageSize",{"type":27,"value":577},{"type":21,"tag":53,"props":3853,"children":3855},{"className":3854},[],[3856],{"type":27,"value":3857},"search",{"type":27,"value":577},{"type":21,"tag":53,"props":3860,"children":3862},{"className":3861},[],[3863],{"type":27,"value":2894},{"type":27,"value":577},{"type":21,"tag":53,"props":3866,"children":3868},{"className":3867},[],[3869],{"type":27,"value":3870},"role",{"type":27,"value":577},{"type":21,"tag":53,"props":3873,"children":3875},{"className":3874},[],[3876],{"type":27,"value":3819},{"type":27,"value":863},{"type":21,"tag":53,"props":3879,"children":3881},{"className":3880},[],[3882],{"type":27,"value":3883},"sortBy",{"type":27,"value":3885}," query params. All filtering, sorting, and pagination is pushed to Postgres. The response includes a ",{"type":21,"tag":53,"props":3887,"children":3889},{"className":3888},[],[3890],{"type":27,"value":3891},"total",{"type":27,"value":3893}," count alongside the page of users. DEPARTMENT_HEAD callers are forced to their own department server-side even if they pass a different ",{"type":21,"tag":53,"props":3895,"children":3897},{"className":3896},[],[3898],{"type":27,"value":2894},{"type":27,"value":3900}," (gate preserved). Role-aware ",{"type":21,"tag":53,"props":3902,"children":3904},{"className":3903},[],[3905],{"type":27,"value":3906},"select",{"type":27,"value":3908}," — employees never receive HR-sensitive fields. Two new composite indexes (",{"type":21,"tag":53,"props":3910,"children":3912},{"className":3911},[],[3913],{"type":27,"value":3914},"(organizationId, isActive, firstName)",{"type":27,"value":436},{"type":21,"tag":53,"props":3917,"children":3919},{"className":3918},[],[3920],{"type":27,"value":3921},"(organizationId, departmentId, isActive)",{"type":27,"value":3923},") back the common sort\u002Ffilter paths.",{"type":21,"tag":41,"props":3925,"children":3926},{},[3927,3932,3933,3939,3941,3947,3949,3955],{"type":21,"tag":45,"props":3928,"children":3929},{},[3930],{"type":27,"value":3931},"Documents list",{"type":27,"value":136},{"type":21,"tag":53,"props":3934,"children":3936},{"className":3935},[],[3937],{"type":27,"value":3938},"\u002Fapi\u002Fdocuments",{"type":27,"value":3940}," now uses Prisma ",{"type":21,"tag":53,"props":3942,"children":3944},{"className":3943},[],[3945],{"type":27,"value":3946},"_count",{"type":27,"value":3948}," relation aggregates for per-document signer counts instead of looping. Server-side pagination, filter, and sort. Response is Redis-cached for 30s keyed by ",{"type":21,"tag":53,"props":3950,"children":3952},{"className":3951},[],[3953],{"type":27,"value":3954},"(organizationId, userId, role, params)",{"type":27,"value":3956}," so an admin's cached view never serves an employee's scoped view. Two new composite indexes back the common filter combinations.",{"type":21,"tag":41,"props":3958,"children":3959},{},[3960,3965,3966,3972,3974,3980,3982,3987],{"type":21,"tag":45,"props":3961,"children":3962},{},[3963],{"type":27,"value":3964},"Calendar list endpoint",{"type":27,"value":136},{"type":21,"tag":53,"props":3967,"children":3969},{"className":3968},[],[3970],{"type":27,"value":3971},"\u002Fapi\u002Ftime-tracking\u002Fentries",{"type":27,"value":3973}," (used by the timesheets calendar + flat list) now has a short-lived Redis cache scoped by organization + role + department\u002Fuser and the date window. The ",{"type":21,"tag":53,"props":3975,"children":3977},{"className":3976},[],[3978],{"type":27,"value":3979},"avatar",{"type":27,"value":3981}," field was dropped from the user ",{"type":21,"tag":53,"props":3983,"children":3985},{"className":3984},[],[3986],{"type":27,"value":3906},{"type":27,"value":3988}," since the calendar never renders it — one less join, one less field to serialize. Date-range validation added to reject malformed inputs early.",{"type":21,"tag":41,"props":3990,"children":3991},{},[3992,3997,3998,4004,4005,4011,4012,4018,4020,4025,4027,4032],{"type":21,"tag":45,"props":3993,"children":3994},{},[3995],{"type":27,"value":3996},"Approvals detail pages",{"type":27,"value":136},{"type":21,"tag":53,"props":3999,"children":4001},{"className":4000},[],[4002],{"type":27,"value":4003},"\u002Fapi\u002Fleaves\u002Fpending",{"type":27,"value":577},{"type":21,"tag":53,"props":4006,"children":4008},{"className":4007},[],[4009],{"type":27,"value":4010},"\u002Fapi\u002Fexpenses\u002Fapprovals",{"type":27,"value":863},{"type":21,"tag":53,"props":4013,"children":4015},{"className":4014},[],[4016],{"type":27,"value":4017},"\u002Fapi\u002Ftime-tracking\u002Fapprovals\u002Findex",{"type":27,"value":4019}," all got: Redis-cached list responses, slim selects, server-side pagination bounds, and count + list + summary aggregated in one ",{"type":21,"tag":53,"props":4021,"children":4023},{"className":4022},[],[4024],{"type":27,"value":3196},{"type":27,"value":4026}," (expenses queue). The ",{"type":21,"tag":53,"props":4028,"children":4030},{"className":4029},[],[4031],{"type":27,"value":3819},{"type":27,"value":4033}," filter on time-tracking approvals is now allow-list validated.",{"type":21,"tag":41,"props":4035,"children":4036},{},[4037,4042,4043,4049,4050,4056,4057,4063,4065,4070],{"type":21,"tag":45,"props":4038,"children":4039},{},[4040],{"type":27,"value":4041},"QuickBooks dashboards",{"type":27,"value":136},{"type":21,"tag":53,"props":4044,"children":4046},{"className":4045},[],[4047],{"type":27,"value":4048},"\u002Fapi\u002Fintegrations\u002Fquickbooks\u002Fstatus",{"type":27,"value":577},{"type":21,"tag":53,"props":4051,"children":4053},{"className":4052},[],[4054],{"type":27,"value":4055},"\u002Fapi\u002Fintegrations\u002Fquickbooks\u002Femployees\u002Fmappings",{"type":27,"value":863},{"type":21,"tag":53,"props":4058,"children":4060},{"className":4059},[],[4061],{"type":27,"value":4062},"\u002Fapi\u002Fintegrations\u002Fquickbooks\u002Fsync\u002Fhistory",{"type":27,"value":4064}," all got: Redis-cached responses with conservative TTL (10-60s), ",{"type":21,"tag":53,"props":4066,"children":4068},{"className":4067},[],[4069],{"type":27,"value":1387},{"type":27,"value":4071},"-based status summaries instead of looped counts, and explicit exclusion of OAuth credentials + raw QBO request\u002Fresponse payload blobs from the cached response. Admins see fresh status without every poll hitting the DB.",{"type":21,"tag":41,"props":4073,"children":4074},{},[4075,4080,4082,4088,4090,4096],{"type":21,"tag":45,"props":4076,"children":4077},{},[4078],{"type":27,"value":4079},"Cache-Control headers everywhere",{"type":27,"value":4081}," — Every new read endpoint sets ",{"type":21,"tag":53,"props":4083,"children":4085},{"className":4084},[],[4086],{"type":27,"value":4087},"Cache-Control: private, max-age=10-30",{"type":27,"value":4089}," and an ",{"type":21,"tag":53,"props":4091,"children":4093},{"className":4092},[],[4094],{"type":27,"value":4095},"X-Cache: HIT|MISS",{"type":27,"value":4097}," telemetry header so ops can see cache effectiveness in DevTools.",{"type":21,"tag":41,"props":4099,"children":4100},{},[4101,4106,4107,4113,4115,4121],{"type":21,"tag":45,"props":4102,"children":4103},{},[4104],{"type":27,"value":4105},"Home dashboard cache",{"type":27,"value":136},{"type":21,"tag":53,"props":4108,"children":4110},{"className":4109},[],[4111],{"type":27,"value":4112},"\u002Fapi\u002Fdashboard\u002Fusers",{"type":27,"value":4114}," (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 \u002F 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 ",{"type":21,"tag":53,"props":4116,"children":4118},{"className":4117},[],[4119],{"type":27,"value":4120},"(country, year)",{"type":27,"value":4122}," in Redis for 7 days — same holidays for every tenant with users in that country, fetched once per week.",{"type":21,"tag":41,"props":4124,"children":4125},{},[4126,4131,4132,4138,4140,4145,4146,4151,4152,4157,4158,4163,4164,4170,4171,4176,4177,4183,4184,4190,4191,4197,4199,4205,4207,4213],{"type":21,"tag":45,"props":4127,"children":4128},{},[4129],{"type":27,"value":4130},"Expenses list pagination",{"type":27,"value":136},{"type":21,"tag":53,"props":4133,"children":4135},{"className":4134},[],[4136],{"type":27,"value":4137},"\u002Fexpenses",{"type":27,"value":4139}," used to request a thousand expense reports with full line-item \u002F per-diem \u002F mileage \u002F receipt relations on page mount, even though the table only renders a page of ~50 rows. The endpoint now takes ",{"type":21,"tag":53,"props":4141,"children":4143},{"className":4142},[],[4144],{"type":27,"value":3137},{"type":27,"value":260},{"type":21,"tag":53,"props":4147,"children":4149},{"className":4148},[],[4150],{"type":27,"value":3850},{"type":27,"value":260},{"type":21,"tag":53,"props":4153,"children":4155},{"className":4154},[],[4156],{"type":27,"value":3857},{"type":27,"value":260},{"type":21,"tag":53,"props":4159,"children":4161},{"className":4160},[],[4162],{"type":27,"value":3819},{"type":27,"value":260},{"type":21,"tag":53,"props":4165,"children":4167},{"className":4166},[],[4168],{"type":27,"value":4169},"reportType",{"type":27,"value":260},{"type":21,"tag":53,"props":4172,"children":4174},{"className":4173},[],[4175],{"type":27,"value":2944},{"type":27,"value":260},{"type":21,"tag":53,"props":4178,"children":4180},{"className":4179},[],[4181],{"type":27,"value":4182},"sort",{"type":27,"value":260},{"type":21,"tag":53,"props":4185,"children":4187},{"className":4186},[],[4188],{"type":27,"value":4189},"startDate",{"type":27,"value":260},{"type":21,"tag":53,"props":4192,"children":4194},{"className":4193},[],[4195],{"type":27,"value":4196},"endDate",{"type":27,"value":4198}," query params and does all pagination \u002F filtering \u002F sorting in Postgres. Heavy relations are opt-in via ",{"type":21,"tag":53,"props":4200,"children":4202},{"className":4201},[],[4203],{"type":27,"value":4204},"includeItems=true",{"type":27,"value":4206},". Optional ",{"type":21,"tag":53,"props":4208,"children":4210},{"className":4209},[],[4211],{"type":27,"value":4212},"includeStats=true",{"type":27,"value":4214}," adds a count + total aggregate in two parallel queries. Response is Redis-cached for 30s with role + viewer-scoped keys.",{"type":21,"tag":41,"props":4216,"children":4217},{},[4218,4223,4224,4230],{"type":21,"tag":45,"props":4219,"children":4220},{},[4221],{"type":27,"value":4222},"Reverse-geocode cache",{"type":27,"value":136},{"type":21,"tag":53,"props":4225,"children":4227},{"className":4226},[],[4228],{"type":27,"value":4229},"\u002Fapi\u002Fmaps\u002Fgeocode",{"type":27,"value":4231}," is hit on every location-aware clock-in \u002F 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.",{"type":21,"tag":41,"props":4233,"children":4234},{},[4235,4240,4242,4248,4250,4256,4258,4263,4265,4270,4272,4278],{"type":21,"tag":45,"props":4236,"children":4237},{},[4238],{"type":27,"value":4239},"Billing subscription dedupe",{"type":27,"value":4241}," — Every component that called ",{"type":21,"tag":53,"props":4243,"children":4245},{"className":4244},[],[4246],{"type":27,"value":4247},"useBilling()",{"type":27,"value":4249}," used to get its own local state and fire its own ",{"type":21,"tag":53,"props":4251,"children":4253},{"className":4252},[],[4254],{"type":27,"value":4255},"\u002Fapi\u002Fbilling\u002Fsubscription",{"type":27,"value":4257}," request, so the ",{"type":21,"tag":53,"props":4259,"children":4261},{"className":4260},[],[4262],{"type":27,"value":1154},{"type":27,"value":4264}," page was making three or more parallel identical requests on mount. ",{"type":21,"tag":53,"props":4266,"children":4268},{"className":4267},[],[4269],{"type":27,"value":4247},{"type":27,"value":4271}," now shares module-level state + an in-flight promise (same pattern as ",{"type":21,"tag":53,"props":4273,"children":4275},{"className":4274},[],[4276],{"type":27,"value":4277},"useBootstrap",{"type":27,"value":4279},"), so concurrent callers all await the same single request. One network round-trip per page load instead of three.",1790889965172]