import { useEffect, useState } from 'react' import type { SpecialAssignmentOut, SpecialAssignmentCreate, AssignmentMode } from '../types/specialAssignment' import { api } from '../api/client' import { Spinner } from '../components/Spinner' import { Layout } from '../components/Layout' import { Modal } from '../components/Modal' interface UserItem { id: string full_name: string personnel_number: string | null } interface UserListResponse { total: number items: UserItem[] } interface Me { first_name: string last_name: string role: string } interface AssignmentRow { assignment: SpecialAssignmentOut user: UserItem } const MODE_LABELS: Record = { fza: 'FZA', payroll: 'Abrechnung', both: 'Beides', } const MODE_COLORS: Record = { fza: 'bg-green-100 text-green-700', payroll: 'bg-blue-100 text-blue-700', both: 'bg-amber-100 text-amber-700', } function factorColor(factor: number): string { if (factor > 1.0) return 'text-amber-700 font-semibold' if (factor < 1.0) return 'text-blue-600 font-semibold' return 'text-gray-400' } function getMonthRange(yearMonth: string): { first: string; last: string } { const [y, m] = yearMonth.split('-').map(Number) const first = `${y}-${String(m).padStart(2, '0')}-01` const lastDate = new Date(y, m, 0) const last = `${y}-${String(m).padStart(2, '0')}-${String(lastDate.getDate()).padStart(2, '0')}` return { first, last } } function overlapsMonth(dateFrom: string, dateTo: string, yearMonth: string): boolean { const { first, last } = getMonthRange(yearMonth) return dateFrom <= last && dateTo >= first } function currentYearMonth(): string { const now = new Date() return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}` } const BATCH_SIZE = 20 const inputClass = 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent' export function SpecialAssignmentsPage() { const [me, setMe] = useState(null) const [users, setUsers] = useState([]) const [pageLoading, setPageLoading] = useState(true) const [pageError, setPageError] = useState(null) // Filter state const [filterUser, setFilterUser] = useState('') const [filterMonth, setFilterMonth] = useState(currentYearMonth()) // Loaded assignments (flat list with user info) const [rows, setRows] = useState([]) const [tableLoading, setTableLoading] = useState(false) // Modal state const [showModal, setShowModal] = useState(false) const [editAssignment, setEditAssignment] = useState(null) const [form, setForm] = useState({ user_id: '', date_from: '', date_to: '', factor: 1.5, mode: 'both', label: '', }) const [modalSaving, setModalSaving] = useState(false) const [modalError, setModalError] = useState(null) // Initial load useEffect(() => { async function init() { try { const [meData, listData] = await Promise.all([ api.get('/auth/me'), api.get('/users/?limit=500'), ]) setMe(meData) setUsers(listData.items) } catch (e: unknown) { setPageError(e instanceof Error ? e.message : 'Fehler beim Laden') } finally { setPageLoading(false) } } init() }, []) async function loadAssignments(userIdFilter: string, usersToQuery: UserItem[]) { setTableLoading(true) setRows([]) try { const targets = userIdFilter ? usersToQuery.filter(u => u.id === userIdFilter) : usersToQuery const allRows: AssignmentRow[] = [] // Process in batches for (let i = 0; i < targets.length; i += BATCH_SIZE) { const batch = targets.slice(i, i + BATCH_SIZE) const results = await Promise.all( batch.map(u => api.get(`/users/${u.id}/special-assignments`) .then(assignments => assignments.map(a => ({ assignment: a, user: u }))) .catch(() => [] as AssignmentRow[]) ) ) for (const chunk of results) { allRows.push(...chunk) } } setRows(allRows) } finally { setTableLoading(false) } } function handleSearch() { if (users.length > 0) { loadAssignments(filterUser, users) } } // Load on mount once users are available useEffect(() => { if (users.length > 0) { loadAssignments(filterUser, users) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [users]) // Filter rows client-side by month const filteredRows = rows .filter(r => overlapsMonth(r.assignment.date_from, r.assignment.date_to, filterMonth)) .sort((a, b) => a.user.full_name.localeCompare(b.user.full_name)) function openNewModal() { setEditAssignment(null) setForm({ user_id: filterUser, date_from: '', date_to: '', factor: 1.5, mode: 'both', label: '' }) setModalError(null) setShowModal(true) } function openEditModal(row: AssignmentRow) { setEditAssignment(row.assignment) setForm({ user_id: row.assignment.user_id, date_from: row.assignment.date_from, date_to: row.assignment.date_to, factor: row.assignment.factor, mode: row.assignment.mode, label: row.assignment.label ?? '', }) setModalError(null) setShowModal(true) } async function handleSave() { if (!form.user_id || !form.date_from || !form.date_to) { setModalError('Mitarbeiter, Von und Bis sind Pflichtfelder.') return } setModalSaving(true) setModalError(null) try { const payload: SpecialAssignmentCreate = { date_from: form.date_from, date_to: form.date_to, factor: form.factor, mode: form.mode, label: form.label || undefined, } if (editAssignment) { const updated = await api.patch( `/users/${form.user_id}/special-assignments/${editAssignment.id}`, payload ) const user = users.find(u => u.id === form.user_id)! setRows(prev => prev.map(r => r.assignment.id === editAssignment.id ? { assignment: updated, user } : r )) } else { const created = await api.post( `/users/${form.user_id}/special-assignments`, payload ) const user = users.find(u => u.id === form.user_id)! setRows(prev => [...prev, { assignment: created, user }]) } setShowModal(false) } catch (e: unknown) { setModalError(e instanceof Error ? e.message : 'Fehler beim Speichern') } finally { setModalSaving(false) } } async function handleDelete(row: AssignmentRow) { if (!confirm(`Zuweisung für ${row.user.full_name} wirklich löschen?`)) return try { await api.del(`/users/${row.user.id}/special-assignments/${row.assignment.id}`) setRows(prev => prev.filter(r => r.assignment.id !== row.assignment.id)) } catch (e: unknown) { alert(e instanceof Error ? e.message : 'Fehler beim Löschen') } } if (pageLoading) return (
) if (pageError) return (

{pageError}

) return (
{/* Header */}

Sondervertretungen

Sondervertretungs-Zeiträume verwalten

{/* Filter bar */}
setFilterMonth(e.target.value)} className={inputClass} />
{/* Table */}
{tableLoading ? (
) : (
{['Mitarbeiter', 'Pers.-Nr.', 'Von', 'Bis', 'Faktor', 'Ziel', 'Bezeichnung', ''].map(h => ( ))} {filteredRows.map(row => ( ))} {filteredRows.length === 0 && ( )}
{h}
{row.user.full_name} {row.user.personnel_number || '—'} {new Date(row.assignment.date_from).toLocaleDateString('de-DE')} {new Date(row.assignment.date_to).toLocaleDateString('de-DE')} ×{Number(row.assignment.factor).toFixed(2)} {MODE_LABELS[row.assignment.mode]} {row.assignment.label || '—'}
Keine Sondervertretungs-Zuweisungen gefunden.
)}
{/* Add/Edit Modal */} {showModal && ( setShowModal(false)} >
{modalError && (
{modalError}
)}
)}
) }