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.
This commit is contained in:
2026-07-13 21:44:28 +02:00
parent d3a5a9fa27
commit ef9852b09c
41 changed files with 9579 additions and 0 deletions

64
lib/format.ts Normal file
View File

@@ -0,0 +1,64 @@
const dateFormatter = new Intl.DateTimeFormat("de-AT", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
const eurFormatter = new Intl.NumberFormat("de-AT", {
style: "currency",
currency: "EUR",
});
export function fmtDate(date: string | Date | null | undefined): string {
if (!date) return "";
const d = typeof date === "string" ? new Date(date) : date;
if (Number.isNaN(d.getTime())) return "";
return dateFormatter.format(d);
}
export function fmtEUR(amount: number | null | undefined): string {
if (amount === null || amount === undefined) return "••• (ausgeblendet)";
return eurFormatter.format(amount);
}
export function initials(firstName: string, lastName: string): string {
const a = firstName.trim().charAt(0).toUpperCase();
const b = lastName.trim().charAt(0).toUpperCase();
return `${a}${b}`;
}
export function fmtAge(birthDate: string | Date): number {
const d = typeof birthDate === "string" ? new Date(birthDate) : birthDate;
const today = new Date();
let age = today.getFullYear() - d.getFullYear();
const hasHadBirthdayThisYear =
today.getMonth() > d.getMonth() ||
(today.getMonth() === d.getMonth() && today.getDate() >= d.getDate());
if (!hasHadBirthdayThisYear) age -= 1;
return age;
}
export function tenure(entryDate: string | Date, endDate?: string | Date | null): string {
const start = typeof entryDate === "string" ? new Date(entryDate) : entryDate;
const end = endDate ? (typeof endDate === "string" ? new Date(endDate) : endDate) : new Date();
let years = end.getFullYear() - start.getFullYear();
let months = end.getMonth() - start.getMonth();
if (end.getDate() < start.getDate()) months -= 1;
if (months < 0) {
years -= 1;
months += 12;
}
if (years < 0) return "0 Monate";
const yearPart = years > 0 ? `${years} ${years === 1 ? "Jahr" : "Jahre"}` : "";
const monthPart = months > 0 ? `${months} ${months === 1 ? "Monat" : "Monate"}` : "";
if (yearPart && monthPart) return `${yearPart}, ${monthPart}`;
return yearPart || monthPart || "unter 1 Monat";
}
export function daysBetween(a: string | Date, b: string | Date = new Date()): number {
const start = typeof a === "string" ? new Date(a) : a;
const end = typeof b === "string" ? new Date(b) : b;
return Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
}