Files
FEDEO/mobile/app/(tabs)/index.tsx
florianfederspiel 409db82368
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 2m50s
Build and Push Docker Images / build-frontend (push) Successful in 1m13s
Mobile Dev
2026-02-21 21:21:39 +01:00

270 lines
7.8 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
import { router } from 'expo-router';
import { fetchStaffTimeSpans, fetchTasks, Task } from '@/src/lib/api';
import { useAuth } from '@/src/providers/auth-provider';
const PRIMARY = '#69c350';
type DashboardData = {
tasks: Task[];
openTasks: number;
inProgressTasks: number;
activeTimeStart: string | null;
pendingSubmissions: number;
todayMinutes: number;
};
function normalizeTaskStatus(value: unknown): 'Offen' | 'In Bearbeitung' | 'Abgeschlossen' {
if (value === 'In Bearbeitung') return 'In Bearbeitung';
if (value === 'Abgeschlossen') return 'Abgeschlossen';
return 'Offen';
}
function formatMinutes(minutes: number): string {
const h = Math.floor(minutes / 60);
const m = minutes % 60;
return `${h}h ${String(m).padStart(2, '0')}m`;
}
function formatDateTime(value: string | null): string {
if (!value) return '-';
return new Date(value).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
export default function DashboardScreen() {
const { token, user } = useAuth();
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<DashboardData>({
tasks: [],
openTasks: 0,
inProgressTasks: 0,
activeTimeStart: null,
pendingSubmissions: 0,
todayMinutes: 0,
});
const currentUserId = useMemo(() => (user?.id ? String(user.id) : null), [user]);
const loadDashboard = useCallback(
async (showSpinner = true) => {
if (!token || !currentUserId) return;
if (showSpinner) setLoading(true);
setError(null);
try {
const [taskRows, spans] = await Promise.all([
fetchTasks(token),
fetchStaffTimeSpans(token, currentUserId),
]);
const tasks = taskRows || [];
const openTasks = tasks.filter((task) => normalizeTaskStatus(task.categorie) === 'Offen').length;
const inProgressTasks = tasks.filter(
(task) => normalizeTaskStatus(task.categorie) === 'In Bearbeitung'
).length;
const activeTime = spans.find((span) => !span.stopped_at) || null;
const pendingSubmissions = spans.filter(
(span) => (span.state === 'draft' || span.state === 'factual') && !!span.stopped_at
).length;
const today = new Date();
const todayIso = today.toISOString().slice(0, 10);
const todayMinutes = spans
.filter((span) => span.started_at?.slice(0, 10) === todayIso)
.reduce((sum, span) => sum + (span.duration_minutes || 0), 0);
setData({
tasks,
openTasks,
inProgressTasks,
activeTimeStart: activeTime?.started_at || null,
pendingSubmissions,
todayMinutes,
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Dashboard konnte nicht geladen werden.');
} finally {
setLoading(false);
setRefreshing(false);
}
},
[currentUserId, token]
);
useEffect(() => {
if (!token || !currentUserId) return;
void loadDashboard(true);
}, [currentUserId, loadDashboard, token]);
async function onRefresh() {
setRefreshing(true);
await loadDashboard(false);
}
return (
<ScrollView
contentContainerStyle={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
{error ? <Text style={styles.error}>{error}</Text> : null}
{loading ? (
<View style={styles.loadingBox}>
<ActivityIndicator />
<Text style={styles.loadingText}>Dashboard wird geladen...</Text>
</View>
) : null}
{!loading ? (
<>
<View style={styles.row}>
<View style={[styles.metricCard, styles.metricCardPrimary]}>
<Text style={styles.metricLabelPrimary}>Aktive Zeit</Text>
<Text style={styles.metricValuePrimary}>
{data.activeTimeStart ? `Seit ${formatDateTime(data.activeTimeStart)}` : 'Nicht aktiv'}
</Text>
</View>
</View>
<View style={styles.row}>
<View style={styles.metricCard}>
<Text style={styles.metricLabel}>Offene Aufgaben</Text>
<Text style={styles.metricValue}>{data.openTasks}</Text>
</View>
<View style={styles.metricCard}>
<Text style={styles.metricLabel}>In Bearbeitung</Text>
<Text style={styles.metricValue}>{data.inProgressTasks}</Text>
</View>
</View>
<View style={styles.row}>
<View style={styles.metricCard}>
<Text style={styles.metricLabel}>Heute erfasst</Text>
<Text style={styles.metricValue}>{formatMinutes(data.todayMinutes)}</Text>
</View>
<View style={styles.metricCard}>
<Text style={styles.metricLabel}>Zum Einreichen</Text>
<Text style={styles.metricValue}>{data.pendingSubmissions}</Text>
</View>
</View>
<View style={styles.quickActionsCard}>
<Text style={styles.quickActionsTitle}>Schnellzugriff</Text>
<View style={styles.quickActionsRow}>
<Pressable style={styles.quickActionButton} onPress={() => router.push('/(tabs)/tasks')}>
<Text style={styles.quickActionText}>Aufgaben</Text>
</Pressable>
<Pressable style={styles.quickActionButton} onPress={() => router.push('/(tabs)/projects')}>
<Text style={styles.quickActionText}>Projekten</Text>
</Pressable>
<Pressable style={styles.quickActionButton} onPress={() => router.push('/(tabs)/time')}>
<Text style={styles.quickActionText}>Zeiten</Text>
</Pressable>
<Pressable style={styles.quickActionButton} onPress={() => router.push('/more/inventory?action=scan')}>
<Text style={styles.quickActionText}>Inventar Scan</Text>
</Pressable>
</View>
</View>
</>
) : null}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
gap: 12,
backgroundColor: '#f9fafb',
},
row: {
flexDirection: 'row',
gap: 10,
},
metricCard: {
flex: 1,
backgroundColor: '#ffffff',
borderRadius: 12,
borderWidth: 1,
borderColor: '#e5e7eb',
padding: 12,
gap: 6,
},
metricCardPrimary: {
borderColor: PRIMARY,
backgroundColor: '#eff9ea',
},
metricLabel: {
color: '#6b7280',
fontSize: 12,
textTransform: 'uppercase',
},
metricValue: {
color: '#111827',
fontSize: 22,
fontWeight: '700',
},
metricLabelPrimary: {
color: '#3d7a30',
fontSize: 12,
textTransform: 'uppercase',
},
metricValuePrimary: {
color: '#2f5f24',
fontSize: 18,
fontWeight: '700',
},
quickActionsCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
borderWidth: 1,
borderColor: '#e5e7eb',
padding: 12,
gap: 10,
},
quickActionsTitle: {
color: '#111827',
fontSize: 15,
fontWeight: '600',
},
quickActionsRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
quickActionButton: {
minWidth: 120,
minHeight: 40,
borderRadius: 10,
backgroundColor: PRIMARY,
alignItems: 'center',
justifyContent: 'center',
},
quickActionText: {
color: '#ffffff',
fontWeight: '600',
},
error: {
color: '#dc2626',
fontSize: 13,
},
loadingBox: {
padding: 16,
alignItems: 'center',
gap: 8,
},
loadingText: {
color: '#6b7280',
},
});