Files
timemaster/frontend/src/utils/calendar.ts
T
patrickandClaude Opus 4.8 eab41ede69 feat: agent-12 – Zwei-Stufen-Genehmigung für Abwesenheiten
Optionale zweistufige Freigabe (Feature-Parität mit Urlaubsverwaltung,
Second-Stage-Authority), ohne SSO:

- Firmen-Opt-in companies.two_stage_approval_enabled + two_stage_min_days
  (nur Anträge ab X Arbeitstagen brauchen Stufe 2; 0 = alle).
- Ablauf PENDING → FIRST_APPROVED → APPROVED: erste Stufe durch Manager-Rollen,
  finale Stufe nur HR/Admin und zwingend eine ANDERE Person als Stufe 1.
- Urlaubs-/FZA-Abzug, CalDAV-Sync und Vertreter-Mail erst bei finaler Genehmigung.
  Ablehnen in beiden Stufen möglich; Eigentümer darf FIRST_APPROVED noch stornieren.
- pending_days, Kalender und Reminder-Digest berücksichtigen FIRST_APPROVED.
- Neuer Status-Wert + absences.first_approved_by; System-Kommentar bei Stufe 1.

Frontend: CompanySettingsPage (Toggle + Schwellwert), AbsencesPage
("Endgültig genehmigen"/Ablehnen für HR/Admin ≠ Erstgenehmiger, Status-Badge).

Migration 0038. 190/190 Tests grün. Deployed 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:17:32 +02:00

77 lines
2.1 KiB
TypeScript

import type { AbsenceSpan } from '../types/absence'
export const MONTHS = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
]
export const MONTHS_SHORT = [
'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun',
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez',
]
export const DAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
export const STATUS_LABELS: Record<string, string> = {
pending: 'Ausstehend',
approved: 'Genehmigt',
rejected: 'Abgelehnt',
cancelled: 'Storniert',
cancellation_requested: 'Storno beantragt',
first_approved: 'Wartet auf Endgenehmigung',
}
export const STATUS_COLORS: Record<string, string> = {
pending: 'bg-yellow-100 text-yellow-700',
approved: 'bg-green-100 text-green-700',
rejected: 'bg-red-100 text-red-700',
cancelled: 'bg-gray-100 text-gray-500',
cancellation_requested: 'bg-orange-100 text-orange-700',
first_approved: 'bg-sky-100 text-sky-700',
}
export const MANAGER_ROLES = ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR', 'MANAGER']
export function isoWeekday(d: Date): number {
return (d.getDay() + 6) % 7
}
export function buildCalendarWeeks(year: number, month: number): Date[][] {
const first = new Date(year, month, 1)
const last = new Date(year, month + 1, 0)
const weeks: Date[][] = []
let week: Date[] = []
// Pad from Monday to the first day of the month
const startWd = isoWeekday(first)
for (let i = 0; i < startWd; i++) {
const d = new Date(year, month, 1 - (startWd - i))
week.push(d)
}
for (let day = 1; day <= last.getDate(); day++) {
week.push(new Date(year, month, day))
if (week.length === 7) {
weeks.push(week)
week = []
}
}
// Pad remaining days to complete last week
if (week.length > 0) {
let next = 1
while (week.length < 7) {
week.push(new Date(year, month + 1, next++))
}
weeks.push(week)
}
return weeks
}
export function getWeekSpans(week: Date[], spans: AbsenceSpan[]): AbsenceSpan[] {
const weekStart = week[0]
const weekEnd = week[week.length - 1]
return spans.filter(s => s.start <= weekEnd && s.end >= weekStart)
}