Initial commit – TimeMaster Zeiterfassung & HR-Tool

Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-05-23 20:03:27 +02:00
co-authored by Claude Sonnet 4.6
commit 45218cc744
177 changed files with 29145 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
import { Link, useLocation } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { useState, useRef, useEffect } from 'react'
interface NavItem {
path: string
label: string
roles?: string[]
}
const MAIN_NAV: NavItem[] = [
{ path: '/dashboard', label: 'Dashboard' },
{ path: '/time', label: 'Zeiterfassung' },
{ path: '/absences', label: 'Abwesenheiten' },
{ path: '/calendar', label: 'Kalender' },
{ path: '/reports', label: 'Berichte', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR', 'MANAGER'] },
{ path: '/users', label: 'Mitarbeiter', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR'] },
]
const SETTINGS_NAV: NavItem[] = [
{ path: '/work-schedules', label: 'Arbeitspläne', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] },
{ path: '/settings/absence-types', label: 'Abwesenheitstypen', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] },
{ path: '/settings/company', label: 'Firma', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] },
{ path: '/settings/kiosk', label: 'Kiosk-Geräte', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] },
{ path: '/users/import', label: 'Mitarbeiter-Import', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] },
{ path: '/settings/import', label: 'Daten importieren', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR'] },
{ path: '/settings/caldav', label: 'CalDAV' },
{ path: '/settings/ldap', label: 'LDAP', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] },
{ path: '/settings/smtp', label: 'SMTP', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] },
{ path: '/settings/audit-log', label: 'Audit-Log', roles: ['COMPANY_ADMIN', 'SUPER_ADMIN'] },
]
const ROLE_LABELS: Record<string, string> = {
SUPER_ADMIN: 'Super Admin',
COMPANY_ADMIN: 'Administrator',
HR: 'HR',
MANAGER: 'Manager',
EMPLOYEE: 'Mitarbeiter',
}
export function Layout({ children, userRole, userName }: {
children: React.ReactNode
userRole: string
userName: string
}) {
const { logout } = useAuth()
const { pathname } = useLocation()
const [settingsOpen, setSettingsOpen] = useState(false)
const settingsRef = useRef<HTMLDivElement>(null)
const visibleMain = MAIN_NAV.filter(n => !n.roles || n.roles.includes(userRole))
const visibleSettings = SETTINGS_NAV.filter(n => !n.roles || n.roles.includes(userRole))
const isSettingsActive = visibleSettings.some(n => pathname === n.path)
useEffect(() => {
function handleClick(e: MouseEvent) {
if (settingsRef.current && !settingsRef.current.contains(e.target as Node))
setSettingsOpen(false)
}
if (settingsOpen) document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [settingsOpen])
useEffect(() => { setSettingsOpen(false) }, [pathname])
const initials = userName
? userName.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
: '?'
return (
<div className='min-h-screen bg-gray-50'>
<header className='bg-white border-b border-gray-200 sticky top-0 z-30'>
<div className='max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-2'>
{/* Logo */}
<Link to='/dashboard' className='flex items-center gap-2 flex-shrink-0'>
<div className='w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center'>
<span className='text-white font-bold text-sm'>TM</span>
</div>
<span className='font-bold text-gray-800 hidden lg:block text-sm'>TimeMaster</span>
</Link>
{/* Hauptnavigation */}
<nav className='flex items-center gap-0.5 flex-1 overflow-x-auto'>
{visibleMain.map(n => (
<Link
key={n.path}
to={n.path}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${
pathname === n.path
? 'bg-blue-50 text-blue-700'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-800'
}`}
>
{n.label}
</Link>
))}
</nav>
{/* Rechte Seite: Einstellungen + User + Abmelden */}
<div className='flex items-center gap-1 flex-shrink-0'>
{/* Zahnrad-Dropdown */}
{visibleSettings.length > 0 && (
<div ref={settingsRef} className='relative'>
<button
onClick={() => setSettingsOpen(o => !o)}
title='Einstellungen'
className={`p-2 rounded-lg transition-colors ${
isSettingsActive || settingsOpen
? 'bg-blue-50 text-blue-700'
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-700'
}`}
>
<svg className='w-5 h-5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1.75}
d='M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' />
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1.75} d='M15 12a3 3 0 11-6 0 3 3 0 016 0z' />
</svg>
</button>
{settingsOpen && (
<div className='absolute right-0 top-full mt-1.5 w-52 bg-white rounded-xl shadow-lg border border-gray-200 py-1.5 z-50'>
<p className='px-4 py-1.5 text-xs font-semibold text-gray-400 uppercase tracking-wide'>Einstellungen</p>
{visibleSettings.map((n, i) => {
const divider = i > 0 && n.path === '/settings/caldav'
return (
<div key={n.path}>
{divider && <div className='my-1 border-t border-gray-100' />}
<Link
to={n.path}
className={`flex items-center px-4 py-2 text-sm transition-colors ${
pathname === n.path
? 'bg-blue-50 text-blue-700 font-medium'
: 'text-gray-700 hover:bg-gray-50'
}`}
>
{n.label}
</Link>
</div>
)
})}
</div>
)}
</div>
)}
{/* User-Avatar → Profil */}
<Link
to='/profile'
title={`${userName} · ${ROLE_LABELS[userRole] ?? userRole}`}
className='flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-gray-100 transition-colors'
>
<div className='w-7 h-7 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0'>
<span className='text-blue-700 font-bold text-xs'>{initials}</span>
</div>
<div className='hidden md:block text-left'>
<p className='text-sm font-medium text-gray-800 leading-tight'>{userName}</p>
<p className='text-xs text-gray-400 leading-tight'>{ROLE_LABELS[userRole] ?? userRole}</p>
</div>
</Link>
{/* Abmelden */}
<button
onClick={logout}
title='Abmelden'
className='p-2 rounded-lg text-gray-500 hover:text-red-600 hover:bg-red-50 transition-colors'
>
<svg className='w-5 h-5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={1.75}
d='M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1' />
</svg>
</button>
</div>
</div>
</header>
<main className='max-w-7xl mx-auto px-4 sm:px-6 py-8'>
{children}
</main>
</div>
)
}
+32
View File
@@ -0,0 +1,32 @@
import { ReactNode } from 'react'
interface ModalProps {
title: string
children: ReactNode
onClose: () => void
size?: 'sm' | 'md' | 'lg' | 'xl'
}
const SIZE_CLASS: Record<NonNullable<ModalProps['size']>, string> = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
}
export function Modal({ title, children, onClose, size = 'md' }: ModalProps) {
return (
<div className='fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4' onClick={onClose}>
<div
className={`bg-white rounded-2xl shadow-xl w-full ${SIZE_CLASS[size]} p-6 max-h-[90vh] overflow-auto`}
onClick={e => e.stopPropagation()}
>
<div className='flex justify-between items-center mb-4'>
<h2 className='text-base font-semibold text-gray-800'>{title}</h2>
<button onClick={onClose} className='text-gray-400 hover:text-gray-600 text-xl leading-none'>&times;</button>
</div>
{children}
</div>
</div>
)
}
@@ -0,0 +1,7 @@
import { Navigate, Outlet } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
export function ProtectedRoute() {
const { token } = useAuth()
return token ? <Outlet /> : <Navigate to='/login' replace />
}
+7
View File
@@ -0,0 +1,7 @@
export function Spinner() {
return (
<div className='flex justify-center items-center h-32'>
<div className='animate-spin rounded-full h-10 w-10 border-4 border-blue-500 border-t-transparent' />
</div>
)
}
@@ -0,0 +1,431 @@
import type {
AbsenceTypeOut,
AbsenceOut,
UserListItem,
VacationBalanceOut,
OvertimeBalanceOut,
} from '../../types/absence'
// ── BalanceEditModal ──────────────────────────────────────────────────────────
interface BalanceEditModalProps {
year: number
balance: VacationBalanceOut
balanceForm: { entitled_days: number; special_days: number; carried_over: number }
setBalanceForm: React.Dispatch<React.SetStateAction<{ entitled_days: number; special_days: number; carried_over: number }>>
balanceSaving: boolean
onSave: () => Promise<void>
onClose: () => void
}
export function BalanceEditModal({
year,
balanceForm,
setBalanceForm,
balanceSaving,
onSave,
onClose,
}: BalanceEditModalProps) {
return (
<div className='fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4'>
<div className='bg-white rounded-xl shadow-xl w-full max-w-sm p-6 space-y-4'>
<h2 className='text-lg font-semibold text-gray-800'>Urlaubskonto {year} bearbeiten</h2>
<div className='space-y-3'>
<label className='block'>
<span className='text-sm font-medium text-gray-700'>Grundurlaub (Tage)</span>
<input
type='number' min={0} max={365} value={balanceForm.entitled_days}
onChange={e => setBalanceForm(f => ({ ...f, entitled_days: Number(e.target.value) }))}
className='mt-1 w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
/>
</label>
<label className='block'>
<span className='text-sm font-medium text-gray-700'>Sondertage (Tage)</span>
<p className='text-xs text-gray-400 mb-1'>z.B. Betriebsurlaub, Jubiläum, individuelle Zusatztage</p>
<input
type='number' min={0} max={365} value={balanceForm.special_days}
onChange={e => setBalanceForm(f => ({ ...f, special_days: Number(e.target.value) }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
/>
</label>
<label className='block'>
<span className='text-sm font-medium text-gray-700'>Resturlaub aus {year - 1} (Tage)</span>
<p className='text-xs text-gray-400 mb-1'>Wird automatisch aus dem Vorjahr berechnet</p>
<input
type='number' min={0} max={365} value={balanceForm.carried_over}
onChange={e => setBalanceForm(f => ({ ...f, carried_over: Number(e.target.value) }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
/>
</label>
<div className='bg-blue-50 rounded-lg p-3 text-sm text-blue-700'>
Gesamt: <strong>{balanceForm.entitled_days + balanceForm.special_days + balanceForm.carried_over} Tage</strong>
</div>
</div>
<div className='flex gap-2 pt-1'>
<button
disabled={balanceSaving}
onClick={onSave}
className='flex-1 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 disabled:opacity-50'
>
{balanceSaving ? 'Speichern…' : 'Speichern'}
</button>
<button
onClick={onClose}
className='px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50'
>
Abbrechen
</button>
</div>
</div>
</div>
)
}
// ── EditAbsenceModal ──────────────────────────────────────────────────────────
interface EditAbsenceModalProps {
editAbsence: AbsenceOut
editForm: {
type_id: string
start_date: string
end_date: string
half_day_start: boolean
half_day_end: boolean
note: string
correction_note: string
}
setEditForm: React.Dispatch<React.SetStateAction<{
type_id: string
start_date: string
end_date: string
half_day_start: boolean
half_day_end: boolean
note: string
correction_note: string
}>>
types: AbsenceTypeOut[]
isManager: boolean
submitting: boolean
error: string
onSave: () => void
onClose: () => void
}
export function EditAbsenceModal({
editAbsence,
editForm,
setEditForm,
types,
isManager,
submitting,
error,
onSave,
onClose,
}: EditAbsenceModalProps) {
return (
<div className='fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50'>
<div className='bg-white rounded-xl shadow-xl w-full max-w-md'>
<div className='px-6 py-4 border-b border-gray-200'>
<h2 className='text-lg font-semibold text-gray-800'>Antrag bearbeiten</h2>
</div>
<div className='px-6 py-4 space-y-4'>
{error && <p className='text-sm text-red-600'>{error}</p>}
{editAbsence.status === 'approved' && !isManager && (
<div className='bg-orange-50 border border-orange-200 rounded-lg p-3 text-sm text-orange-700'>
Der Antrag wurde bereits genehmigt. Nach der Änderung wird er zur erneuten Genehmigung eingereicht.
</div>
)}
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Abwesenheitsart *</label>
<select
value={editForm.type_id}
onChange={e => setEditForm(f => ({ ...f, type_id: e.target.value }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
>
<option value=''>Bitte wählen</option>
{types.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
</select>
</div>
<div className='grid grid-cols-2 gap-3'>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Von *</label>
<input
type='date' value={editForm.start_date}
onChange={e => setEditForm(f => ({ ...f, start_date: e.target.value }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Bis *</label>
<input
type='date' value={editForm.end_date} min={editForm.start_date}
onChange={e => setEditForm(f => ({ ...f, end_date: e.target.value }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
/>
</div>
</div>
<div className='flex gap-4'>
<label className='flex items-center gap-2 text-sm text-gray-700 cursor-pointer'>
<input
type='checkbox' checked={editForm.half_day_start}
onChange={e => setEditForm(f => ({ ...f, half_day_start: e.target.checked }))}
className='rounded'
/>
Erster Tag halbtags
</label>
<label className='flex items-center gap-2 text-sm text-gray-700 cursor-pointer'>
<input
type='checkbox' checked={editForm.half_day_end}
onChange={e => setEditForm(f => ({ ...f, half_day_end: e.target.checked }))}
className='rounded'
/>
Letzter Tag halbtags
</label>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Notiz</label>
<textarea
value={editForm.note}
onChange={e => setEditForm(f => ({ ...f, note: e.target.value }))}
rows={2}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none'
/>
</div>
{editAbsence.status === 'approved' && !isManager && (
<div>
<label className='block text-sm font-medium text-red-600 mb-1'>Änderungsgrund *</label>
<input
type='text' value={editForm.correction_note}
onChange={e => setEditForm(f => ({ ...f, correction_note: e.target.value }))}
placeholder='z. B. Urlaub wurde verkürzt'
className='w-full border border-red-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-400'
/>
<p className='text-xs text-gray-400 mt-1'>Wird im Änderungsprotokoll gespeichert.</p>
</div>
)}
</div>
<div className='px-6 py-4 border-t border-gray-200 flex justify-end gap-2'>
<button
onClick={onClose}
className='px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50'
>
Abbrechen
</button>
<button
onClick={onSave}
disabled={submitting}
className='px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 disabled:opacity-50'
>
{submitting
? 'Wird gespeichert…'
: (editAbsence.status === 'approved' && !isManager ? 'Änderung einreichen' : 'Speichern')
}
</button>
</div>
</div>
</div>
)
}
// ── CreateAbsenceModal ────────────────────────────────────────────────────────
interface CreateAbsenceModalProps {
form: {
type_id: string
start_date: string
end_date: string
half_day_start: boolean
half_day_end: boolean
note: string
for_user_id: string
}
setForm: React.Dispatch<React.SetStateAction<{
type_id: string
start_date: string
end_date: string
half_day_start: boolean
half_day_end: boolean
note: string
for_user_id: string
}>>
types: AbsenceTypeOut[]
colleagues: UserListItem[]
isManager: boolean
submitting: boolean
error: string
overtimeBalance: OvertimeBalanceOut | null
onCreate: () => void
onClose: () => void
}
export function CreateAbsenceModal({
form,
setForm,
types,
colleagues,
isManager,
submitting,
error,
overtimeBalance,
onCreate,
onClose,
}: CreateAbsenceModalProps) {
return (
<div className='fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50'>
<div className='bg-white rounded-xl shadow-xl w-full max-w-md'>
<div className='px-6 py-4 border-b border-gray-200'>
<h2 className='text-lg font-semibold text-gray-800'>Abwesenheitsantrag</h2>
</div>
<div className='px-6 py-4 space-y-4'>
{error && <p className='text-sm text-red-600'>{error}</p>}
{isManager && (
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Mitarbeiter</label>
<select
value={form.for_user_id}
onChange={e => setForm(f => ({ ...f, for_user_id: e.target.value }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
>
<option value=''> Für mich selbst </option>
{colleagues.map(c => <option key={c.id} value={c.id}>{c.full_name} ({c.email})</option>)}
</select>
</div>
)}
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Abwesenheitsart *</label>
<select
value={form.type_id}
onChange={e => setForm(f => ({ ...f, type_id: e.target.value }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
>
<option value=''>Bitte wählen</option>
{types.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
</select>
{form.type_id && types.find(t => t.id === form.type_id)?.affects_overtime_balance && overtimeBalance !== null && (
<p className='text-xs mt-1 text-purple-600'>
Überstunden-Konto: <strong>{overtimeBalance.available_hours.toFixed(1)} h verfügbar</strong> ( {(overtimeBalance.available_hours / 8).toFixed(1)} Tage)
</p>
)}
</div>
<div className='grid grid-cols-2 gap-3'>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Von *</label>
<input
type='date' value={form.start_date}
onChange={e => setForm(f => ({ ...f, start_date: e.target.value }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Bis *</label>
<input
type='date' value={form.end_date} min={form.start_date}
onChange={e => setForm(f => ({ ...f, end_date: e.target.value }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
/>
</div>
</div>
<div className='flex gap-4'>
<label className='flex items-center gap-2 text-sm text-gray-700 cursor-pointer'>
<input
type='checkbox' checked={form.half_day_start}
onChange={e => setForm(f => ({ ...f, half_day_start: e.target.checked }))}
className='rounded'
/>
Erster Tag halbtags
</label>
<label className='flex items-center gap-2 text-sm text-gray-700 cursor-pointer'>
<input
type='checkbox' checked={form.half_day_end}
onChange={e => setForm(f => ({ ...f, half_day_end: e.target.checked }))}
className='rounded'
/>
Letzter Tag halbtags
</label>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Notiz</label>
<textarea
value={form.note}
onChange={e => setForm(f => ({ ...f, note: e.target.value }))}
rows={2}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none'
/>
</div>
</div>
<div className='px-6 py-4 border-t border-gray-200 flex justify-end gap-2'>
<button onClick={onClose} className='px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50'>
Abbrechen
</button>
<button
onClick={onCreate}
disabled={submitting}
className='px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 disabled:opacity-50'
>
{submitting ? 'Wird gesendet…' : 'Antrag stellen'}
</button>
</div>
</div>
</div>
)
}
// ── QuickSickModal ────────────────────────────────────────────────────────────
interface QuickSickModalProps {
form: { start_date: string; end_date: string }
setForm: React.Dispatch<React.SetStateAction<{ start_date: string; end_date: string }>>
submitting: boolean
error: string
onSubmit: () => void
onClose: () => void
}
export function QuickSickModal({ form, setForm, submitting, error, onSubmit, onClose }: QuickSickModalProps) {
return (
<div className='fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50'>
<div className='bg-white rounded-xl shadow-xl w-full max-w-sm'>
<div className='px-6 py-4 border-b border-gray-200 flex items-center gap-2'>
<span className='text-2xl'>🤒</span>
<div>
<h2 className='text-lg font-semibold text-gray-800'>Krank melden</h2>
<p className='text-xs text-gray-500'>Wird sofort genehmigt Attest-Frist wird automatisch berechnet.</p>
</div>
</div>
<div className='px-6 py-4 space-y-4'>
{error && <p className='text-sm text-red-600'>{error}</p>}
<div className='grid grid-cols-2 gap-3'>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Von *</label>
<input
type='date' value={form.start_date}
onChange={e => setForm(f => ({ ...f, start_date: e.target.value }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 mb-1'>Bis *</label>
<input
type='date' value={form.end_date} min={form.start_date}
onChange={e => setForm(f => ({ ...f, end_date: e.target.value }))}
className='w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500'
/>
</div>
</div>
</div>
<div className='px-6 py-4 border-t border-gray-200 flex justify-end gap-2'>
<button onClick={onClose} className='px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50'>
Abbrechen
</button>
<button
onClick={onSubmit}
disabled={submitting || !form.start_date || !form.end_date}
className='px-4 py-2 bg-orange-600 text-white rounded-lg text-sm font-medium hover:bg-orange-700 disabled:opacity-50'
>
{submitting ? 'Wird gesendet…' : 'Krank melden'}
</button>
</div>
</div>
</div>
)
}