chore(sync): KI-AGENT: Aktuellen Entwicklungsstand für TestFlight übernehmen
This commit is contained in:
@@ -39,6 +39,8 @@ function getStateLabel(state: string): string {
|
||||
}
|
||||
|
||||
function getTypeLabel(type: string): string {
|
||||
if (type === 'pause') return 'Pause';
|
||||
if (type === 'overtime_compensation') return 'Freizeitausgleich';
|
||||
if (type === 'vacation') return 'Urlaub';
|
||||
if (type === 'sick') return 'Krankheit';
|
||||
if (type === 'holiday') return 'Feiertag';
|
||||
@@ -133,6 +135,17 @@ export default function TimeTrackingScreen() {
|
||||
}
|
||||
}, [active, currentUserId, token, load]);
|
||||
|
||||
async function onPause() {
|
||||
if (!token || !currentUserId || !active) return;
|
||||
setActionLoading(true); setError(null);
|
||||
try {
|
||||
await createStaffTimeEvent(token, { eventtype: active.paused ? 'pause_end' : 'pause_start',
|
||||
eventtime: new Date().toISOString(), user_id: currentUserId });
|
||||
await load(false);
|
||||
} catch (err) { setError(err instanceof Error ? err.message : 'Pause konnte nicht gespeichert werden.'); }
|
||||
finally { setActionLoading(false); }
|
||||
}
|
||||
|
||||
async function onSubmit(entry: StaffTimeSpan) {
|
||||
if (!token || !entry.eventIds?.length) return;
|
||||
setActionLoading(true);
|
||||
@@ -206,10 +219,15 @@ export default function TimeTrackingScreen() {
|
||||
<View style={styles.statusCard}>
|
||||
<Text style={styles.statusLabel}>Aktive Zeit</Text>
|
||||
<Text style={styles.statusValue}>
|
||||
{active ? `Läuft seit ${formatDateTime(active.started_at)}` : 'Keine aktive Zeit'}
|
||||
{active ? `${active.paused ? 'In Pause' : 'Läuft'} seit ${formatDateTime(active.started_at)}` : 'Keine aktive Zeit'}
|
||||
</Text>
|
||||
|
||||
<View style={styles.statusActions}>
|
||||
{active?.model === 'spans-v2' && active.type === 'work' ? (
|
||||
<Pressable style={[styles.startButton, actionLoading ? styles.buttonDisabled : null]} onPress={onPause} disabled={actionLoading}>
|
||||
<Text style={styles.startButtonText}>{active.paused ? 'Pause beenden' : 'Pause beginnen'}</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
{active ? (
|
||||
<Pressable
|
||||
style={[styles.stopButton, actionLoading ? styles.buttonDisabled : null]}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from '@/src/lib/server-config';
|
||||
import { useAuth } from '@/src/providers/auth-provider';
|
||||
|
||||
import { NotificationPreferences } from '@/src/components/NotificationPreferences';
|
||||
|
||||
const PRIMARY = '#69c350';
|
||||
|
||||
function isValidServerUrl(value: string): boolean {
|
||||
@@ -153,6 +155,7 @@ export default function SettingsScreen() {
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
{token && <NotificationPreferences key={String(activeTenantId)} token={token} />}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.title}>Server-Instanz</Text>
|
||||
<Text style={styles.hint}>
|
||||
|
||||
43
mobile/src/components/NotificationPreferences.tsx
Normal file
43
mobile/src/components/NotificationPreferences.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Pressable, Switch, Text, View } from 'react-native';
|
||||
import { fetchNotificationPreferences, saveNotificationPreferences, type NotificationPreferenceEvent } from '@/src/lib/api';
|
||||
|
||||
export function NotificationPreferences({ token }: { token: string }) {
|
||||
const [events, setEvents] = useState<NotificationPreferenceEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setLoading(true);
|
||||
fetchNotificationPreferences(token).then(result => { if (active) setEvents(result.events); })
|
||||
.catch(error => { if (active) setMessage(error.message); })
|
||||
.finally(() => { if (active) setLoading(false); });
|
||||
return () => { active = false; };
|
||||
}, [token]);
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
setMessage('');
|
||||
try { await saveNotificationPreferences(token, events); setMessage('Einstellungen gespeichert.'); }
|
||||
catch (error) { setMessage(error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'); }
|
||||
finally { setSaving(false); }
|
||||
}
|
||||
return <View style={{ gap: 12, padding: 16, backgroundColor: '#fff', borderRadius: 12 }}>
|
||||
<Text style={{ fontSize: 18, fontWeight: '600' }}>Meine Benachrichtigungen</Text>
|
||||
<Text>Gilt nur für dein Profil im aktuell ausgewählten Mandanten. E-Mail geht an deine Konto-Adresse, Push an deine registrierten Geräte.</Text>
|
||||
{loading && <Text>Einstellungen werden geladen …</Text>}
|
||||
{!!message && <Text accessibilityRole="alert">{message}</Text>}
|
||||
{!loading && events.map(event => <View key={event.eventKey} style={{ gap: 4 }}>
|
||||
<Text style={{ fontWeight: '600' }}>{event.displayName}</Text>
|
||||
<Text>{event.description}</Text>
|
||||
{(['email', 'push'] as const).filter(channel => channel in event.channels).map(channel => <View key={channel} style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text>{channel === 'email' ? 'E-Mail' : 'Push'}</Text>
|
||||
<Switch accessibilityLabel={`${event.displayName}: ${channel}`} disabled={saving} value={event.channels[channel] === true}
|
||||
onValueChange={enabled => setEvents(rows => rows.map(row => row.eventKey === event.eventKey ? { ...row, channels: { ...row.channels, [channel]: enabled } } : row))} />
|
||||
</View>)}
|
||||
</View>)}
|
||||
{!loading && events.length > 0 && <Pressable accessibilityRole="button" disabled={saving} onPress={save} style={{ padding: 12, backgroundColor: '#69c350', borderRadius: 8 }}>
|
||||
<Text>{saving ? 'Speichert …' : 'Speichern'}</Text>
|
||||
</Pressable>}
|
||||
</View>;
|
||||
}
|
||||
@@ -34,6 +34,8 @@ export type TenantProfile = {
|
||||
};
|
||||
|
||||
export type StaffTimeSpan = {
|
||||
paused?: boolean;
|
||||
model?: string;
|
||||
id: string | null;
|
||||
eventIds: string[];
|
||||
state: string;
|
||||
@@ -610,8 +612,22 @@ export async function fetchStaffTimeSpans(
|
||||
const query = targetUserId ? `?targetUserId=${encodeURIComponent(targetUserId)}` : '';
|
||||
const spans = await apiRequest<any[]>(`/api/staff/time/spans${query}`, { token });
|
||||
|
||||
return (spans || [])
|
||||
const seen = new Set<string>();
|
||||
return (spans || []).filter((span) => {
|
||||
if (span.model !== 'spans-v2') return true;
|
||||
if (seen.has(span.id)) return false;
|
||||
seen.add(span.id); return true;
|
||||
})
|
||||
.map((span) => {
|
||||
if (span.attendance) {
|
||||
const s = span.attendance;
|
||||
const end = s.ended_at ? Date.parse(s.ended_at) : Date.now();
|
||||
const pauseMs = s.breaks.reduce((n: number, p: any) => n + ((p.ended_at ? Date.parse(p.ended_at) : end) - Date.parse(p.started_at)), 0);
|
||||
return { id: s.id, eventIds: [s.id], state: s.status, started_at: s.started_at,
|
||||
stopped_at: s.ended_at, duration_minutes: Math.floor((end - Date.parse(s.started_at) - pauseMs) / 60000),
|
||||
user_id: s.user_id, type: s.type, description: s.description,
|
||||
paused: s.breaks.some((p: any) => !p.ended_at), model: 'spans-v2' } as StaffTimeSpan;
|
||||
}
|
||||
const started = span.startedAt ? new Date(span.startedAt) : null;
|
||||
const ended = span.endedAt ? new Date(span.endedAt) : new Date();
|
||||
const durationMinutes =
|
||||
@@ -1315,3 +1331,17 @@ export async function uploadPlantFile(
|
||||
|
||||
return parsed as ProjectFile;
|
||||
}
|
||||
|
||||
export type NotificationPreferenceEvent = {
|
||||
eventKey: string;
|
||||
displayName: string;
|
||||
description: string | null;
|
||||
channels: Partial<Record<'email' | 'push', boolean>>;
|
||||
};
|
||||
export async function fetchNotificationPreferences(token: string) {
|
||||
return apiRequest<{ tenantId: number; events: NotificationPreferenceEvent[] }>('/api/notifications/preferences', { token });
|
||||
}
|
||||
export async function saveNotificationPreferences(token: string, events: NotificationPreferenceEvent[]) {
|
||||
return apiRequest('/api/notifications/preferences', { token, method: 'PUT', body: events.flatMap(event =>
|
||||
Object.entries(event.channels).map(([channel, enabled]) => ({ eventType: event.eventKey, channel, enabled }))) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user