Mobile Dev
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

This commit is contained in:
2026-02-21 21:21:39 +01:00
parent 30d761f899
commit 409db82368
16 changed files with 2663 additions and 689 deletions

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
import { router, useLocalSearchParams } from 'expo-router';
import {
createStaffTimeEvent,
@@ -47,6 +48,7 @@ function getTypeLabel(type: string): string {
export default function TimeTrackingScreen() {
const { token, user } = useAuth();
const params = useLocalSearchParams<{ action?: string | string[] }>();
const [entries, setEntries] = useState<StaffTimeSpan[]>([]);
const [loading, setLoading] = useState(true);
@@ -55,8 +57,13 @@ export default function TimeTrackingScreen() {
const [error, setError] = useState<string | null>(null);
const currentUserId = useMemo(() => (user?.id ? String(user.id) : null), [user]);
const handledActionRef = useRef<string | null>(null);
const active = useMemo(() => entries.find((entry) => !entry.stopped_at) || null, [entries]);
const incomingAction = useMemo(() => {
const raw = Array.isArray(params.action) ? params.action[0] : params.action;
return String(raw || '').toLowerCase();
}, [params.action]);
const load = useCallback(
async (showSpinner = true) => {
@@ -87,7 +94,7 @@ export default function TimeTrackingScreen() {
await load(false);
}
async function onStart() {
const onStart = useCallback(async () => {
if (!token || !currentUserId) return;
setActionLoading(true);
setError(null);
@@ -105,9 +112,9 @@ export default function TimeTrackingScreen() {
} finally {
setActionLoading(false);
}
}
}, [currentUserId, token, load]);
async function onStop() {
const onStop = useCallback(async () => {
if (!token || !currentUserId || !active) return;
setActionLoading(true);
setError(null);
@@ -124,7 +131,7 @@ export default function TimeTrackingScreen() {
} finally {
setActionLoading(false);
}
}
}, [active, currentUserId, token, load]);
async function onSubmit(entry: StaffTimeSpan) {
if (!token || !entry.eventIds?.length) return;
@@ -141,6 +148,57 @@ export default function TimeTrackingScreen() {
}
}
const onSubmitAll = useCallback(async () => {
if (!token) return;
const submitCandidates = entries.filter(
(entry) => (entry.state === 'draft' || entry.state === 'factual') && !!entry.stopped_at && !!entry.eventIds?.length
);
if (submitCandidates.length === 0) return;
setActionLoading(true);
setError(null);
try {
for (const entry of submitCandidates) {
await submitStaffTime(token, entry.eventIds);
}
await load(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Einreichen fehlgeschlagen.');
} finally {
setActionLoading(false);
}
}, [entries, load, token]);
useEffect(() => {
if (!token || !currentUserId) return;
if (!incomingAction) return;
if (handledActionRef.current === incomingAction) return;
if (incomingAction === 'start' && !active) {
handledActionRef.current = incomingAction;
void onStart().finally(() => router.replace('/(tabs)/time'));
return;
}
if (incomingAction === 'stop' && active) {
handledActionRef.current = incomingAction;
void onStop().finally(() => router.replace('/(tabs)/time'));
return;
}
if (incomingAction === 'submit') {
handledActionRef.current = incomingAction;
void onSubmitAll().finally(() => router.replace('/(tabs)/time'));
return;
}
handledActionRef.current = incomingAction;
void router.replace('/(tabs)/time');
}, [active, currentUserId, incomingAction, onStart, onStop, onSubmitAll, token]);
return (
<ScrollView
contentContainerStyle={styles.container}
@@ -148,7 +206,7 @@ export default function TimeTrackingScreen() {
<View style={styles.statusCard}>
<Text style={styles.statusLabel}>Aktive Zeit</Text>
<Text style={styles.statusValue}>
{active ? `Laeuft seit ${formatDateTime(active.started_at)}` : 'Keine aktive Zeit'}
{active ? `Läuft seit ${formatDateTime(active.started_at)}` : 'Keine aktive Zeit'}
</Text>
<View style={styles.statusActions}>
@@ -179,7 +237,7 @@ export default function TimeTrackingScreen() {
</View>
) : null}
{!loading && entries.length === 0 ? <Text style={styles.empty}>Keine Zeiteintraege vorhanden.</Text> : null}
{!loading && entries.length === 0 ? <Text style={styles.empty}>Keine Zeiteinträge vorhanden.</Text> : null}
{!loading &&
entries.map((entry) => {
@@ -194,7 +252,7 @@ export default function TimeTrackingScreen() {
<Text style={styles.entryTime}>Start: {formatDateTime(entry.started_at)}</Text>
<Text style={styles.entryTime}>
Ende: {entry.stopped_at ? formatDateTime(entry.stopped_at) : 'laeuft...'}
Ende: {entry.stopped_at ? formatDateTime(entry.stopped_at) : 'läuft...'}
</Text>
<Text style={styles.entryTime}>Dauer: {formatDuration(entry.duration_minutes)}</Text>