From 901c5c426e8a634b49589aafdc1929eda12f461b Mon Sep 17 00:00:00 2001 From: Maximilian Stubhan Date: Tue, 14 Jul 2026 20:32:20 +0200 Subject: [PATCH] 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`). --- .env.example | 5 + .gitignore | 10 + actions/employees.ts | 4 +- actions/positions.ts | 2 +- app/(app)/audit/page.tsx | 1 - app/(app)/employees/[id]/page.tsx | 13 +- app/(app)/employees/page.tsx | 2 +- app/(app)/layout.tsx | 24 +- app/(app)/orgchart/page.tsx | 2 +- app/(app)/page.tsx | 20 +- app/(app)/positions/page.tsx | 4 +- app/(app)/reports/page.tsx | 4 +- app/(auth)/login/page.tsx | 7 +- app/api/cron/apply-pending-changes/route.ts | 24 + components/employees/EmployeeDetail.tsx | 85 +- .../employees/panels/DatenAendernPanel.tsx | 2 +- components/employees/panels/KarenzPanel.tsx | 2 +- components/employees/panels/PromotePanel.tsx | 15 +- components/employees/panels/RehirePanel.tsx | 2 +- .../employees/panels/TerminatePanel.tsx | 2 +- components/employees/panels/TransferPanel.tsx | 2 +- components/employees/tabs/StammdatenTab.tsx | 2 +- components/employees/tabs/VertragTab.tsx | 5 +- components/hire/HireWizard.tsx | 31 +- components/hire/HireWizardContext.tsx | 7 + components/hire/StepSummary.tsx | 3 +- components/hire/StepVertrag.tsx | 4 - components/hire/types.ts | 2 - components/reports/ReportsPageClient.tsx | 1 - components/shell/Topbar.tsx | 7 +- components/ui/CountryPicker.tsx | 10 +- components/ui/Lookup.tsx | 32 +- lib/format.ts | 10 - lib/positions.ts | 2 +- lib/reports.ts | 11 +- lib/supabase/types.ts | 120 +- package-lock.json | 1225 ++++++++++++++++- package.json | 13 +- proxy.ts | 29 +- supabase/config.toml | 33 + .../20260601000000_initial_schema.sql | 360 +++++ ...01000100_nationality_country_free_text.sql | 11 + ...0601000200_business_mutation_functions.sql | 555 ++++++++ .../20260601000300_start_karenz_function.sql | 29 + ...00400_reorg_undo_history_delete_policy.sql | 15 + ...00_change_employee_data_effective_date.sql | 71 + .../20260714120000_hr_only_access.sql | 172 +++ .../20260714120050_pending_org_changes.sql | 38 + .../20260714120100_salary_deprecation.sql | 125 ++ .../20260714120200_effective_dating_rpcs.sql | 387 ++++++ .../20260714120300_reorg_undo_append_only.sql | 59 + .../20260714120400_performance_indexes.sql | 33 + .../20260714120500_default_grants.sql | 25 + .../20260714120600_data_integrity_guards.sql | 248 ++++ supabase/seed.ts | 105 +- tests/integration/authorization.test.ts | 93 ++ tests/integration/data-integrity.test.ts | 123 ++ tests/integration/effective-dating.test.ts | 163 +++ tests/integration/helpers.ts | 146 ++ tests/integration/reorg.test.ts | 147 ++ tests/unit/colors.test.ts | 29 + tests/unit/format.test.ts | 78 ++ tests/unit/org.test.ts | 52 + tests/unit/reports.test.ts | 164 +++ vercel.json | 8 + vitest.config.ts | 14 + vitest.integration.config.ts | 22 + 67 files changed, 4765 insertions(+), 286 deletions(-) create mode 100644 app/api/cron/apply-pending-changes/route.ts create mode 100644 supabase/config.toml create mode 100644 supabase/migrations/20260601000000_initial_schema.sql create mode 100644 supabase/migrations/20260601000100_nationality_country_free_text.sql create mode 100644 supabase/migrations/20260601000200_business_mutation_functions.sql create mode 100644 supabase/migrations/20260601000300_start_karenz_function.sql create mode 100644 supabase/migrations/20260601000400_reorg_undo_history_delete_policy.sql create mode 100644 supabase/migrations/20260601000500_change_employee_data_effective_date.sql create mode 100644 supabase/migrations/20260714120000_hr_only_access.sql create mode 100644 supabase/migrations/20260714120050_pending_org_changes.sql create mode 100644 supabase/migrations/20260714120100_salary_deprecation.sql create mode 100644 supabase/migrations/20260714120200_effective_dating_rpcs.sql create mode 100644 supabase/migrations/20260714120300_reorg_undo_append_only.sql create mode 100644 supabase/migrations/20260714120400_performance_indexes.sql create mode 100644 supabase/migrations/20260714120500_default_grants.sql create mode 100644 supabase/migrations/20260714120600_data_integrity_guards.sql create mode 100644 tests/integration/authorization.test.ts create mode 100644 tests/integration/data-integrity.test.ts create mode 100644 tests/integration/effective-dating.test.ts create mode 100644 tests/integration/helpers.ts create mode 100644 tests/integration/reorg.test.ts create mode 100644 tests/unit/colors.test.ts create mode 100644 tests/unit/format.test.ts create mode 100644 tests/unit/org.test.ts create mode 100644 tests/unit/reports.test.ts create mode 100644 vercel.json create mode 100644 vitest.config.ts create mode 100644 vitest.integration.config.ts diff --git a/.env.example b/.env.example index d76a042..0fb0ab8 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,8 @@ NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= + +# Shared secret Vercel Cron sends as `Authorization: Bearer ` when it +# calls /api/cron/apply-pending-changes (set the same value in the Vercel +# project's env vars). Generate with e.g. `openssl rand -hex 32`. +CRON_SECRET= diff --git a/.gitignore b/.gitignore index c12de23..be6aeb8 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,13 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# supabase CLI local dev (generated, project-machine-specific) +/supabase/.branches +/supabase/.temp +/supabase/snippets + +# playwright +/playwright-report +/test-results +/blob-report diff --git a/actions/employees.ts b/actions/employees.ts index 50ed41d..7768405 100644 --- a/actions/employees.ts +++ b/actions/employees.ts @@ -31,7 +31,6 @@ export async function hireEmployee(payload: { contract_end_date?: string; employment_type?: "Vollzeit" | "Teilzeit"; weekly_hours?: number; - monthly_salary_gross: number; paygrade?: "A" | "B" | "C" | "D" | "E" | "F"; source: "Intern" | "Extern"; }): Promise { @@ -66,7 +65,6 @@ export async function promoteEmployee(payload: { employee_id: string; effective_date: string; new_title: string; - new_salary: number; new_paygrade?: "A" | "B" | "C" | "D" | "E" | "F"; }): Promise { return callRpc("promote_employee", payload, [`/employees/${payload.employee_id}`, "/employees"]); @@ -119,7 +117,7 @@ export type EmployeeSearchResult = { id: string; first_name: string; last_name: export async function searchActiveEmployees(query: string): Promise { const supabase = await createClient(); let q = supabase - .from("employees_directory") + .from("employees") .select("id, first_name, last_name, job_title, team_id") .in("status", ["Aktiv", "Karenz"]) .limit(20); diff --git a/actions/positions.ts b/actions/positions.ts index 044564b..ae9f174 100644 --- a/actions/positions.ts +++ b/actions/positions.ts @@ -43,7 +43,7 @@ export type SuperiorSearchResult = { id: string; first_name: string; last_name: export async function searchSuperiors(query: string, forLeadPosition: boolean): Promise { const supabase = await createClient(); let q = supabase - .from("employees_directory") + .from("employees") .select("id, first_name, last_name, job_title, division_id") .eq("status", "Aktiv") .limit(20); diff --git a/app/(app)/audit/page.tsx b/app/(app)/audit/page.tsx index d149e3e..ac14d84 100644 --- a/app/(app)/audit/page.tsx +++ b/app/(app)/audit/page.tsx @@ -2,7 +2,6 @@ import Link from "next/link"; import { Suspense } from "react"; import { AuditFilters } from "@/components/audit/AuditFilters"; import { actionBadgeStyle } from "@/lib/colors"; -import { fmtDate } from "@/lib/format"; import { createClient } from "@/lib/supabase/server"; const PAGE_SIZE = 25; diff --git a/app/(app)/employees/[id]/page.tsx b/app/(app)/employees/[id]/page.tsx index 0e7bd22..4669ef9 100644 --- a/app/(app)/employees/[id]/page.tsx +++ b/app/(app)/employees/[id]/page.tsx @@ -8,22 +8,16 @@ export default async function EmployeeDetailPage({ params }: PageProps) { const { id } = await params; const supabase = await createClient(); - const { - data: { user }, - } = await supabase.auth.getUser(); - const { data: profile } = await supabase.from("profiles").select("role").eq("id", user!.id).single(); - const canEdit = profile?.role === "hr_admin"; - - const { data: employee } = await supabase.from("employees_directory").select("*").eq("id", id).single(); + const { data: employee } = await supabase.from("employees").select("*").eq("id", id).single(); if (!employee) notFound(); const [{ data: manager }, { data: directReports }, { data: history }, { data: divisions }, { data: departments }, { data: teams }, { data: locations }, { data: openPositions }] = await Promise.all([ employee.manager_id - ? supabase.from("employees_directory").select("id, first_name, last_name, job_title").eq("id", employee.manager_id).single() + ? supabase.from("employees").select("id, first_name, last_name, job_title").eq("id", employee.manager_id).single() : Promise.resolve({ data: null }), supabase - .from("employees_directory") + .from("employees") .select("id, first_name, last_name, job_title, status") .eq("manager_id", id) .order("last_name"), @@ -46,7 +40,6 @@ export default async function EmployeeDetailPage({ params }: PageProps) { teams={teams ?? []} locations={locations ?? []} openPositions={openPositions ?? []} - canEdit={canEdit} /> ); } diff --git a/app/(app)/employees/page.tsx b/app/(app)/employees/page.tsx index 73dcbad..371fa80 100644 --- a/app/(app)/employees/page.tsx +++ b/app/(app)/employees/page.tsx @@ -36,7 +36,7 @@ export default async function EmployeesPage({ searchParams }: EmployeesPageProps const to = from + PAGE_SIZE - 1; let query = supabase - .from("employees_directory") + .from("employees") .select( "id, first_name, last_name, personnel_number, job_title, team_id, division_id, location_id, entry_date, employment_type, weekly_hours, status", { count: "exact" } diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx index c1d2a50..5736afa 100644 --- a/app/(app)/layout.tsx +++ b/app/(app)/layout.tsx @@ -13,25 +13,27 @@ export default async function AppLayout({ children }: { children: ReactNode }) { } = await supabase.auth.getUser(); if (!user) redirect("/login"); - const { data: profile } = await supabase.from("profiles").select("full_name, email, role").eq("id", user.id).single(); - const canEdit = profile?.role === "hr_admin"; + // Defense in depth: proxy.ts already redirects any non-active-HR session + // away before this layout ever renders. Re-checking here means a gap in + // the proxy matcher (or a future route added outside it) still fails + // closed instead of silently granting access — see docs/security.md. + const { data: profile } = await supabase.from("profiles").select("full_name, email, role, is_active").eq("id", user.id).maybeSingle(); + if (profile?.role !== "hr" || profile?.is_active !== true) redirect("/login"); - const userLabel = profile?.full_name || profile?.email || user.email || ""; + const userLabel = profile.full_name || profile.email || user.email || ""; - const [openPositions, locationsRes, draftsRes] = canEdit - ? await Promise.all([ - loadOpenPositions(supabase), - supabase.from("locations").select("id, name, country").order("name"), - supabase.from("hire_drafts").select("id, step, payload, updated_at").eq("created_by", user.id).order("updated_at", { ascending: false }), - ]) - : [[], { data: [] }, { data: [] }]; + const [openPositions, locationsRes, draftsRes] = await Promise.all([ + loadOpenPositions(supabase), + supabase.from("locations").select("id, name, country").order("name"), + supabase.from("hire_drafts").select("id, step, payload, updated_at").eq("created_by", user.id).order("updated_at", { ascending: false }), + ]); return (
- +
{children}
diff --git a/app/(app)/orgchart/page.tsx b/app/(app)/orgchart/page.tsx index 8e11b5a..0f2a6bc 100644 --- a/app/(app)/orgchart/page.tsx +++ b/app/(app)/orgchart/page.tsx @@ -14,7 +14,7 @@ export default async function OrgChartPage() { { data: reorgScenarios }, ] = await Promise.all([ supabase - .from("employees_directory") + .from("employees") .select("id, personnel_number, first_name, last_name, job_title, manager_id, team_id, division_id, is_lead, org_level") .in("status", ["Aktiv", "Karenz"]), supabase.from("divisions").select("*").order("name"), diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index f642400..30c36d1 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -67,36 +67,36 @@ export default async function DashboardPage() { upcomingReturnsRes, historyRes, ] = await Promise.all([ - supabase.from("employees_directory").select("id", { count: "exact", head: true }).in("status", ["Aktiv", "Karenz"]), - supabase.from("employees_directory").select("id", { count: "exact", head: true }).eq("status", "Karenz"), + supabase.from("employees").select("id", { count: "exact", head: true }).in("status", ["Aktiv", "Karenz"]), + supabase.from("employees").select("id", { count: "exact", head: true }).eq("status", "Karenz"), supabase - .from("employees_directory") + .from("employees") .select("id", { count: "exact", head: true }) .gte("entry_date", yearStart) .lte("entry_date", yearEnd), supabase - .from("employees_directory") + .from("employees") .select("id", { count: "exact", head: true }) .gte("exit_date", yearStart) .lte("exit_date", yearEnd), supabase.from("positions").select("id", { count: "exact", head: true }).eq("status", "open"), - supabase.from("employees_directory").select("weekly_hours").in("status", ["Aktiv", "Karenz"]), + supabase.from("employees").select("weekly_hours").in("status", ["Aktiv", "Karenz"]), supabase.from("divisions").select("id, name"), - supabase.from("employees_directory").select("division_id").in("status", ["Aktiv", "Karenz"]), + supabase.from("employees").select("division_id").in("status", ["Aktiv", "Karenz"]), supabase - .from("employees_directory") + .from("employees") .select("id, first_name, last_name, entry_date") .eq("status", "Geplant") .gte("entry_date", todayIso) .lte("entry_date", in60Iso), supabase - .from("employees_directory") + .from("employees") .select("id, first_name, last_name, exit_date") .not("exit_date", "is", null) .gte("exit_date", todayIso) .lte("exit_date", in60Iso), supabase - .from("employees_directory") + .from("employees") .select("id, first_name, last_name, karenz_return_date") .eq("status", "Karenz") .not("karenz_return_date", "is", null) @@ -148,7 +148,7 @@ export default async function DashboardPage() { const historyEmployeeIds = Array.from(new Set((historyRes.data ?? []).map((h) => h.employee_id))); const historyEmployeesRes = historyEmployeeIds.length - ? await supabase.from("employees_directory").select("id, first_name, last_name").in("id", historyEmployeeIds) + ? await supabase.from("employees").select("id, first_name, last_name").in("id", historyEmployeeIds) : { data: [] as { id: string; first_name: string; last_name: string }[] }; const employeeNameById = new Map((historyEmployeesRes.data ?? []).map((e) => [e.id, `${e.first_name} ${e.last_name}`])); diff --git a/app/(app)/positions/page.tsx b/app/(app)/positions/page.tsx index e2a23c4..386735c 100644 --- a/app/(app)/positions/page.tsx +++ b/app/(app)/positions/page.tsx @@ -12,9 +12,9 @@ export default async function PositionsPage() { supabase.from("divisions").select("*").order("name"), supabase.from("departments").select("*"), supabase.from("teams").select("*"), - supabase.from("employees_directory").select("team_id, division_id, weekly_hours").in("status", ["Aktiv", "Karenz"]), + supabase.from("employees").select("team_id, division_id, weekly_hours").in("status", ["Aktiv", "Karenz"]), supabase - .from("employees_directory") + .from("employees") .select("id, first_name, last_name, team_id, division_id, org_level, is_lead") .eq("status", "Aktiv") .or("is_lead.eq.true,org_level.eq.0"), diff --git a/app/(app)/reports/page.tsx b/app/(app)/reports/page.tsx index ef5c8a2..2cc062e 100644 --- a/app/(app)/reports/page.tsx +++ b/app/(app)/reports/page.tsx @@ -47,9 +47,9 @@ export default async function ReportsPage({ searchParams }: { searchParams: Prom }; let query = supabase - .from("employees_directory") + .from("employees") .select( - "id, first_name, last_name, job_title, division_id, team_id, location_id, status, employment_type, contract_type, entry_date, exit_date, weekly_hours, monthly_salary_gross, source, paygrade, birth_date, gender" + "id, first_name, last_name, job_title, division_id, team_id, location_id, status, employment_type, contract_type, entry_date, exit_date, weekly_hours, source, paygrade, birth_date, gender" ); if (params.division) query = query.eq("division_id", params.division); diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index dfcd21f..3730d9a 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,4 +1,4 @@ -import { login } from "@/actions/auth"; +import { login, logout } from "@/actions/auth"; type LoginPageProps = { searchParams: Promise<{ error?: string }>; @@ -16,6 +16,11 @@ export default async function LoginPage({ searchParams }: LoginPageProps) { {error && (
{error} +
+ +
)} diff --git a/app/api/cron/apply-pending-changes/route.ts b/app/api/cron/apply-pending-changes/route.ts new file mode 100644 index 0000000..cdc5641 --- /dev/null +++ b/app/api/cron/apply-pending-changes/route.ts @@ -0,0 +1,24 @@ +import { NextResponse, type NextRequest } from "next/server"; +import { createAdminClient } from "@/lib/supabase/admin"; + +// Applies effective-dated changes (Versetzung/Beförderung/Karenz/Reorg/Daten +// ändern with a future "Wirksam ab" date) once their date has arrived — see +// apply_due_pending_changes() in supabase/migrations. Runs as a Vercel Cron +// job (see vercel.json), not on behalf of any HR user, so it authenticates +// via a shared secret rather than a Supabase session and uses the +// service-role client (the one legitimate server-only use case for it). +export async function GET(request: NextRequest) { + const authHeader = request.headers.get("authorization"); + if (!process.env.CRON_SECRET || authHeader !== `Bearer ${process.env.CRON_SECRET}`) { + return NextResponse.json({ error: "Nicht autorisiert." }, { status: 401 }); + } + + const supabase = createAdminClient(); + const { data, error } = await supabase.rpc("apply_due_pending_changes"); + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + return NextResponse.json({ applied: data }); +} diff --git a/components/employees/EmployeeDetail.tsx b/components/employees/EmployeeDetail.tsx index d497435..14974ac 100644 --- a/components/employees/EmployeeDetail.tsx +++ b/components/employees/EmployeeDetail.tsx @@ -17,7 +17,7 @@ import { OrganisationTab } from "./tabs/OrganisationTab"; import { StammdatenTab } from "./tabs/StammdatenTab"; import { VertragTab } from "./tabs/VertragTab"; -type EmployeeRow = Database["public"]["Views"]["employees_directory"]["Row"]; +type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; type Division = Database["public"]["Tables"]["divisions"]["Row"]; type Department = Database["public"]["Tables"]["departments"]["Row"]; type Team = Database["public"]["Tables"]["teams"]["Row"]; @@ -36,14 +36,13 @@ type EmployeeDetailProps = { teams: Team[]; locations: Location[]; openPositions: OpenPosition[]; - canEdit: boolean; }; type PanelType = "transfer" | "promote" | "karenz" | "daten" | "terminate" | "rehire" | null; -const TABS = ["Stammdaten", "Vertrag & Gehalt", "Organisation", "Historie"] as const; +const TABS = ["Stammdaten", "Vertrag", "Organisation", "Historie"] as const; export function EmployeeDetail(props: EmployeeDetailProps) { - const { employee, manager, directReports, history, divisions, departments, teams, locations, canEdit } = props; + const { employee, manager, directReports, history, divisions, departments, teams, locations } = props; const [tab, setTab] = useState<(typeof TABS)[number]>("Stammdaten"); const [panel, setPanel] = useState(null); @@ -77,32 +76,30 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
- {canEdit && ( -
- {isActive && ( - <> - setPanel("transfer")} /> - setPanel("promote")} /> - setPanel("karenz")} /> - setPanel("daten")} /> - - - )} - {employee.status === "Ausgetreten" && ( +
+ {isActive && ( + <> + setPanel("transfer")} /> + setPanel("promote")} /> + setPanel("karenz")} /> + setPanel("daten")} /> - )} -
- )} + + )} + {employee.status === "Ausgetreten" && ( + + )} +
@@ -122,29 +119,25 @@ export function EmployeeDetail(props: EmployeeDetailProps) {
{tab === "Stammdaten" && } - {tab === "Vertrag & Gehalt" && } + {tab === "Vertrag" && } {tab === "Organisation" && } {tab === "Historie" && }
- {canEdit && ( - <> - setPanel(null)} - employee={employee} - divisions={divisions} - departments={departments} - teams={teams} - currentTeamId={employee.team_id} - /> - setPanel(null)} employee={employee} /> - setPanel(null)} employee={employee} /> - setPanel(null)} employee={employee} /> - setPanel(null)} employee={employee} directReportCount={directReports.length} /> - setPanel(null)} employee={employee} /> - - )} + setPanel(null)} + employee={employee} + divisions={divisions} + departments={departments} + teams={teams} + currentTeamId={employee.team_id} + /> + setPanel(null)} employee={employee} /> + setPanel(null)} employee={employee} /> + setPanel(null)} employee={employee} /> + setPanel(null)} employee={employee} directReportCount={directReports.length} /> + setPanel(null)} employee={employee} /> ); } diff --git a/components/employees/panels/DatenAendernPanel.tsx b/components/employees/panels/DatenAendernPanel.tsx index 3c9d18c..5f1fcef 100644 --- a/components/employees/panels/DatenAendernPanel.tsx +++ b/components/employees/panels/DatenAendernPanel.tsx @@ -9,7 +9,7 @@ import { useToast } from "@/components/ui/Toast"; import { UN_COUNTRIES } from "@/lib/countries"; import type { ContractType, Database, EmploymentType, GenderType } from "@/lib/supabase/types"; -type EmployeeRow = Database["public"]["Views"]["employees_directory"]["Row"]; +type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; export function DatenAendernPanel({ open, onClose, employee }: { open: boolean; onClose: () => void; employee: EmployeeRow }) { const { showToast } = useToast(); diff --git a/components/employees/panels/KarenzPanel.tsx b/components/employees/panels/KarenzPanel.tsx index dfc16f8..06e8bde 100644 --- a/components/employees/panels/KarenzPanel.tsx +++ b/components/employees/panels/KarenzPanel.tsx @@ -9,7 +9,7 @@ import { useToast } from "@/components/ui/Toast"; import { fmtDate } from "@/lib/format"; import type { Database } from "@/lib/supabase/types"; -type EmployeeRow = Database["public"]["Views"]["employees_directory"]["Row"]; +type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; type Mode = "adjust" | "return"; type EmploymentMode = "unverändert" | "Vollzeit" | "Teilzeit"; diff --git a/components/employees/panels/PromotePanel.tsx b/components/employees/panels/PromotePanel.tsx index 173d761..b2778f9 100644 --- a/components/employees/panels/PromotePanel.tsx +++ b/components/employees/panels/PromotePanel.tsx @@ -7,7 +7,7 @@ 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"]; +type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"]; const PAYGRADES: { value: PaygradeType; label: string }[] = [ { value: "A", label: "A – Einstieg" }, @@ -23,12 +23,11 @@ export function PromotePanel({ open, onClose, employee }: { open: boolean; onClo 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(employee.paygrade); const [pending, setPending] = useState(false); async function handleSubmit() { - if (!effectiveDate || !newTitle || !newSalary) { + if (!effectiveDate || !newTitle) { showToast("Bitte alle Pflichtfelder ausfüllen.", "error"); return; } @@ -37,7 +36,6 @@ export function PromotePanel({ open, onClose, employee }: { open: boolean; onClo employee_id: employee.id, effective_date: effectiveDate, new_title: newTitle, - new_salary: Number(newSalary), new_paygrade: paygrade, }); setPending(false); @@ -85,15 +83,6 @@ export function PromotePanel({ open, onClose, employee }: { open: boolean; onClo setNewTitle(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm" /> -
- - setNewSalary(e.target.value)} - className="w-full rounded border border-border px-3 py-2 text-sm" - /> -
update({ salary: e.target.value })} className="w-full rounded border border-border px-3 py-2 text-sm" /> -