270 lines
7.8 KiB
TypeScript
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',
|
|
},
|
|
});
|