feat: Stunden-Auszahlungen in /mobile Profil-Screen
- Überstunden-Saldo (Gesamt/Entnommen/Verfügbar) als 3-spaltige Karte - Letzte 5 Auszahlungen mit Stunden (lila), Abrechnungsmonat, Notiz - Parallel-Load mit Overtime-Balance beim Profil-Laden Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1470,3 +1470,24 @@ Keine Commits in dieser Session.
|
|||||||
- frontend/src/pages/CompanySettingsPage.tsx | 88 +++++++++++++++++++++++++++++-
|
- frontend/src/pages/CompanySettingsPage.tsx | 88 +++++++++++++++++++++++++++++-
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-05-25 22:17 – 22:18 (1m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** timemaster
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- a63b0e8 feat: Stunden-Auszahlung Feature (/hr/payouts)
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 136 ++++++
|
||||||
|
- backend/app/main.py | 2 +
|
||||||
|
- backend/app/models/__init__.py | 2 +
|
||||||
|
- backend/app/models/hours_payout.py | 45 ++
|
||||||
|
- backend/app/routers/hours_payouts.py | 189 +++++++++
|
||||||
|
- backend/app/schemas/hours_payout.py | 33 ++
|
||||||
|
- backend/migrations/versions/0030_hours_payouts.py | 36 ++
|
||||||
|
- frontend/src/App.tsx | 2 +
|
||||||
|
- frontend/src/components/Layout.tsx | 1 +
|
||||||
|
- frontend/src/pages/HoursPayoutPage.tsx | 484 ++++++++++++++++++++++
|
||||||
|
- frontend/src/types/hoursPayout.ts | 26 ++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -11,6 +11,30 @@ interface UserOut {
|
|||||||
personnel_number: string | null
|
personnel_number: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OvertimeBalance {
|
||||||
|
total_hours: number
|
||||||
|
taken_hours: number
|
||||||
|
available_hours: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HoursPayoutOut {
|
||||||
|
id: string
|
||||||
|
hours: number
|
||||||
|
period_year: number | null
|
||||||
|
period_month: number | null
|
||||||
|
note: string | null
|
||||||
|
created_by_name: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HoursPayoutListResponse {
|
||||||
|
payouts: HoursPayoutOut[]
|
||||||
|
total_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const MONTH_NAMES = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun',
|
||||||
|
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
|
||||||
|
|
||||||
const ROLE_LABELS: Record<string, string> = {
|
const ROLE_LABELS: Record<string, string> = {
|
||||||
SUPER_ADMIN: 'Super Admin',
|
SUPER_ADMIN: 'Super Admin',
|
||||||
COMPANY_ADMIN: 'Administrator',
|
COMPANY_ADMIN: 'Administrator',
|
||||||
@@ -24,12 +48,21 @@ export function MobileProfileScreen() {
|
|||||||
const [user, setUser] = useState<UserOut | null>(null)
|
const [user, setUser] = useState<UserOut | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [overtime, setOvertime] = useState<OvertimeBalance | null>(null)
|
||||||
|
const [payouts, setPayouts] = useState<HoursPayoutOut[]>([])
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const me = await api.get<UserOut>('/auth/me')
|
const me = await api.get<UserOut>('/auth/me')
|
||||||
setUser(me)
|
setUser(me)
|
||||||
|
// Überstunden-Saldo + eigene Auszahlungen parallel laden
|
||||||
|
const [bal, payoutData] = await Promise.all([
|
||||||
|
api.get<OvertimeBalance>(`/absences/overtime-balance?user_id=${me.id}`).catch(() => null),
|
||||||
|
api.get<HoursPayoutListResponse>(`/hr/payouts?user_id=${me.id}`).catch(() => null),
|
||||||
|
])
|
||||||
|
if (bal) setOvertime(bal)
|
||||||
|
if (payoutData) setPayouts(payoutData.payouts.slice(0, 5)) // letzte 5
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
|
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -96,6 +129,54 @@ export function MobileProfileScreen() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Überstunden-Saldo */}
|
||||||
|
{overtime !== null && (
|
||||||
|
<div className='bg-white rounded-2xl border border-gray-200 shadow-sm px-5 py-4'>
|
||||||
|
<p className='text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3'>Überstunden-Konto</p>
|
||||||
|
<div className='grid grid-cols-3 gap-2 text-center'>
|
||||||
|
<div>
|
||||||
|
<p className='text-lg font-bold text-gray-800'>{overtime.total_hours.toFixed(1)}</p>
|
||||||
|
<p className='text-xs text-gray-400'>Gesamt</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className='text-lg font-bold text-orange-600'>{overtime.taken_hours.toFixed(1)}</p>
|
||||||
|
<p className='text-xs text-gray-400'>Entnommen</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className={`text-lg font-bold ${overtime.available_hours >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{overtime.available_hours.toFixed(1)}
|
||||||
|
</p>
|
||||||
|
<p className='text-xs text-gray-400'>Verfügbar</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stunden-Auszahlungen */}
|
||||||
|
{payouts.length > 0 && (
|
||||||
|
<div className='bg-white rounded-2xl border border-gray-200 shadow-sm px-5 py-4'>
|
||||||
|
<p className='text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3'>
|
||||||
|
💸 Stunden-Auszahlungen
|
||||||
|
</p>
|
||||||
|
<div className='divide-y divide-gray-100'>
|
||||||
|
{payouts.map(p => (
|
||||||
|
<div key={p.id} className='flex items-center justify-between py-3 first:pt-0 last:pb-0'>
|
||||||
|
<div>
|
||||||
|
<p className='text-sm font-semibold text-purple-700'>{p.hours.toFixed(2)} h</p>
|
||||||
|
<p className='text-xs text-gray-400'>
|
||||||
|
{p.period_year && p.period_month
|
||||||
|
? `${MONTH_NAMES[p.period_month - 1]} ${p.period_year}`
|
||||||
|
: new Date(p.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
||||||
|
</p>
|
||||||
|
{p.note && <p className='text-xs text-gray-500 mt-0.5 truncate max-w-[180px]'>{p.note}</p>}
|
||||||
|
</div>
|
||||||
|
<p className='text-xs text-gray-400 text-right'>{p.created_by_name}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Desktop-Version Link */}
|
{/* Desktop-Version Link */}
|
||||||
<a
|
<a
|
||||||
href='/dashboard'
|
href='/dashboard'
|
||||||
|
|||||||
Reference in New Issue
Block a user