Commit Graph

4 Commits

Author SHA1 Message Date
79f0e19bf8 Org assignment history, mobile support, and a correctness pass
Data model
- employee_assignments records org placement over time (valid_from/valid_to),
  written by a trigger on `employees` rather than inside each RPC: ~70
  `update employees` statements spread over fifteen migrations mean per-call
  bookkeeping would miss paths today and again with every future RPC. A
  partial unique index enforces the one-open-interval invariant the trigger
  relies on when closing the current row.
- The Organigramm gains a Stichtag (default today). Membership comes from
  entry/exit/karenz, past placement from the new history, future placement
  projected from pending_org_changes. Placements predating the migration are
  backfilled with today's values and flagged as such in the UI, since
  employee_history only ever stored free text and cannot be reconstructed.

Correctness
- Reports and exports silently truncated at PostgREST's 1000-row cap
  (db.max_rows); employee_history is already past it at ~800 staff. Every
  whole-table read now pages explicitly.
- XLSX date cells were a day early: ExcelJS converts a Date to an Excel
  serial straight off getTime(), so a Date built at local midnight lands on
  the previous day's serial in any positive-offset zone.
- Date handling is pinned to Europe/Vienna throughout, and date-only strings
  are formatted without a Date round-trip. The dashboard's YTD window was
  built by round-tripping a local Date through toISOString(), which shifted
  it a day early and dropped 31 December entirely.
- Export routes parsed measure/group/split/eventType with unchecked `as`
  casts, so an unknown value reached column headers as `undefined` and the
  Content-Disposition filename. Parsed against the label maps now, with the
  filename slugged as a backstop.
- toXlsx keyed columns by header text, silently dropping the second of any
  two columns sharing a name — split columns take their header from data.
- The org chart tree walks had no cycle guard; nothing in the schema forbids
  a manager_id cycle, and one would hang the tab rather than misreport.
- The login page reflected ?error= verbatim, letting anyone put arbitrary
  text on the real sign-in screen; messages are looked up by code now.
- React Flow needs elementsSelectable on, or it sets pointer-events:none on
  the whole node and the expand control stops responding.

UI
- Mobile: the shell was unusable below lg — a fixed 236px margin pushed
  content off-screen with no mobile navigation at all. The sidebar is now a
  drawer, dvh replaces vh, safe-area insets are honoured, inputs are 16px so
  iOS stops zooming on focus, and form grids stack.
- Org chart nodes redesigned: per-kind accent stripes and icons, vacant
  roles called out, expand control moved to the bottom edge carrying the
  child count.
- Pagination is windowed; it previously rendered one link per page (54 for
  the employee list, unbounded for the audit log).
- Positions page reduced to open positions with a single "Besetzen" action.
- The employee Organisation tab links into the org chart focused on that
  person, reusing the chart's existing search-match highlighting.

Also included, uncommitted until now
- Dependants, HR notes, academic titles, split address fields, position
  validity and role/employment fields, with their migrations and UI.
- Docker/compose deployment setup, data-model and security-review docs.
2026-07-24 23:38:10 +02:00
901c5c426e Consolidation pass: HR-only access, effective-dated mutations, data integrity guards, test suite
Reworks the app from a two-role (hr_admin/manager) model to a single
HR-only role gated by profiles.is_active, fixes transfer/promote/karenz/
reorg RPCs to actually defer future-dated changes via a new
pending_org_changes table instead of writing them immediately (applied
by a daily Vercel Cron route), makes reorg undo append-only instead of
deleting history, adds Karenz-return and history-date integrity guards,
deprecates the salary column, and adds explicit schema grants + perf
indexes needed to run against a fresh (non-hosted) Postgres instance.

Adds vitest unit + integration test suites (the latter against a real
local Supabase instance) covering all of the above, plus lint/typecheck/
build wiring (`npm run check`).
2026-07-14 20:32:20 +02:00
f91a69147e Phase 3: Hire wizard, draft resume, and two real SQL bugfixes
- components/hire/: 4-step Hire Wizard (Person/Position/Vertrag/
  Zusammenfassung) matching sec4.4, with a HireWizardProvider context so it
  can be opened both from the global "+ Neueinstellung" button and from a
  "Fortsetzen" link on a saved draft.
- actions/hireDrafts.ts: save/delete hire_drafts (owner-scoped RLS already
  in place from Phase 1). Dashboard now shows the "Entwuerfe" card the
  Phase 1 plan deferred, since the wizard it depends on now exists.
- lib/positions.ts: shared open-positions loader (position number, org
  breadcrumb, resolved manager name) used by both the wizard and (later)
  the Positions page.

Two real bugs found via live testing and fixed in supabase/functions.sql:
1. hire_employee/rehire_employee: a two-branch CASE returning bare string
   literals defaults to `text`, not the target enum, so `status = case
   when ... then 'Geplant' else 'Aktiv' end` failed against the
   employment_status column. Fixed with an explicit ::employment_status
   cast on the whole CASE expression.
2. Postgres precedence gotcha: ->> and || sit at the *same* precedence
   tier and left-associate, so `payload->>'first_name' || ' ' ||
   payload->>'last_name'` does not group the way it reads - it tries to
   apply ->> to an intermediate text value and fails with "operator does
   not exist: text ->> unknown". Fixed by parenthesizing every ->>'...'
   expression that participates in a || chain.

Also fixed: hire_employee referenced v_position.title outside the branch
that assigns v_position, raising "record not assigned" whenever a hire
wasn't tied to a position_id; extracted a v_job_title variable instead.

Verified live end-to-end: wizard search -> select position -> submit
creates the employee, closes the position, and writes matching
employee_history + audit_log rows atomically.
2026-07-13 22:37:59 +02:00
ef9852b09c Phase 1: project foundation for Alpenwerk HR
Scaffolds the Next.js 16 / TypeScript strict / Tailwind v3 app per
NEXTJS_REBUILD_SUPERPROMPT.md, and implements the Foundation slice from
the Phase 1 plan:

- Corrected Supabase schema (supabase/schema.sql): org units, employees,
  history, positions, hire drafts, saved reports, audit log, reorg
  scenarios, role-based profiles, salary-masking view, RLS policies,
  auto-derivation triggers, position-number generator.
- Seed script (supabase/seed.ts): ~800 realistic Austrian employees across
  9 divisions / 16 departments / 35 teams, history, 8 open positions, and
  hr_admin/manager test accounts.
- Supabase clients (lib/supabase/*), design tokens (tailwind.config.ts),
  format/color helpers (lib/format.ts, lib/colors.ts).
- Shared UI kit (components/ui): Avatar, StatusChip, Toast, Modal,
  SlideOver, SegmentedControl, Lookup.
- Auth (login page, Server Actions) and proxy.ts (Next 16's replacement
  for middleware) guarding the authenticated route group.
- Shell (Sidebar, Topbar, NewHireButton stub) and the Dashboard page,
  reading live data via employees_directory.

Employees list/detail, hire wizard, action panels, org chart, positions,
reports, and audit log are deferred to later phases per the plan.
2026-07-13 21:44:28 +02:00