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:
2026-05-25 22:22:08 +02:00
parent a63b0e835f
commit 549783a05e
2 changed files with 105 additions and 3 deletions
@@ -11,6 +11,30 @@ interface UserOut {
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> = {
SUPER_ADMIN: 'Super Admin',
COMPANY_ADMIN: 'Administrator',
@@ -21,15 +45,24 @@ const ROLE_LABELS: Record<string, string> = {
export function MobileProfileScreen() {
const { logout } = useAuth()
const [user, setUser] = useState<UserOut | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [user, setUser] = useState<UserOut | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [overtime, setOvertime] = useState<OvertimeBalance | null>(null)
const [payouts, setPayouts] = useState<HoursPayoutOut[]>([])
const load = useCallback(async () => {
setError(null)
try {
const me = await api.get<UserOut>('/auth/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) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
@@ -96,6 +129,54 @@ export function MobileProfileScreen() {
)}
</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 */}
<a
href='/dashboard'