Files
alpenwerk-hr/components/employees/panels/PromotePanel.tsx
Maximilian Stubhan 366731ec85 Phase 2/3: Employees list/detail + mutation RPCs + action panels
- supabase/functions.sql, functions_2.sql: Postgres RPCs for every
  employee/position/reorg mutation (hire, terminate, transfer, promote,
  start/adjust/return karenz, change data, rehire, create position, staff
  internally, apply/undo reorg). Each resolves manager_id server-side,
  writes history + audit atomically, and enforces hr_admin via
  require_hr_admin() (backed by the existing RLS policy).
- actions/employees.ts, positions.ts, reorg.ts: Server Actions wrapping
  the RPCs, returning success/error for client-side toast handling.
- Employees list (search/filter/pagination) and detail (4 tabs: Stammdaten,
  Vertrag & Gehalt, Organisation, Historie) reading from employees_directory.
- 6 action slide-over panels: Transfer, Promote, Karenz (start/adjust/
  return), Daten aendern (person+contract diffing), Terminate (with direct-
  report reparenting warning + offboarding checklist), Rehire.
- lib/org.ts: shared division/department/team/location lookups.

Verified live: promote mutation updates salary, writes history/audit, and
the detail page reflects it after refresh, no console errors.

Note: the spec's Karenz-verwalten panel only covers employees already on
Karenz; added a start-Karenz mode (Karenzbeginn/geplante Rueckkehr) to
cover the Aktiv-employee case implied by the header button but not
specified in the panel list.
2026-07-13 22:07:38 +02:00

115 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { promoteEmployee } from "@/actions/employees";
import { SlideOver } from "@/components/ui/SlideOver";
import { useToast } from "@/components/ui/Toast";
import type { Database, PaygradeType } from "@/lib/supabase/types";
type EmployeeRow = Database["public"]["Views"]["employees_directory"]["Row"];
const PAYGRADES: { value: PaygradeType; label: string }[] = [
{ value: "A", label: "A Einstieg" },
{ value: "B", label: "B Qualifiziert" },
{ value: "C", label: "C Erfahren" },
{ value: "D", label: "D Spezialist:in" },
{ value: "E", label: "E Teamleitung" },
{ value: "F", label: "F Bereichsleitung / GF" },
];
export function PromotePanel({ open, onClose, employee }: { open: boolean; onClose: () => void; employee: EmployeeRow }) {
const { showToast } = useToast();
const router = useRouter();
const [effectiveDate, setEffectiveDate] = useState("");
const [newTitle, setNewTitle] = useState(employee.job_title);
const [newSalary, setNewSalary] = useState(String(employee.monthly_salary_gross ?? ""));
const [paygrade, setPaygrade] = useState<PaygradeType>(employee.paygrade);
const [pending, setPending] = useState(false);
async function handleSubmit() {
if (!effectiveDate || !newTitle || !newSalary) {
showToast("Bitte alle Pflichtfelder ausfüllen.", "error");
return;
}
setPending(true);
const result = await promoteEmployee({
employee_id: employee.id,
effective_date: effectiveDate,
new_title: newTitle,
new_salary: Number(newSalary),
new_paygrade: paygrade,
});
setPending(false);
if (result.success) {
showToast(`${employee.first_name} ${employee.last_name} wurde befördert.`);
router.refresh();
onClose();
} else {
showToast(result.error ?? "Fehler beim Speichern.", "error");
}
}
return (
<SlideOver
open={open}
onClose={onClose}
title="Beförderung"
subtitle={`${employee.first_name} ${employee.last_name} · ${employee.job_title}`}
footer={
<>
<button onClick={onClose} className="rounded px-4 py-2 text-sm font-semibold text-ink-body hover:bg-surface">
Abbrechen
</button>
<button
onClick={handleSubmit}
disabled={pending}
className="rounded bg-purple-text px-4 py-2 text-sm font-semibold text-white disabled:opacity-50"
>
Befördern
</button>
</>
}
>
<div className="flex flex-col gap-4">
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Wirksam ab*</label>
<input
type="date"
value={effectiveDate}
onChange={(e) => setEffectiveDate(e.target.value)}
className="w-full rounded border border-border px-3 py-2 text-sm"
/>
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Neue Position*</label>
<input value={newTitle} onChange={(e) => setNewTitle(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" />
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Neues Bruttogehalt (14x/Jahr)*</label>
<input
type="number"
value={newSalary}
onChange={(e) => setNewSalary(e.target.value)}
className="w-full rounded border border-border px-3 py-2 text-sm"
/>
</div>
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Paygrade</label>
<select
value={paygrade}
onChange={(e) => setPaygrade(e.target.value as PaygradeType)}
className="w-full rounded border border-border px-3 py-2 text-sm"
>
{PAYGRADES.map((p) => (
<option key={p.value} value={p.value}>
{p.label}
</option>
))}
</select>
</div>
</div>
</SlideOver>
);
}