import { useEffect, useState } from 'react' import type { HoursPayoutOut, HoursPayoutListResponse } from '../types/hoursPayout' 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 is_active: boolean } interface UserListResponse { total: number items: UserItem[] } interface Me { first_name: string last_name: string role: string } interface OvertimeBalance { balance_hours: number } const MONTH_NAMES = [ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember', ] const inputClass = 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent' function formatPeriod(year: number | null, month: number | null): string { if (!year) return '—' if (!month) return String(year) return `${MONTH_NAMES[month - 1]} ${year}` } const STATUS_BADGE: Record = { requested: { label: 'Beantragt', cls: 'bg-amber-100 text-amber-700' }, approved: { label: 'Genehmigt', cls: 'bg-green-100 text-green-700' }, rejected: { label: 'Abgelehnt', cls: 'bg-red-100 text-red-700' }, cancelled: { label: 'Zurückgezogen', cls: 'bg-gray-100 text-gray-500' }, } function StatusBadge({ status }: { status: string }) { const b = STATUS_BADGE[status] ?? { label: status, cls: 'bg-gray-100 text-gray-500' } return {b.label} } export function HoursPayoutPage() { const [me, setMe] = useState(null) const [users, setUsers] = useState([]) const [pageLoading, setPageLoading] = useState(true) const [pageError, setPageError] = useState(null) // Filter state const currentYear = new Date().getFullYear() const [filterUser, setFilterUser] = useState('') const [filterYear, setFilterYear] = useState(currentYear) const [filterMonth, setFilterMonth] = useState(0) // Table data const [payouts, setPayouts] = useState([]) const [totalCount, setTotalCount] = useState(0) const [tableLoading, setTableLoading] = useState(false) // Modal state const [showModal, setShowModal] = useState(false) const [formUserId, setFormUserId] = useState('') const [formHours, setFormHours] = useState(8) const [formYear, setFormYear] = useState(currentYear) const [formMonth, setFormMonth] = useState(new Date().getMonth() + 1) const [formNote, setFormNote] = useState('') const [formHasPeriod, setFormHasPeriod] = useState(true) const [modalSaving, setModalSaving] = useState(false) const [modalError, setModalError] = useState(null) // Overtime balance for selected user const [overtimeBalance, setOvertimeBalance] = useState(null) const [overtimeLoading, setOvertimeLoading] = useState(false) // 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.filter(u => u.is_active)) } catch (e: unknown) { setPageError(e instanceof Error ? e.message : 'Fehler beim Laden') } finally { setPageLoading(false) } } init() }, []) // Load on mount after users available useEffect(() => { if (!pageLoading) { loadPayouts() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [pageLoading]) async function loadPayouts() { setTableLoading(true) try { const params: Record = {} if (filterUser) params.user_id = filterUser if (filterYear) params.year = String(filterYear) if (filterMonth > 0) params.month = String(filterMonth) const data = await api.get( `/hr/payouts?${new URLSearchParams(params)}` ) setPayouts(data.payouts) setTotalCount(data.total_count) } catch (e: unknown) { setPageError(e instanceof Error ? e.message : 'Fehler beim Laden der Auszahlungen') } finally { setTableLoading(false) } } function handleSearch() { loadPayouts() } async function loadOvertimeBalance(userId: string) { if (!userId) { setOvertimeBalance(null) return } setOvertimeLoading(true) try { const data = await api.get(`/absences/overtime-balance?user_id=${userId}`) setOvertimeBalance(data.balance_hours) } catch { setOvertimeBalance(null) } finally { setOvertimeLoading(false) } } function openNewModal() { setFormUserId('') setFormHours(8) setFormYear(currentYear) setFormMonth(new Date().getMonth() + 1) setFormNote('') setFormHasPeriod(true) setOvertimeBalance(null) setModalError(null) setShowModal(true) } function handleUserChange(userId: string) { setFormUserId(userId) loadOvertimeBalance(userId) } async function handleSave() { if (!formUserId) { setModalError('Bitte einen Mitarbeiter auswählen.') return } if (!formHours || formHours <= 0) { setModalError('Stunden müssen größer als 0 sein.') return } setModalSaving(true) setModalError(null) try { await api.post('/hr/payouts', { user_id: formUserId, hours: formHours, period_year: formHasPeriod ? formYear : null, period_month: formHasPeriod ? formMonth : null, note: formNote.trim() || null, }) setShowModal(false) loadPayouts() } catch (e: unknown) { setModalError(e instanceof Error ? e.message : 'Fehler beim Anlegen') } finally { setModalSaving(false) } } async function handleDelete(payout: HoursPayoutOut) { if (!confirm('Auszahlung wirklich stornieren?')) return try { await api.del(`/hr/payouts/${payout.id}`) setPayouts(prev => prev.filter(p => p.id !== payout.id)) setTotalCount(prev => prev - 1) } catch (e: unknown) { alert(e instanceof Error ? e.message : 'Fehler beim Stornieren') } } async function handleApprove(payout: HoursPayoutOut) { if (!confirm(`Auszahlung über ${Number(payout.hours).toFixed(2)} h für ${payout.user_name} genehmigen? Der Saldo wird abgezogen.`)) return try { const updated = await api.post(`/hr/payouts/${payout.id}/approve`, {}) setPayouts(prev => prev.map(p => (p.id === payout.id ? updated : p))) } catch (e: unknown) { alert(e instanceof Error ? e.message : 'Fehler beim Genehmigen') } } async function handleReject(payout: HoursPayoutOut) { const reason = prompt('Grund der Ablehnung (optional):') if (reason === null) return // Abbrechen try { const updated = await api.post(`/hr/payouts/${payout.id}/reject`, { rejection_reason: reason.trim() || null, }) setPayouts(prev => prev.map(p => (p.id === payout.id ? updated : p))) } catch (e: unknown) { alert(e instanceof Error ? e.message : 'Fehler beim Ablehnen') } } if (pageLoading) return (
) if (pageError) return (

{pageError}

) const isOverBudget = overtimeBalance !== null && formHours > overtimeBalance return (
{/* Header */}

💸 Stunden-Auszahlung

Überstunden-Auszahlungen verwalten

{/* Filter bar */}
setFilterYear(Number(e.target.value))} min={2000} max={2100} className={inputClass} />
{/* Table */}
{tableLoading ? (
) : ( <>
{['Mitarbeiter', 'Stunden', 'Status', 'Abrechnungsmonat', 'Notiz', 'Angelegt von', 'Datum', 'Aktion'].map(h => ( ))} {payouts.map(payout => ( ))} {payouts.length === 0 && ( )}
{h}
{payout.user_name} {Number(payout.hours).toFixed(2)} h {payout.status === 'rejected' && payout.rejection_reason && (
{payout.rejection_reason}
)}
{formatPeriod(payout.period_year, payout.period_month)} {payout.note || '—'} {payout.created_by_name} {new Date(payout.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', })} {payout.status === 'requested' ? (
) : payout.status === 'approved' ? ( ) : ( )}
Keine Auszahlungen gefunden.
{totalCount > 0 && (
{totalCount} Einträge gesamt
)} )}
{/* New Payout Modal */} {showModal && ( setShowModal(false)} >
{/* Mitarbeiter */} {/* Overtime balance indicator */} {formUserId && (
{overtimeLoading ? ( Lade Überstundenguthaben… ) : overtimeBalance !== null ? ( Verfügbares Überstundenguthaben: {Number(overtimeBalance).toFixed(2)} h {isOverBudget && ' ⚠️ Auszahlung überschreitet verfügbares Guthaben'} ) : ( Guthaben nicht verfügbar )}
)} {/* Stunden */} {/* Abrechnungsmonat */}
Abrechnungsmonat
{formHasPeriod && (
setFormYear(Number(e.target.value))} className={inputClass} />
)}
{/* Notiz */}