Commit Graph

8 Commits

Author SHA1 Message Date
ab50c21b8a Klick auf eine Einheit zeigt ihre Angaben
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m56s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m7s
Die naheliegendste Geste in dieser Ansicht war ohne Wirkung: eine Karte im
Organigramm liess sich anklicken und antwortete nicht. Jetzt oeffnet sie eine
Leiste mit dem, was an der Einheit steht -- Orgnummer, Art, die Kette nach
oben, Gueltigkeit, Leitung, Belegschaft, unbesetzte Planstellen, die direkt
untergeordneten Einheiten und die Zahl der Personen im ganzen Teilbaum. Dazu
dieselben zwei Handlungen wie auf der Karte, nur beschriftet statt als Symbol.

Alles daraus stammt aus den Daten, die die Struktursicht ohnehin geladen hat.
Kein zusaetzlicher Gang zur Datenbank und damit auch keine zweite Wahrheit --
was die Leiste zeigt, ist dasselbe, woraus der Baum daneben gezeichnet ist.

Die Liste klickt sich genauso: beide Ansichten kommen aus einem Baum, und eine
Handlung, die es nur in einer von beiden gibt, findet man in der anderen nie.

org_units liefert dafuer zusaetzlich valid_from und valid_to, aber nur an die
Struktursicht. Der Druck kommt ohne sie aus; auf OrgUnitNode stehen sie deshalb
als optionale Felder und nicht als zwei weitere Spalten, die ueberall
mitgeschleppt werden.
2026-09-28 21:27:36 +02:00
a3dde61ec1 Eine leere Organisationseinheit wieder entfernen
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m29s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m10s
Der Papierkorb neben dem Plus, im Organigramm wie in der Liste, und nur dort,
wo nichts darunter haengt. Ein Knopf, der beim Klick eine Absage erteilt, ist
schlechter als kein Knopf -- die Datenbank prueft es trotzdem, denn sie sieht
auch geschlossene Planstellen, die im Baum gar nicht gezeichnet sind.

Abgewiesen wird mit Grund: untergeordnete Einheiten, Planstellen (geschlossene
zaehlen mit), Kostenstellen, die darauf verweisen, und die Wurzel selbst.

Geloescht und nicht geschlossen, und das ist eine bewusste Einschraenkung.
delete_position macht seit dem OM-Umbau den Unterschied vor: besetzt gewesen →
valid_to, nie besetzt gewesen → weg. Fuer Einheiten laesst sich davon heute nur
die zweite Haelfte umsetzen, weil ein valid_to an einer Einheit zwar
eingetragen, aber nirgends gelesen wuerde -- weder Organigramm noch
orgMapsAbfragen, Berichte, Druck oder die Auswahl beim Anlegen einer Planstelle
schraenken org_units auf den Stichtag ein. Die geschlossene Einheit staende
ueberall weiter da, nur mit einem Datum, das niemand sieht. Das Schliessen zum
Stichtag kommt, wenn org_units gegen den Stichtag gelesen wird -- dieselbe
Arbeit, die auch das Verschieben braucht.
2026-09-28 16:51:26 +02:00
a6b6a7d67c Eine Organisationseinheit aus dem Organigramm heraus anlegen
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m30s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m22s
Bisher gab es dafuer nur den Import. Jetzt sitzt auf jeder Einheit im
Organigramm -- in der Grafik wie in der Liste -- ein Plus, das eine
untergeordnete Einheit anlegt, wahlweise gleich mit Leitungsplanstelle.

Bewusst nur dieser eine Fall. Umbenennen, verschieben und schliessen fehlen
nicht aus Zeitmangel: org_units.parent_id traegt kein Datum, ein Verschieben
aenderte damit auch jede Auswertung auf einen vergangenen Stichtag, und der
fruehere Stand waere danach nirgends mehr ablesbar. Anlegen stellt diese Frage
nicht, weil vorher nichts da war -- und es kann dabei auch kein Kreis
entstehen, was hier mehr wiegt als es klingt: die rekursive Abfrage in
om_reporting_lines hat weder Tiefenbegrenzung noch Kreiserkennung.

Die Leitungsplanstelle entsteht ueber create_position statt durch eine zweite
Fassung derselben Logik -- dort haengen Jobkatalog, Nummernvergabe und die
Pruefung "je Einheit genau eine Leitung".

Die Orgnummer wird eingetragen, nicht vergeben. Vorgeschlagen wird die naechste
freie Nummer der bestehenden Reihe, und nur dann, wenn sich im Bestand genau
eine Systematik ablesen laesst -- eine plausibel aussehende, aber erfundene
Nummer prueft niemand nach.

Das Datum kommt nicht aus dem Stichtag der Ansicht. Wer sich die Struktur zum
letzten Jahresende ansieht und auf Plus drueckt, will in aller Regel eine
Einheit von heute anlegen.
2026-09-28 16:28:33 +02:00
948ddf5c70 Verhaeltnis als Auswahlliste, und im Organigramm keine Vakanz ganz oben
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m41s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m16s
Zwei Nachtraege zur Rueckmeldung des Kunden.

Das Fenster fuer den Notfallkontakt hatte ein Freitextfeld fuer das
Verhaeltnis, waehrend der Einstellungsassistent und "Daten aendern"
dieselbe Auswahlliste fuehren (EMERGENCY_RELATIONS). Freitext hiesse,
dass "Gattin", "Ehefrau" und "Frau" nebeneinander stehen und sich nicht
auswerten lassen -- genau der Grund, aus dem die Liste ueberhaupt
existiert. Jetzt fuehren alle drei Stellen dieselbe.

Und "Leitung vakant" stand im Organigramm weiterhin bei der obersten
Einheit. Entfernt wurde es zuletzt nur im Druck; die Ansicht am
Bildschirm hat ihre eigene Beschriftung. Die Gesellschaft wird nicht
gefuehrt, sondern ist das Ganze -- der Vermerk las sich dort wie eine
offene Stelle und faerbte den obersten Knoten obendrein als Vakanz ein.
Ueberall sonst bleibt er.
2026-09-23 12:58:57 +02:00
8a76b3688f Show people surname first
Employee names now read "Winkler, Hannah" wherever a person appears in a
list, a table, a heading or a tree node. That is the order a personnel
list is kept in, it is the order people are looked up in, and it finally
matches the sorting — the employee list has always been ordered by
surname, which made an alphabetical page look unsorted.

The name was being assembled inline in about twenty places. A rename
that catches half of them is worse than none, so it now goes through
fmtName in lib/format.ts and every display site calls it.

Sentences keep the natural order: "Hannah Winkler wurde versetzt" reads
like German, "Winkler, Hannah wurde versetzt" reads like a form. So the
toasts are unchanged and only labels moved.

Two things the change would have quietly broken:

The org chart's own filter matched against "first last". It now matches
either order, with or without the comma, so typing what you see works
and so does typing what you remember.

The print model sorted by the last word of the composed name, which
happened to be the surname and is now the first name — every printed
unit would have come out sorted by first name. It sorts on the surname
field itself now, which is what it meant all along.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:44:03 +02:00
27669e0359 Put the whole application on the OM model, and delete what it replaced
Die Datenbank stand seit dem Cut-over auf org_units/om_positions/
position_assignments, die Anwendung fragte weiter nach employees.division_id,
team_id und manager_id — Spalten, die es nicht mehr gab. Die Oberfläche war
deshalb leer, obwohl die Daten vollständig da waren. Das ist jetzt behoben,
und zwar nicht durch Nachbau der alten Begriffe, sondern indem sie verschwinden.

Neu ist eine dünne Schicht, die die Verkettung Person → Besetzung →
Planstelle → Einheit einmal auflöst (lib/placement.ts) und der Baum als reine
Funktionen darauf (lib/org.ts): Vorfahrenkette, Teilbaum, Brotkrume. Alles
Weitere hängt daran.

Was sich dadurch von selbst erledigt hat:

  - Das Organigramm musste drei Quellen versöhnen, weil keine den ganzen
    Zeitstrahl abdeckte. position_assignments ist zeitabhängig, also
    beantwortet eine Abfrage "wer besetzte am Stichtag welche Planstelle" —
    für Vergangenheit und Zukunft gleichermassen. Wer keine Planstelle hatte,
    war nicht da; eine zweite Zugehörigkeitsregel braucht es nicht mehr.
  - Die Struktursicht war auf genau vier Ebenen verdrahtet und rendert jetzt
    rekursiv über parent_id. Liste und Grafik entstehen aus *einem* Baum;
    vorher lag dieselbe Hierarchie zweimal vor und konnte auseinanderlaufen.
  - Eine offene Stelle ist keine eigene Tabelle mehr, sondern eine Planstelle
    ohne laufende Besetzung — das Komplement kann nicht aus dem Tritt geraten.
  - Eine Versetzung ist der Wechsel auf eine Zielplanstelle statt Zielteam
    plus frei getipptem Titel. Sie kann damit nicht mehr dort landen, wo es
    keine Stelle gibt, und die Tätigkeit kommt aus dem Job-Katalog.
  - Beim Anlegen einer Planstelle entfällt die Suche nach der vorgesetzten
    Person: sie ergibt sich aus der Einheit, die Frage kann nicht mehr falsch
    beantwortet werden.

Zwei Auswertungen werden dabei richtiger, nicht nur anders. Ein
Stichtagsbericht gruppierte bisher nach der *heutigen* Zuordnung, weil es
keine Historie gab; er löst sie jetzt zum Stichtag auf. Und ein Ereignis
trägt die Einheit, in der die Person am Tag des Ereignisses sass — vorher
stand ein Austritt von vor zwei Jahren unter einem Team, in das sie nie
versetzt worden war. Der Bereichsfilter greift überall auf den ganzen
Teilbaum; auf den Bereich allein angewandt lieferte er nur die
Bereichsleitung.

Gelöscht: die Reorganisations-Werkbank samt Szenarien und Zügen (sie
verschob Teams und Abteilungen zwischen Bereichen — Objekte, die es nicht
mehr gibt; im OM-Modell ist das ein Umhängen von parent_id), die
Mitarbeiter- und Vorgesetztensuche, die nur sie und die Ausschreibung
brauchten, und aus lib/supabase/types.ts die Tabellen divisions,
departments, teams, positions und employee_assignments.

Die beiliegende Migration räumt die Datenbank entsprechend auf. Sie entfernt
auch Funktionen, die der Cut-over verfehlt hat: create_position,
delete_position und undo_reorg existierten zusätzlich in einer
jsonb-Variante und tauchen deshalb weiter in der PostgREST-Schnittstelle auf,
obwohl ihre Tabellen weg sind — ein Aufruf wäre erst zur Laufzeit
gescheitert. An ihre Stelle treten create_position und delete_position im
OM-Sinn; letzteres schliesst eine früher besetzte Planstelle, statt sie zu
löschen, sonst verschwände mit ihr die Besetzungshistorie.

Typecheck, Lint, Build und 182 Tests sind grün. Die Integrationstests sind
mitgezogen, aber weiterhin ungelaufen — dafür braucht es eine laufende
lokale Datenbank.
2026-07-27 20:02:26 +02:00
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
108da8d5e6 Phase 4: Org chart (Mitarbeiter/Positionen/Reorganisation) + one more RLS bugfix
- components/orgchart/: 3-way segmented view sharing one server fetch
  (switching tabs doesn't refetch):
  - EmployeeTree: expand/collapse hierarchy from the CEO down, search with
    auto-expand-to-match and highlighting, "Bereiche anzeigen" /
    "Alles einklappen".
  - PositionTree: models the org *structure* (GF -> Bereichsleitung ->
    Abteilung -> Teamleitung -> grouped IC positions by title, expandable
    to the actual holders) independent of who's currently in it, plus
    dashed rows for open requisitions linking to /positions.
  - ReorgWorkbench: batch multiple moves (employees / whole team / whole
    department / whole division as source, always a specific team as
    target), live headcount-impact table, apply via the existing
    apply_reorg RPC, and an undo card wired to undo_reorg.

Bug found via live apply+undo testing: undo_reorg's cleanup DELETE on
employee_history silently matched zero rows, because that table has no
DELETE policy at all (by design, for audit immutability) - RLS filters
DELETE-eligible rows to none rather than erroring. Added
supabase/functions_3.sql: a policy scoped to hr_admin deleting only rows
that carry a reorg_scenario_id, so every other history event type stays
genuinely immutable. Verified live: apply moves an employee and updates
the headcount table correctly; undo reverts team/division/manager AND
now actually removes the Reorganisation history entries it created.

Simplification flagged here (not hidden): the spec's "Ganzes Team /
Ganze Abteilung / Ganzer Bereich" reorg moves the structural org unit
itself to a new division; this implementation resolves all four move
kinds down to individual employee moves against a specific target team,
since the schema's team->department->division chain doesn't support
freely reparenting a team object without also picking a department. The
workbench UI, headcount-impact math, and apply/undo all work correctly
under this model - only the exact "move the team as a unit" semantics
differs from the literal spec wording.
2026-07-13 22:57:49 +02:00