diff --git a/DEVLOG.md b/DEVLOG.md index 076e09c..6cefdaa 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1558,3 +1558,21 @@ Keine Commits in dieser Session. - backend/app/services/report_service.py | 19 ++++++++++++++++--- --- +## 2026-05-25 22:48 – 22:51 (3m) +**Beschreibung:** Claude Code Session +**Projekt:** timemaster + +### Commits +- 23ba7f1 feat: Überstunden-Kappung + Jahresverfall pro Firma konfigurierbar + +### GeΓ€nderte Dateien +- DEVLOG.md | 67 +++++++++ +- backend/app/models/company.py | 7 + +- backend/app/models/overtime_balance.py | 1 + +- backend/app/routers/absences.py | 32 ++++ +- backend/app/schemas/company.py | 10 ++ +- backend/app/services/report_service.py | 54 +++++++ +- .../versions/0031_overtime_cap_expiry.py | 33 +++++ +- frontend/src/pages/CompanySettingsPage.tsx | 161 ++++++++++++++++++++- + +--- diff --git a/frontend/src/pages/mobile/MobileTodayScreen.tsx b/frontend/src/pages/mobile/MobileTodayScreen.tsx index 4d743af..e017809 100644 --- a/frontend/src/pages/mobile/MobileTodayScreen.tsx +++ b/frontend/src/pages/mobile/MobileTodayScreen.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback } from 'react' +import { useEffect, useState, useCallback, useMemo } from 'react' import { api } from '../../api/client' interface TimeEntryOut { @@ -17,6 +17,8 @@ interface TimeEntryListResponse { items: TimeEntryOut[] } +type View = 'today' | 'month' + function fmtTime(iso: string | null): string { if (!iso) return '–' if (/^\d{2}:\d{2}(:\d{2})?$/.test(iso)) return iso.slice(0, 5) @@ -49,11 +51,17 @@ const STATUS_COLORS: Record = { } export function MobileTodayScreen() { - const [entries, setEntries] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) + const [view, setView] = useState('today') + const [entries, setEntries] = useState([]) + const [monthEntries, setMonthEntries] = useState([]) + const [loading, setLoading] = useState(true) + const [loadingMonth, setLoadingMonth] = useState(false) + const [error, setError] = useState(null) const today = new Date().toISOString().slice(0, 10) + const now = new Date() + const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01` + const monthEnd = today const load = useCallback(async () => { setError(null) @@ -67,10 +75,40 @@ export function MobileTodayScreen() { } }, [today]) + const loadMonth = useCallback(async () => { + if (monthEntries.length > 0) return // schon geladen + setLoadingMonth(true) + try { + const res = await api.get( + `/time/entries?date_from=${monthStart}&date_to=${monthEnd}&limit=500` + ) + setMonthEntries(res.items) + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoadingMonth(false) + } + }, [monthStart, monthEnd, monthEntries.length]) + useEffect(() => { load() }, [load]) + useEffect(() => { if (view === 'month') loadMonth() }, [view, loadMonth]) const totalWorked = entries.reduce((sum, e) => sum + (e.worked_hours ?? 0), 0) + // Monat: Stunden pro Tag gruppieren + const monthByDate = useMemo(() => { + const map = new Map() + for (const e of monthEntries) { + if (!map.has(e.date)) map.set(e.date, []) + map.get(e.date)!.push(e) + } + return new Map([...map.entries()].sort((a, b) => b[0].localeCompare(a[0]))) + }, [monthEntries]) + + const monthTotalHours = monthEntries.reduce((sum, e) => sum + (e.worked_hours ?? 0), 0) + const monthDaysWorked = monthByDate.size + const monthName = now.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }) + if (loading) { return (
@@ -82,20 +120,25 @@ export function MobileTodayScreen() { return (
- {/* Header-Karte mit Datum + Gesamtzeit */} -
-
-

Heute

-

- {new Date(today + 'T00:00:00').toLocaleDateString('de-DE', { - weekday: 'long', day: '2-digit', month: 'long', - })} -

-
-
-

Gesamt

-

{fmtH(totalWorked)}

-
+ + {/* Heute / Monat Toggle */} +
+ +
{error && ( @@ -104,42 +147,124 @@ export function MobileTodayScreen() {
)} - {/* Eintrags-Liste */} - {entries.length === 0 ? ( -
-

πŸ“‹

-

Noch keine EintrΓ€ge heute

-

Stempel dich ein, um loszulegen

-
- ) : ( -
- {entries.map((entry, idx) => ( -
-
-
-

Eintrag {idx + 1}

-
- {fmtTime(entry.start_time)} - β†’ - {fmtTime(entry.end_time)} + {/* ── HEUTE-ANSICHT ─────────────────────────────────────────────── */} + {view === 'today' && ( + <> +
+
+

Heute

+

+ {new Date(today + 'T00:00:00').toLocaleDateString('de-DE', { + weekday: 'long', day: '2-digit', month: 'long', + })} +

+
+
+

Gesamt

+

{fmtH(totalWorked)}

+
+
+ + {entries.length === 0 ? ( +
+

πŸ“‹

+

Noch keine EintrΓ€ge heute

+

Stempel dich ein, um loszulegen

+
+ ) : ( +
+ {entries.map((entry, idx) => ( +
+
+
+

Eintrag {idx + 1}

+
+ {fmtTime(entry.start_time)} + β†’ + {fmtTime(entry.end_time)} +
+ {entry.break_minutes > 0 && ( +

Pause: {entry.break_minutes} min

+ )} + {entry.note && ( +

{entry.note}

+ )} +
+
+

{fmtH(entry.worked_hours)}

+ + {STATUS_LABELS[entry.status] ?? entry.status} + +
- {entry.break_minutes > 0 && ( -

Pause: {entry.break_minutes} min

- )} - {entry.note && ( -

{entry.note}

- )} -
-
-

{fmtH(entry.worked_hours)}

- - {STATUS_LABELS[entry.status] ?? entry.status} -
+ ))} +
+ )} + + )} + + {/* ── MONATS-ANSICHT ────────────────────────────────────────────── */} + {view === 'month' && ( + <> + {/* Monats-KPIs */} +
+

{monthName}

+
+
+

{fmtH(monthTotalHours)}

+

Gesamt gearbeitet

+
+
+

{monthDaysWorked}

+

Arbeitstage

- ))} -
+ {monthDaysWorked > 0 && ( +

+ Ø {fmtH(monthTotalHours / monthDaysWorked)} pro Tag +

+ )} +
+ + {loadingMonth ? ( +
+
+
+ ) : monthByDate.size === 0 ? ( +
+

πŸ“…

+

Keine EintrΓ€ge diesen Monat

+
+ ) : ( +
+ {[...monthByDate.entries()].map(([date, dayEntries]) => { + const dayTotal = dayEntries.reduce((s, e) => s + (e.worked_hours ?? 0), 0) + const d = new Date(date + 'T00:00:00') + return ( +
+
+

+ {d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' })} +

+

+ {dayEntries.length === 1 + ? `${fmtTime(dayEntries[0].start_time)} – ${fmtTime(dayEntries[0].end_time)}` + : `${dayEntries.length} EintrΓ€ge`} +

+
+
+

{fmtH(dayTotal)}

+ + {STATUS_LABELS[dayEntries[0].status] ?? dayEntries[0].status} + +
+
+ ) + })} +
+ )} + )}
)