feat: Abwesenheiten-Screen in Mobile-App
- MobileAbsencesScreen.tsx: - Urlaubskonto-Karte (verbleibende Tage + Fortschrittsbalken) - Liste eigener Abwesenheiten (aktuell/geplant + vergangen) - Farbpunkt pro Abwesenheitstyp, Status-Badge - Bottom-Sheet Modal: Antrag stellen oder Krank melden - Start-/Enddatum-Picker, Typ-Auswahl, optionale Notiz - SICK-Typ → quick-sick Endpoint, sonst POST /absences/ - MobileBottomNav: 4. Tab 'Urlaub' (war 3 Tabs) - MobilePage: Screen 'absences' eingebunden Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1094,3 +1094,43 @@ Keine Commits in dieser Session.
|
|||||||
- frontend/src/api/client.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++
|
- frontend/src/api/client.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-05-24 21:15 – 21:17 (2m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** timemaster
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- edb1568 feat: mobile Login-Seite /mobile/login
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 77 ++++++
|
||||||
|
- frontend/src/App.tsx | 4 +
|
||||||
|
- frontend/src/pages/mobile/MobileBottomNav.tsx | 69 +++++
|
||||||
|
- frontend/src/pages/mobile/MobileLoginPage.tsx | 195 ++++++++++++++
|
||||||
|
- frontend/src/pages/mobile/MobilePage.tsx | 75 ++++++
|
||||||
|
- frontend/src/pages/mobile/MobileProfileScreen.tsx | 124 +++++++++
|
||||||
|
- frontend/src/pages/mobile/MobileStampScreen.tsx | 310 ++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/mobile/MobileTodayScreen.tsx | 146 ++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-05-24 23:21 – 23:22 (1m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** timemaster
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- 4a1dec7 fix: mobile/tablet Geräteerkennung in LoginPage → Redirect zu /mobile/login
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- frontend/src/pages/LoginPage.tsx | 11 +++++++++--
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-05-24 23:23 – 23:23 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** timemaster
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- frontend/src/pages/LoginPage.tsx | 11 +++++++++--
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -0,0 +1,370 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { api } from '../../api/client'
|
||||||
|
|
||||||
|
// ── Typen ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface AbsenceType {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
category: string
|
||||||
|
color: string | null
|
||||||
|
requires_approval: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Absence {
|
||||||
|
id: string
|
||||||
|
type_id: string
|
||||||
|
start_date: string
|
||||||
|
end_date: string
|
||||||
|
half_day_start: boolean
|
||||||
|
half_day_end: boolean
|
||||||
|
working_days: number
|
||||||
|
status: 'pending' | 'approved' | 'rejected' | 'cancelled'
|
||||||
|
note: string | null
|
||||||
|
rejection_reason: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AbsenceList {
|
||||||
|
total: number
|
||||||
|
items: Absence[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VacationBalance {
|
||||||
|
entitled_days: number
|
||||||
|
special_days: number
|
||||||
|
carried_over: number
|
||||||
|
used_days: number
|
||||||
|
total_days: number
|
||||||
|
remaining_days: number
|
||||||
|
pending_days: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function fmtDate(d: string) {
|
||||||
|
return new Date(d + 'T00:00:00').toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(s: Absence['status']) {
|
||||||
|
return { pending: 'Ausstehend', approved: 'Genehmigt', rejected: 'Abgelehnt', cancelled: 'Storniert' }[s]
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusColor(s: Absence['status']) {
|
||||||
|
return {
|
||||||
|
pending: 'bg-yellow-100 text-yellow-700',
|
||||||
|
approved: 'bg-green-100 text-green-700',
|
||||||
|
rejected: 'bg-red-100 text-red-700',
|
||||||
|
cancelled: 'bg-gray-100 text-gray-500',
|
||||||
|
}[s]
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
const todayStr = today.toISOString().slice(0, 10)
|
||||||
|
|
||||||
|
// ── Neuer Antrag Modal ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface NewAbsenceModalProps {
|
||||||
|
types: AbsenceType[]
|
||||||
|
onClose: () => void
|
||||||
|
onCreated: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function NewAbsenceModal({ types, onClose, onCreated }: NewAbsenceModalProps) {
|
||||||
|
const [typeId, setTypeId] = useState(types[0]?.id ?? '')
|
||||||
|
const [startDate, setStartDate] = useState(todayStr)
|
||||||
|
const [endDate, setEndDate] = useState(todayStr)
|
||||||
|
const [note, setNote] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Kategorie SICK → quick-sick, sonst normaler Antrag
|
||||||
|
const selectedType = types.find(t => t.id === typeId)
|
||||||
|
const isSick = selectedType?.category === 'SICK'
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true); setError(null)
|
||||||
|
try {
|
||||||
|
if (isSick) {
|
||||||
|
await api.post('/absences/quick-sick', { start_date: startDate, end_date: endDate })
|
||||||
|
} else {
|
||||||
|
await api.post('/absences/', { type_id: typeId, start_date: startDate, end_date: endDate, note: note || null })
|
||||||
|
}
|
||||||
|
onCreated()
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Fehler beim Erstellen')
|
||||||
|
} finally {
|
||||||
|
setLoading(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='fixed inset-0 z-50 flex flex-col justify-end bg-black/40' onClick={onClose}>
|
||||||
|
<div
|
||||||
|
className='bg-white rounded-t-2xl p-5 flex flex-col gap-4'
|
||||||
|
style={{ paddingBottom: 'calc(1.25rem + env(safe-area-inset-bottom))' }}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<h2 className='text-lg font-bold text-gray-900'>Neuer Antrag</h2>
|
||||||
|
<button onClick={onClose} className='text-gray-400 text-2xl leading-none'>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className='bg-red-50 border border-red-200 rounded-xl px-4 py-3 text-sm text-red-700'>{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className='flex flex-col gap-3'>
|
||||||
|
{/* Typ */}
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<label className='text-sm font-medium text-gray-700'>Art</label>
|
||||||
|
<select
|
||||||
|
value={typeId}
|
||||||
|
onChange={e => setTypeId(e.target.value)}
|
||||||
|
className='min-h-[48px] px-3 rounded-xl border border-gray-300 text-gray-900 text-base focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white'
|
||||||
|
>
|
||||||
|
{types.map(t => (
|
||||||
|
<option key={t.id} value={t.id}>{t.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Datum */}
|
||||||
|
<div className='grid grid-cols-2 gap-3'>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<label className='text-sm font-medium text-gray-700'>Von</label>
|
||||||
|
<input
|
||||||
|
type='date'
|
||||||
|
value={startDate}
|
||||||
|
onChange={e => { setStartDate(e.target.value); if (e.target.value > endDate) setEndDate(e.target.value) }}
|
||||||
|
required
|
||||||
|
className='min-h-[48px] px-3 rounded-xl border border-gray-300 text-gray-900 text-base focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<label className='text-sm font-medium text-gray-700'>Bis</label>
|
||||||
|
<input
|
||||||
|
type='date'
|
||||||
|
value={endDate}
|
||||||
|
min={startDate}
|
||||||
|
onChange={e => setEndDate(e.target.value)}
|
||||||
|
required
|
||||||
|
className='min-h-[48px] px-3 rounded-xl border border-gray-300 text-gray-900 text-base focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notiz (nicht bei Krankmeldung) */}
|
||||||
|
{!isSick && (
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<label className='text-sm font-medium text-gray-700'>Notiz <span className='text-gray-400 font-normal'>(optional)</span></label>
|
||||||
|
<textarea
|
||||||
|
value={note}
|
||||||
|
onChange={e => setNote(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder='z.B. Hochzeit, Arzttermin …'
|
||||||
|
className='px-3 py-2 rounded-xl border border-gray-300 text-gray-900 text-base focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type='submit'
|
||||||
|
disabled={loading}
|
||||||
|
className='min-h-[52px] rounded-xl bg-blue-600 active:bg-blue-800 text-white font-semibold text-base transition-colors disabled:opacity-50 flex items-center justify-center gap-2 mt-1'
|
||||||
|
>
|
||||||
|
{loading
|
||||||
|
? <span className='animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent' />
|
||||||
|
: isSick ? '🤒 Krank melden' : '✓ Antrag stellen'
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Haupt-Screen ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function MobileAbsencesScreen() {
|
||||||
|
const [balance, setBalance] = useState<VacationBalance | null>(null)
|
||||||
|
const [absences, setAbsences] = useState<Absence[]>([])
|
||||||
|
const [types, setTypes] = useState<AbsenceType[]>([])
|
||||||
|
const [typeMap, setTypeMap] = useState<Record<string, AbsenceType>>({})
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [showModal, setShowModal] = useState(false)
|
||||||
|
|
||||||
|
const year = today.getFullYear()
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const [bal, list, typeList] = await Promise.all([
|
||||||
|
api.get<VacationBalance>(`/absences/balance?year=${year}`),
|
||||||
|
api.get<AbsenceList>(`/absences/?year=${year}&limit=50`),
|
||||||
|
api.get<AbsenceType[]>('/absence-types/'),
|
||||||
|
])
|
||||||
|
setBalance(bal)
|
||||||
|
setAbsences(list.items)
|
||||||
|
const active = typeList.filter(t => (t as AbsenceType & { is_active?: boolean }).is_active !== false)
|
||||||
|
setTypes(active)
|
||||||
|
setTypeMap(Object.fromEntries(active.map(t => [t.id, t])))
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [year])
|
||||||
|
|
||||||
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
|
if (loading) return (
|
||||||
|
<div className='flex flex-col items-center justify-center flex-1 py-20 gap-4'>
|
||||||
|
<div className='animate-spin rounded-full h-10 w-10 border-4 border-blue-500 border-t-transparent' />
|
||||||
|
<p className='text-sm text-gray-400'>Wird geladen…</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Abwesenheiten sortiert: zukünftige + laufende zuerst, dann vergangene
|
||||||
|
const sorted = [...absences].sort((a, b) => b.start_date.localeCompare(a.start_date))
|
||||||
|
const upcoming = sorted.filter(a => a.end_date >= todayStr && a.status !== 'cancelled' && a.status !== 'rejected')
|
||||||
|
const past = sorted.filter(a => a.end_date < todayStr || a.status === 'cancelled' || a.status === 'rejected')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4 px-4 pt-4'>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className='bg-red-50 border border-red-200 rounded-xl px-4 py-3 text-sm text-red-700'>{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Urlaubskonto */}
|
||||||
|
{balance && (
|
||||||
|
<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-widest mb-3'>Urlaubskonto {year}</p>
|
||||||
|
<div className='flex items-center justify-between mb-3'>
|
||||||
|
<div>
|
||||||
|
<p className='text-4xl font-bold text-blue-600'>{balance.remaining_days}</p>
|
||||||
|
<p className='text-sm text-gray-500 mt-0.5'>Tage verbleibend</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col gap-1.5 text-right text-sm'>
|
||||||
|
<div className='flex gap-2 justify-end'>
|
||||||
|
<span className='text-gray-400'>Anspruch</span>
|
||||||
|
<span className='font-semibold text-gray-700'>{balance.total_days} Tage</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-2 justify-end'>
|
||||||
|
<span className='text-gray-400'>Genommen</span>
|
||||||
|
<span className='font-semibold text-gray-700'>{balance.used_days} Tage</span>
|
||||||
|
</div>
|
||||||
|
{balance.pending_days > 0 && (
|
||||||
|
<div className='flex gap-2 justify-end'>
|
||||||
|
<span className='text-gray-400'>Ausstehend</span>
|
||||||
|
<span className='font-semibold text-yellow-600'>{balance.pending_days} Tage</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{balance.carried_over > 0 && (
|
||||||
|
<div className='flex gap-2 justify-end'>
|
||||||
|
<span className='text-gray-400'>Übertrag</span>
|
||||||
|
<span className='font-semibold text-gray-700'>{balance.carried_over} Tage</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fortschrittsbalken */}
|
||||||
|
<div className='h-2 bg-gray-100 rounded-full overflow-hidden'>
|
||||||
|
<div
|
||||||
|
className='h-full bg-blue-500 rounded-full transition-all'
|
||||||
|
style={{ width: `${Math.min(100, (balance.used_days / Math.max(balance.total_days, 1)) * 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Neuer Antrag Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
className='flex items-center justify-center gap-2 min-h-[52px] rounded-xl bg-blue-600 active:bg-blue-800 text-white font-semibold text-base shadow-sm transition-colors'
|
||||||
|
>
|
||||||
|
<span className='text-lg'>+</span>
|
||||||
|
Antrag stellen / Krank melden
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Kommende Abwesenheiten */}
|
||||||
|
{upcoming.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<p className='text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2 px-1'>Aktuell & Geplant</p>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
{upcoming.map(a => (
|
||||||
|
<AbsenceCard key={a.id} absence={a} type={typeMap[a.type_id]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Vergangene Abwesenheiten */}
|
||||||
|
{past.length > 0 && (
|
||||||
|
<section className='mb-4'>
|
||||||
|
<p className='text-xs font-semibold text-gray-400 uppercase tracking-widest mb-2 px-1'>Vergangen</p>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
{past.map(a => (
|
||||||
|
<AbsenceCard key={a.id} absence={a} type={typeMap[a.type_id]} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{upcoming.length === 0 && past.length === 0 && !loading && (
|
||||||
|
<div className='flex flex-col items-center py-12 gap-2 text-gray-400'>
|
||||||
|
<span className='text-4xl'>🌴</span>
|
||||||
|
<p className='text-sm'>Keine Abwesenheiten in {year}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{showModal && types.length > 0 && (
|
||||||
|
<NewAbsenceModal
|
||||||
|
types={types}
|
||||||
|
onClose={() => setShowModal(false)}
|
||||||
|
onCreated={() => { setShowModal(false); load() }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Karte pro Abwesenheit ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function AbsenceCard({ absence, type }: { absence: Absence; type?: AbsenceType }) {
|
||||||
|
const isSameDay = absence.start_date === absence.end_date
|
||||||
|
const dateStr = isSameDay
|
||||||
|
? fmtDate(absence.start_date)
|
||||||
|
: `${fmtDate(absence.start_date)} – ${fmtDate(absence.end_date)}`
|
||||||
|
|
||||||
|
const colorDot = type?.color ?? '#94a3b8'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='bg-white rounded-xl border border-gray-200 shadow-sm px-4 py-3 flex items-start gap-3'>
|
||||||
|
{/* Farbpunkt */}
|
||||||
|
<div className='mt-1 w-3 h-3 rounded-full flex-shrink-0' style={{ backgroundColor: colorDot }} />
|
||||||
|
|
||||||
|
<div className='flex-1 min-w-0'>
|
||||||
|
<div className='flex items-center justify-between gap-2 flex-wrap'>
|
||||||
|
<p className='font-semibold text-gray-800 text-sm truncate'>{type?.name ?? '–'}</p>
|
||||||
|
<span className={`text-[11px] font-semibold px-2 py-0.5 rounded-full ${statusColor(absence.status)}`}>
|
||||||
|
{statusLabel(absence.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className='text-xs text-gray-500 mt-0.5'>{dateStr}</p>
|
||||||
|
{absence.working_days > 0 && (
|
||||||
|
<p className='text-xs text-gray-400 mt-0.5'>{absence.working_days} Arbeitstag{absence.working_days !== 1 ? 'e' : ''}</p>
|
||||||
|
)}
|
||||||
|
{absence.rejection_reason && (
|
||||||
|
<p className='text-xs text-red-500 mt-1'>Grund: {absence.rejection_reason}</p>
|
||||||
|
)}
|
||||||
|
{absence.note && (
|
||||||
|
<p className='text-xs text-gray-400 mt-1 italic'>„{absence.note}"</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
type Screen = 'stamp' | 'today' | 'profile'
|
type Screen = 'stamp' | 'today' | 'absences' | 'profile'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
active: Screen
|
active: Screen
|
||||||
@@ -29,6 +29,16 @@ export function MobileBottomNav({ active, onChange }: Props) {
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'absences',
|
||||||
|
label: 'Urlaub',
|
||||||
|
icon: (
|
||||||
|
<svg className='w-6 h-6' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={1.75}>
|
||||||
|
<path strokeLinecap='round' strokeLinejoin='round' d='M3 17l4-8 4 4 4-6 4 10' />
|
||||||
|
<path strokeLinecap='round' strokeLinejoin='round' d='M3 20h18' />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'profile',
|
id: 'profile',
|
||||||
label: 'Profil',
|
label: 'Profil',
|
||||||
@@ -52,9 +62,7 @@ export function MobileBottomNav({ active, onChange }: Props) {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => onChange(item.id)}
|
onClick={() => onChange(item.id)}
|
||||||
className={`flex-1 flex flex-col items-center justify-center gap-1 min-h-[56px] py-2 transition-colors ${
|
className={`flex-1 flex flex-col items-center justify-center gap-1 min-h-[56px] py-2 transition-colors ${
|
||||||
active === item.id
|
active === item.id ? 'text-blue-600' : 'text-gray-400'
|
||||||
? 'text-blue-600'
|
|
||||||
: 'text-gray-400'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import { MobileBottomNav } from './MobileBottomNav'
|
|||||||
import { MobileStampScreen } from './MobileStampScreen'
|
import { MobileStampScreen } from './MobileStampScreen'
|
||||||
import { MobileTodayScreen } from './MobileTodayScreen'
|
import { MobileTodayScreen } from './MobileTodayScreen'
|
||||||
import { MobileProfileScreen } from './MobileProfileScreen'
|
import { MobileProfileScreen } from './MobileProfileScreen'
|
||||||
|
import { MobileAbsencesScreen } from './MobileAbsencesScreen'
|
||||||
|
|
||||||
type Screen = 'stamp' | 'today' | 'profile'
|
type Screen = 'stamp' | 'today' | 'absences' | 'profile'
|
||||||
|
|
||||||
const SCREEN_TITLES: Record<Screen, string> = {
|
const SCREEN_TITLES: Record<Screen, string> = {
|
||||||
stamp: 'Zeiterfassung',
|
stamp: 'Zeiterfassung',
|
||||||
today: 'Heute',
|
today: 'Heute',
|
||||||
|
absences: 'Abwesenheiten',
|
||||||
profile: 'Profil',
|
profile: 'Profil',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ export function MobilePage() {
|
|||||||
>
|
>
|
||||||
{screen === 'stamp' && <MobileStampScreen />}
|
{screen === 'stamp' && <MobileStampScreen />}
|
||||||
{screen === 'today' && <MobileTodayScreen />}
|
{screen === 'today' && <MobileTodayScreen />}
|
||||||
|
{screen === 'absences' && <MobileAbsencesScreen />}
|
||||||
{screen === 'profile' && <MobileProfileScreen />}
|
{screen === 'profile' && <MobileProfileScreen />}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user