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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { api } from '../api/client'
|
||||
import type { UserOut, UserListItem, VacationBalanceOut } from '../types/absence'
|
||||
import { MANAGER_ROLES } from '../utils/calendar'
|
||||
|
||||
export function usePlanerView(user: UserOut | null, colleagues: UserListItem[], year: number) {
|
||||
const [viewMode, setViewMode] = useState<'liste' | 'planer'>('liste')
|
||||
const [planMonth, setPlanMonth] = useState(new Date().getMonth())
|
||||
const [colleagueBalances, setColleagueBalances] = useState<Record<string, VacationBalanceOut>>({})
|
||||
const [balancesLoaded, setBalancesLoaded] = useState(false)
|
||||
const [showYearGrid, setShowYearGrid] = useState<boolean>(() => {
|
||||
try { return localStorage.getItem('planer_showYearGrid') !== 'false' }
|
||||
catch { return true }
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
try { localStorage.setItem('planer_showYearGrid', String(showYearGrid)) }
|
||||
catch { /* ignore */ }
|
||||
}, [showYearGrid])
|
||||
|
||||
const loadColleagueBalances = useCallback(async (list: UserListItem[]) => {
|
||||
if (list.length === 0 || balancesLoaded) return
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
list.map(c => api.get<VacationBalanceOut>(`/absences/balance/${c.id}?year=${year}`))
|
||||
)
|
||||
const map: Record<string, VacationBalanceOut> = {}
|
||||
results.forEach((r, i) => { if (r.status === 'fulfilled') map[list[i].id] = r.value })
|
||||
setColleagueBalances(map)
|
||||
setBalancesLoaded(true)
|
||||
} catch { /* ignore */ }
|
||||
}, [year, balancesLoaded])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'planer' && user && MANAGER_ROLES.includes(user.role) && colleagues.length > 0 && !balancesLoaded) {
|
||||
loadColleagueBalances(colleagues)
|
||||
}
|
||||
}, [viewMode, user, colleagues, balancesLoaded, loadColleagueBalances])
|
||||
|
||||
return {
|
||||
viewMode,
|
||||
setViewMode,
|
||||
planMonth,
|
||||
setPlanMonth,
|
||||
colleagueBalances,
|
||||
balancesLoaded,
|
||||
showYearGrid,
|
||||
setShowYearGrid,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user