Files
timemaster/frontend/src/hooks/useAbsences.ts
T
sysops 1fedd683e0 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>
2026-05-23 20:03:27 +02:00

187 lines
5.9 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react'
import { api } from '../api/client'
import type {
UserOut,
AbsenceTypeOut,
AbsenceOut,
AbsenceListResponse,
UserListItem,
VacationBalanceOut,
OvertimeBalanceOut,
} from '../types/absence'
import { MANAGER_ROLES } from '../utils/calendar'
export function useAbsences(year: number, statusFilter: string) {
const [user, setUser] = useState<UserOut | null>(null)
const [types, setTypes] = useState<AbsenceTypeOut[]>([])
const [absences, setAbsences] = useState<AbsenceOut[]>([])
const [total, setTotal] = useState(0)
const [balance, setBalance] = useState<VacationBalanceOut | null>(null)
const [overtimeBalance, setOvertimeBalance] = useState<OvertimeBalanceOut | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [colleagues, setColleagues] = useState<UserListItem[]>([])
const colleagueMap = Object.fromEntries(colleagues.map(c => [c.id, c.full_name]))
const load = useCallback(async () => {
setLoading(true)
try {
const params = statusFilter ? `?status=${statusFilter}&year=${year}` : `?year=${year}`
const [me, typeList, absList, bal, otBal] = await Promise.all([
api.get<UserOut>('/auth/me'),
api.get<AbsenceTypeOut[]>('/absence-types/'),
api.get<AbsenceListResponse>(`/absences/${params}`),
api.get<VacationBalanceOut>(`/absences/balance?year=${year}`),
api.get<OvertimeBalanceOut>('/absences/overtime-balance'),
])
setUser(me)
setTypes(typeList.filter(t => t.is_active))
setAbsences(absList.items)
setTotal(absList.total)
setBalance(bal)
setOvertimeBalance(otBal)
if (MANAGER_ROLES.includes(me.role) && colleagues.length === 0) {
try {
const res = await api.get<{ items: UserListItem[] }>('/users/?limit=500')
setColleagues(res.items)
} catch { /* ignore */ }
}
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [statusFilter, year]) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { load() }, [load])
const createAbsence = async (
form: { type_id: string; start_date: string; end_date: string; half_day_start: boolean; half_day_end: boolean; note: string; for_user_id: string },
onSuccess: () => void,
setSubmitting: (v: boolean) => void,
) => {
if (!form.type_id || !form.start_date || !form.end_date) {
setError('Bitte alle Pflichtfelder ausfüllen')
return
}
setSubmitting(true)
setError('')
try {
await api.post('/absences/', {
type_id: form.type_id,
start_date: form.start_date,
end_date: form.end_date,
half_day_start: form.half_day_start,
half_day_end: form.half_day_end,
note: form.note || null,
for_user_id: form.for_user_id || null,
})
onSuccess()
await load()
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Fehler beim Erstellen')
} finally {
setSubmitting(false)
}
}
const approve = async (id: string) => {
setError('')
try { await api.post(`/absences/${id}/approve`, {}); await load() }
catch (e: unknown) { setError(e instanceof Error ? e.message : 'Fehler') }
}
const reject = async (id: string, rejectReason: string, onSuccess: () => void) => {
if (!rejectReason.trim()) return
setError('')
try {
await api.post(`/absences/${id}/reject`, { rejection_reason: rejectReason })
onSuccess()
await load()
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Fehler')
}
}
const saveEdit = async (
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 },
isManager: boolean,
onSuccess: () => void,
setSubmitting: (v: boolean) => void,
) => {
if (!editForm.type_id || !editForm.start_date || !editForm.end_date) {
setError('Bitte alle Pflichtfelder ausfüllen')
return
}
if (editAbsence.status === 'approved' && !isManager && !editForm.correction_note.trim()) {
setError('Änderungsgrund ist Pflicht bei genehmigten Anträgen.')
return
}
setSubmitting(true)
setError('')
try {
await api.patch(`/absences/${editAbsence.id}`, {
type_id: editForm.type_id,
start_date: editForm.start_date,
end_date: editForm.end_date,
half_day_start: editForm.half_day_start,
half_day_end: editForm.half_day_end,
note: editForm.note || null,
correction_note: editForm.correction_note.trim() || null,
})
onSuccess()
await load()
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Fehler beim Speichern')
} finally {
setSubmitting(false)
}
}
const cancel = async (id: string) => {
setError('')
try { await api.del(`/absences/${id}`); await load() }
catch (e: unknown) { setError(e instanceof Error ? e.message : 'Fehler') }
}
const loadColleaguesIfNeeded = async () => {
if (colleagues.length === 0) {
try {
const res = await api.get<{ items: UserListItem[] }>('/users/?limit=500')
setColleagues(res.items)
} catch { /* ignore */ }
}
}
const typeName = (typeId: string) => types.find(t => t.id === typeId)?.name ?? typeId
const typeColor = (typeId: string) => types.find(t => t.id === typeId)?.color ?? '#6B7280'
const updateBalance = (updated: VacationBalanceOut) => setBalance(updated)
return {
user,
types,
absences,
total,
balance,
overtimeBalance,
loading,
error,
setError,
colleagues,
colleagueMap,
load,
createAbsence,
approve,
reject,
saveEdit,
cancel,
loadColleaguesIfNeeded,
typeName,
typeColor,
updateBalance,
}
}