Works council members, expectant mothers, parents on leave, registered disabled employees, apprentices — each has its own rules that come before a dismissal. The tool does not judge whether one is lawful, but it must not stay quiet about it either, and looking it up on the contract tab is exactly the step that gets skipped under time pressure. So: a checkbox, an optional end date, and a red warning at the top of the termination panel naming the date — or saying plainly that no end was recorded. It shows for a no-show too; the protection runs from the start of the contract, not the first day worked. The date is optional on purpose. A works council mandate has a known end, a pregnancy does not, and a mandatory field would force an invented number. A constraint says only what cannot be: an end date without the flag, which would be a leftover nobody could interpret. The field goes the whole way through — hire, data change, contract sheet, export, report criteria (as a yes/no and as a date range), and the import. A field that exists in one screen and not the next is how people stop trusting the numbers. Terminating is now offered for planned entries as well, labelled "Nicht angetreten", with No Show preselected. Without it a person who never turned up stayed a planned entry forever, since nothing else can end one. One finding worth recording: tsc has been reporting success on a broken program. A generated file under .next got corrupted when a build ran against a live dev server, and its syntax errors suppressed semantic checking everywhere else — two genuine type errors in this change went unreported until I typechecked with .next excluded. The file is removed and the ordinary typecheck is meaningful again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
540 lines
20 KiB
TypeScript
540 lines
20 KiB
TypeScript
// Hand-written to match supabase/schema.sql + supabase/migrations/*.sql (no DB
|
|
// connection string available to run `supabase gen types typescript` in this
|
|
// environment — regenerate from the live project once you have the Supabase
|
|
// CLI linked).
|
|
|
|
export type EmploymentStatus = "Aktiv" | "Karenz" | "Geplant" | "Ausgetreten";
|
|
export type EmploymentType = "Vollzeit" | "Teilzeit";
|
|
export type ContractType = "unbefristet" | "befristet";
|
|
export type PaygradeType = "A" | "B" | "C" | "D" | "E" | "F";
|
|
export type SourceType = "Intern" | "Extern";
|
|
export type GenderType = "m" | "w";
|
|
export type WorkerType = "Angestellte:r" | "Arbeiter:in";
|
|
export type CollectiveAgreement = "Handel" | "Süßwaren";
|
|
export type Weekday = "Mo" | "Di" | "Mi" | "Do" | "Fr" | "Sa" | "So";
|
|
|
|
/**
|
|
* Antriebsart des Dienstwagens.
|
|
*
|
|
* Bewusst keine dritte Möglichkeit „unbekannt": die Angabe hängt per CHECK
|
|
* an has_dienstwagen, und wer einen Dienstwagen hat, weiss auch, ob er lädt
|
|
* oder tankt.
|
|
*/
|
|
export type DienstwagenArt = "Verbrenner" | "Elektro";
|
|
|
|
/**
|
|
* Verhältnis zum Notfallkontakt.
|
|
*
|
|
* Auswahlliste statt Freitext, damit sich danach auswerten lässt und nicht
|
|
* „Gattin", „Ehefrau" und „Frau" nebeneinander stehen. „Sonstige" ist der
|
|
* Ausweg für alles, was hier fehlt — ohne ihn wäre die Liste eine Anmassung.
|
|
*
|
|
* In der Datenbank bleibt die Spalte `text`: eine Aufzählung dort würde jede
|
|
* Ergänzung zu einer Migration machen, und diese Liste wird sich ändern.
|
|
*/
|
|
export const EMERGENCY_RELATIONS = [
|
|
"Gattin/Gatte",
|
|
"Lebensgefährtin/Lebensgefährte",
|
|
"Mutter",
|
|
"Vater",
|
|
"Tochter",
|
|
"Sohn",
|
|
"Schwester",
|
|
"Bruder",
|
|
"Freundin/Freund",
|
|
"Sonstige",
|
|
] as const;
|
|
|
|
export type EmergencyRelation = (typeof EMERGENCY_RELATIONS)[number];
|
|
|
|
/**
|
|
* Eine einzelne Feldänderung im Protokoll.
|
|
*
|
|
* `vorher` und `nachher` sind bewusst Text: die Datenbank stellt jeden Typ
|
|
* über ::text dar, damit ein Datum, eine Zahl und eine Liste von Arbeitstagen
|
|
* in derselben Spalte nebeneinander stehen können. Für die Anzeige reicht
|
|
* das; gerechnet wird damit nicht.
|
|
*/
|
|
export type AuditChange = { feld: string; vorher: string | null; nachher: string | null };
|
|
export type RelationshipType = "Ehepartner:in" | "Lebenspartner:in" | "Kind" | "Sonstige";
|
|
export type NoteCategory = "Allgemein" | "Vertraulich" | "Personalgespräch" | "Wiedervorlage" | "Lob / Anerkennung";
|
|
// Single HR-only role (see docs/decisions/0001-hr-only-access.md). Kept as a
|
|
// union (not a string literal) so a future hr_admin/hr_user split, if ever
|
|
// technically required, is a type-level addition, not a rewrite.
|
|
export type ProfileRole = "hr";
|
|
/** Etikett einer Organisationseinheit; die Struktur steckt in parent_id. */
|
|
export type OrgUnitType = "Gesellschaft" | "Bereich" | "Abteilung" | "Team";
|
|
export type HistoryEventType =
|
|
| "Eintritt"
|
|
| "Beförderung"
|
|
| "Versetzung"
|
|
| "Karenz"
|
|
| "Vertragsänderung"
|
|
| "Stammdatenänderung"
|
|
| "Austritt"
|
|
| "Wiedereintritt"
|
|
| "Reorganisation"
|
|
| "Gehaltsanpassung"
|
|
| "Rückkehr";
|
|
export type PendingChangeType =
|
|
| "transfer"
|
|
| "promotion"
|
|
| "karenz_start"
|
|
| "karenz_return"
|
|
| "contract_change"
|
|
| "reorg";
|
|
export type PendingChangeStatus = "pending" | "applied" | "cancelled";
|
|
|
|
// @supabase/postgrest-js requires every table/view to carry a Relationships
|
|
// array (used for typed embedded selects) — left empty since no code in this
|
|
// app relies on nested/embedded resource selects.
|
|
type NoRelationships = { Relationships: [] };
|
|
|
|
export type Database = {
|
|
public: {
|
|
Tables: {
|
|
locations: NoRelationships & {
|
|
Row: { id: string; name: string; country: string };
|
|
Insert: { id?: string; name: string; country: string };
|
|
Update: Partial<{ id: string; name: string; country: string }>;
|
|
};
|
|
profiles: NoRelationships & {
|
|
Row: {
|
|
id: string;
|
|
email: string;
|
|
full_name: string | null;
|
|
role: ProfileRole;
|
|
is_active: boolean;
|
|
created_by: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
Insert: {
|
|
id: string;
|
|
email: string;
|
|
full_name?: string | null;
|
|
role?: ProfileRole;
|
|
is_active?: boolean;
|
|
created_by?: string | null;
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
};
|
|
Update: Partial<{
|
|
id: string;
|
|
email: string;
|
|
full_name: string | null;
|
|
role: ProfileRole;
|
|
is_active: boolean;
|
|
created_by: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>;
|
|
};
|
|
employees: NoRelationships & {
|
|
Row: {
|
|
id: string;
|
|
personnel_number: number;
|
|
first_name: string;
|
|
last_name: string;
|
|
gender: GenderType;
|
|
birth_date: string;
|
|
sv_nummer: string | null;
|
|
nationality: string;
|
|
address: string | null;
|
|
postal_code: string | null;
|
|
city: string | null;
|
|
address_country: string | null;
|
|
/** Private Adresse, freiwillig — eindeutig, wenn angegeben. */
|
|
email: string | null;
|
|
phone: string | null;
|
|
job_title: string;
|
|
location_id: string;
|
|
employment_type: EmploymentType;
|
|
weekly_hours: number;
|
|
/** @deprecated Salary is out of MVP scope; column kept only for pre-existing data. */
|
|
monthly_salary_gross: number | null;
|
|
contract_type: ContractType;
|
|
contract_end_date: string | null;
|
|
paygrade: PaygradeType;
|
|
source: SourceType;
|
|
status: EmploymentStatus;
|
|
entry_date: string;
|
|
exit_date: string | null;
|
|
exit_reason: string | null;
|
|
karenz_start_date: string | null;
|
|
karenz_return_date: string | null;
|
|
absence_type: string | null;
|
|
avatar_color: string | null;
|
|
worker_type: WorkerType;
|
|
collective_agreement: CollectiveAgreement;
|
|
work_days: Weekday[];
|
|
is_betriebsrat: boolean;
|
|
has_dienstwagen: boolean;
|
|
/** Null genau dann, wenn kein Dienstwagen vorhanden ist — chk_dienstwagen_art. */
|
|
dienstwagen_art: DienstwagenArt | null;
|
|
/** Besonderer Kündigungsschutz — löst beim Austritt eine Warnung aus. */
|
|
has_kuendigungsschutz: boolean;
|
|
/** Ende des Schutzes, falls bekannt. Nur mit dem Kennzeichen zusammen erlaubt. */
|
|
kuendigungsschutz_bis: string | null;
|
|
/** Nur gemeinsam gesetzt oder gemeinsam leer — chk_emergency_contact. */
|
|
emergency_contact_name: string | null;
|
|
emergency_contact_phone: string | null;
|
|
emergency_contact_relation: string | null;
|
|
is_laterale_fuehrung: boolean;
|
|
is_c_level: boolean;
|
|
title_prefix: string[];
|
|
title_suffix: string[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
Insert: {
|
|
id?: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
gender: GenderType;
|
|
birth_date: string;
|
|
sv_nummer?: string | null;
|
|
nationality?: string;
|
|
address?: string | null;
|
|
postal_code?: string | null;
|
|
city?: string | null;
|
|
address_country?: string | null;
|
|
email?: string | null;
|
|
phone?: string | null;
|
|
job_title: string;
|
|
location_id: string;
|
|
employment_type?: EmploymentType;
|
|
weekly_hours?: number;
|
|
contract_type?: ContractType;
|
|
contract_end_date?: string | null;
|
|
paygrade?: PaygradeType;
|
|
source?: SourceType;
|
|
status?: EmploymentStatus;
|
|
entry_date: string;
|
|
exit_date?: string | null;
|
|
exit_reason?: string | null;
|
|
karenz_start_date?: string | null;
|
|
karenz_return_date?: string | null;
|
|
absence_type?: string | null;
|
|
avatar_color?: string | null;
|
|
worker_type?: WorkerType;
|
|
collective_agreement?: CollectiveAgreement;
|
|
work_days?: Weekday[];
|
|
is_betriebsrat?: boolean;
|
|
has_dienstwagen?: boolean;
|
|
dienstwagen_art?: DienstwagenArt | null;
|
|
has_kuendigungsschutz?: boolean;
|
|
kuendigungsschutz_bis?: string | null;
|
|
emergency_contact_name?: string | null;
|
|
emergency_contact_phone?: string | null;
|
|
emergency_contact_relation?: string | null;
|
|
is_laterale_fuehrung?: boolean;
|
|
is_c_level?: boolean;
|
|
title_prefix?: string[];
|
|
title_suffix?: string[];
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
};
|
|
Update: Partial<Database["public"]["Tables"]["employees"]["Insert"]>;
|
|
};
|
|
employee_history: NoRelationships & {
|
|
Row: {
|
|
id: string;
|
|
employee_id: string;
|
|
event_date: string;
|
|
event_type: HistoryEventType;
|
|
description: string;
|
|
/** Feldweise Änderungen — dieselbe Form wie audit_log.changes. */
|
|
changes: AuditChange[] | null;
|
|
/** Der geplante Vorgang, solange die Änderung noch nicht wirksam ist. */
|
|
pending_id: string | null;
|
|
created_at: string;
|
|
};
|
|
Insert: {
|
|
id?: string;
|
|
employee_id: string;
|
|
event_date: string;
|
|
event_type: HistoryEventType;
|
|
description: string;
|
|
changes?: AuditChange[] | null;
|
|
pending_id?: string | null;
|
|
created_at?: string;
|
|
};
|
|
Update: Partial<Database["public"]["Tables"]["employee_history"]["Insert"]>;
|
|
};
|
|
employee_dependents: NoRelationships & {
|
|
Row: {
|
|
id: string;
|
|
employee_id: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
relationship: RelationshipType;
|
|
sv_nummer: string | null;
|
|
birth_date: string;
|
|
created_at: string;
|
|
};
|
|
Insert: {
|
|
id?: string;
|
|
employee_id: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
relationship: RelationshipType;
|
|
sv_nummer?: string | null;
|
|
birth_date: string;
|
|
created_at?: string;
|
|
};
|
|
Update: Partial<Database["public"]["Tables"]["employee_dependents"]["Insert"]>;
|
|
};
|
|
employee_notes: NoRelationships & {
|
|
Row: {
|
|
id: string;
|
|
employee_id: string;
|
|
author_user_id: string | null;
|
|
author_name: string;
|
|
category: NoteCategory;
|
|
note_text: string;
|
|
due_date: string | null;
|
|
done: boolean;
|
|
done_at: string | null;
|
|
done_by: string | null;
|
|
created_at: string;
|
|
};
|
|
Insert: {
|
|
id?: string;
|
|
employee_id: string;
|
|
author_user_id?: string | null;
|
|
author_name: string;
|
|
category?: NoteCategory;
|
|
note_text: string;
|
|
due_date?: string | null;
|
|
done?: boolean;
|
|
done_at?: string | null;
|
|
done_by?: string | null;
|
|
created_at?: string;
|
|
};
|
|
Update: Partial<Database["public"]["Tables"]["employee_notes"]["Insert"]>;
|
|
};
|
|
hire_drafts: NoRelationships & {
|
|
Row: { id: string; created_by: string | null; step: number; payload: Record<string, unknown>; updated_at: string };
|
|
Insert: { id?: string; created_by?: string | null; step?: number; payload: Record<string, unknown>; updated_at?: string };
|
|
Update: Partial<Database["public"]["Tables"]["hire_drafts"]["Insert"]>;
|
|
};
|
|
saved_reports: NoRelationships & {
|
|
Row: { id: string; created_by: string | null; name: string; config: Record<string, unknown>; created_at: string };
|
|
Insert: { id?: string; created_by?: string | null; name: string; config: Record<string, unknown>; created_at?: string };
|
|
Update: Partial<Database["public"]["Tables"]["saved_reports"]["Insert"]>;
|
|
};
|
|
audit_log: NoRelationships & {
|
|
Row: {
|
|
id: string;
|
|
occurred_at: string;
|
|
actor_user_id: string | null;
|
|
actor_name: string;
|
|
action: string;
|
|
target_label: string;
|
|
target_employee_id: string | null;
|
|
details: string | null;
|
|
/**
|
|
* Feldweise Änderungen. Null bei Einträgen aus der Zeit vor
|
|
* 20260803140000_audit_changes_detail.sql — dort wurden nur die
|
|
* Feldnamen behalten, nicht die Werte, und das lässt sich nicht
|
|
* nachliefern.
|
|
*/
|
|
changes: AuditChange[] | null;
|
|
};
|
|
Insert: {
|
|
id?: string;
|
|
occurred_at?: string;
|
|
actor_user_id?: string | null;
|
|
actor_name: string;
|
|
action: string;
|
|
target_label: string;
|
|
target_employee_id?: string | null;
|
|
details?: string | null;
|
|
changes?: AuditChange[] | null;
|
|
};
|
|
Update: Partial<Database["public"]["Tables"]["audit_log"]["Insert"]>;
|
|
};
|
|
pending_org_changes: NoRelationships & {
|
|
Row: {
|
|
id: string;
|
|
employee_id: string;
|
|
change_type: PendingChangeType;
|
|
effective_date: string;
|
|
payload: Record<string, unknown>;
|
|
status: PendingChangeStatus;
|
|
created_by: string | null;
|
|
created_at: string;
|
|
applied_at: string | null;
|
|
};
|
|
Insert: {
|
|
id?: string;
|
|
employee_id: string;
|
|
change_type: PendingChangeType;
|
|
effective_date: string;
|
|
payload: Record<string, unknown>;
|
|
status?: PendingChangeStatus;
|
|
created_by?: string | null;
|
|
created_at?: string;
|
|
applied_at?: string | null;
|
|
};
|
|
Update: Partial<Database["public"]["Tables"]["pending_org_changes"]["Insert"]>;
|
|
};
|
|
// ── SAP-OM-Modell ──────────────────────────────────────────
|
|
// O: rekursiv über parent_id, unit_type ist nur ein Etikett.
|
|
org_units: {
|
|
Relationships: [
|
|
{
|
|
foreignKeyName: "org_units_parent_id_fkey";
|
|
columns: ["parent_id"];
|
|
referencedRelation: "org_units";
|
|
referencedColumns: ["id"];
|
|
},
|
|
];
|
|
Row: {
|
|
id: string;
|
|
org_number: string;
|
|
name: string;
|
|
parent_id: string | null;
|
|
unit_type: OrgUnitType;
|
|
valid_from: string;
|
|
valid_to: string | null;
|
|
created_at: string;
|
|
};
|
|
Insert: {
|
|
id?: string;
|
|
org_number: string;
|
|
name: string;
|
|
parent_id?: string | null;
|
|
unit_type: OrgUnitType;
|
|
valid_from?: string;
|
|
valid_to?: string | null;
|
|
created_at?: string;
|
|
};
|
|
Update: Partial<Database["public"]["Tables"]["org_units"]["Insert"]>;
|
|
};
|
|
// C: Katalog der Tätigkeiten.
|
|
jobs: NoRelationships & {
|
|
Row: { id: string; code: string; title: string; created_at: string };
|
|
Insert: { id?: string; code: string; title: string; created_at?: string };
|
|
Update: Partial<Database["public"]["Tables"]["jobs"]["Insert"]>;
|
|
};
|
|
// S: Planstelle. Der Name om_positions stammt aus der Zeit, in der die
|
|
// alte positions-Tabelle noch danebenstand; sie ist inzwischen weg.
|
|
om_positions: {
|
|
Relationships: [
|
|
{
|
|
foreignKeyName: "om_positions_org_unit_id_fkey";
|
|
columns: ["org_unit_id"];
|
|
referencedRelation: "org_units";
|
|
referencedColumns: ["id"];
|
|
},
|
|
{
|
|
foreignKeyName: "om_positions_job_id_fkey";
|
|
columns: ["job_id"];
|
|
referencedRelation: "jobs";
|
|
referencedColumns: ["id"];
|
|
},
|
|
];
|
|
Row: {
|
|
id: string;
|
|
position_number: string;
|
|
org_unit_id: string;
|
|
job_id: string;
|
|
is_chief: boolean;
|
|
valid_from: string;
|
|
valid_to: string | null;
|
|
created_at: string;
|
|
};
|
|
Insert: {
|
|
id?: string;
|
|
position_number: string;
|
|
org_unit_id: string;
|
|
job_id: string;
|
|
is_chief?: boolean;
|
|
valid_from?: string;
|
|
valid_to?: string | null;
|
|
created_at?: string;
|
|
};
|
|
Update: Partial<Database["public"]["Tables"]["om_positions"]["Insert"]>;
|
|
};
|
|
// A008: Person besetzt Planstelle, zeitabhängig.
|
|
position_assignments: {
|
|
Row: {
|
|
id: string;
|
|
position_id: string;
|
|
employee_id: string;
|
|
valid_from: string;
|
|
valid_to: string | null;
|
|
created_at: string;
|
|
};
|
|
Insert: {
|
|
id?: string;
|
|
position_id: string;
|
|
employee_id: string;
|
|
valid_from: string;
|
|
valid_to?: string | null;
|
|
created_at?: string;
|
|
};
|
|
Update: Partial<Database["public"]["Tables"]["position_assignments"]["Insert"]>;
|
|
// Beide Richtungen: über die Planstelle hängt die Verortung in der
|
|
// Organisation, über die Person die Verortung in der Akte. Die
|
|
// Einbettung erspart an einem Dutzend Stellen eine zweite Abfrage.
|
|
Relationships: [
|
|
{
|
|
foreignKeyName: "position_assignments_position_id_fkey";
|
|
columns: ["position_id"];
|
|
referencedRelation: "om_positions";
|
|
referencedColumns: ["id"];
|
|
},
|
|
{
|
|
foreignKeyName: "position_assignments_employee_id_fkey";
|
|
columns: ["employee_id"];
|
|
referencedRelation: "employees";
|
|
referencedColumns: ["id"];
|
|
},
|
|
];
|
|
};
|
|
};
|
|
Views: Record<string, never>;
|
|
Functions: {
|
|
hire_employee: { Args: { payload: Record<string, unknown> }; Returns: string };
|
|
terminate_employee: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
transfer_employee: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
promote_employee: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
start_karenz: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
adjust_karenz_return: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
record_karenz_return: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
change_employee_data: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
rehire_employee: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
add_employee_dependent: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
delete_employee_dependent: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
add_employee_note: { Args: { payload: Record<string, unknown> }; Returns: string };
|
|
complete_employee_note: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
// Nimmt eine irrtümliche Stammdaten-/Vertragsänderung zurück. Der einzige
|
|
// Weg an der fehlenden delete-Policy auf employee_history vorbei.
|
|
delete_history_entry: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
// Berichtigt Wert und/oder Datum eines Historieneintrags.
|
|
update_history_entry: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
// Planstelle anlegen bzw. schliessen — im OM-Modell Operationen auf
|
|
// om_positions, nicht mehr auf einer eigenen Ausschreibungstabelle.
|
|
create_position: { Args: { payload: Record<string, unknown> }; Returns: string };
|
|
update_position: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
delete_position: { Args: { payload: Record<string, unknown> }; Returns: void };
|
|
is_valid_svnr: { Args: { p_svnr: string; p_birth_date?: string | null }; Returns: boolean };
|
|
apply_due_pending_changes: { Args: Record<string, never>; Returns: number };
|
|
om_reporting_lines: {
|
|
Args: { p_as_of?: string };
|
|
Returns: {
|
|
employee_id: string;
|
|
position_id: string;
|
|
org_unit_id: string;
|
|
is_chief: boolean;
|
|
formal_manager_id: string | null;
|
|
acting_manager_id: string | null;
|
|
}[];
|
|
};
|
|
};
|
|
};
|
|
};
|