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:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user