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:
@@ -0,0 +1,186 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user