KI-AGENT: Kalender, Terminbearbeitung und Dateivorschau verbessern
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
Keyboard,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import { CalendarEvent, createEvent, Project } from '@/src/lib/api';
|
||||
import { CalendarEvent, createEvent, EventState, Project, updateEvent } from '@/src/lib/api';
|
||||
import { useAuth } from '@/src/providers/auth-provider';
|
||||
|
||||
const PRIMARY = '#69c350';
|
||||
@@ -22,8 +23,10 @@ type EventCreateModalProps = {
|
||||
projects?: Project[];
|
||||
initialProjectId?: number | null;
|
||||
initialDate?: Date | null;
|
||||
event?: CalendarEvent | null;
|
||||
onClose: () => void;
|
||||
onCreated: (event: CalendarEvent) => void | Promise<void>;
|
||||
onCreated?: (event: CalendarEvent) => void | Promise<void>;
|
||||
onUpdated?: (event: CalendarEvent) => void | Promise<void>;
|
||||
};
|
||||
|
||||
function pad(value: number): string {
|
||||
@@ -64,13 +67,16 @@ export function EventCreateModal({
|
||||
projects = [],
|
||||
initialProjectId = null,
|
||||
initialDate = null,
|
||||
event = null,
|
||||
onClose,
|
||||
onCreated,
|
||||
onUpdated,
|
||||
}: EventCreateModalProps) {
|
||||
const { token } = useAuth();
|
||||
const [name, setName] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
const [link, setLink] = useState('');
|
||||
const [state, setState] = useState<EventState>('Final');
|
||||
const [start, setStart] = useState(() => createDefaults(initialDate).start);
|
||||
const [end, setEnd] = useState(() => createDefaults(initialDate).end);
|
||||
const [activePicker, setActivePicker] = useState<'startDate' | 'startTime' | 'endDate' | 'endTime' | null>(null);
|
||||
@@ -82,16 +88,19 @@ export function EventCreateModal({
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
const defaults = createDefaults(initialDate);
|
||||
setName('');
|
||||
setNotes('');
|
||||
setLink('');
|
||||
setStart(defaults.start);
|
||||
setEnd(defaults.end);
|
||||
const eventStart = event?.startDate ? new Date(event.startDate) : null;
|
||||
const eventEnd = event?.endDate ? new Date(event.endDate) : null;
|
||||
setName(event?.name || '');
|
||||
setNotes(String(event?.notes || ''));
|
||||
setLink(String(event?.link || ''));
|
||||
setState(event?.state === 'Entwurf' ? 'Entwurf' : 'Final');
|
||||
setStart(eventStart && !Number.isNaN(eventStart.getTime()) ? eventStart : defaults.start);
|
||||
setEnd(eventEnd && !Number.isNaN(eventEnd.getTime()) ? eventEnd : defaults.end);
|
||||
setActivePicker(null);
|
||||
setSelectedProjectId(initialProjectId);
|
||||
setSelectedProjectId(relationId(event?.project) ?? initialProjectId);
|
||||
setProjectSearch('');
|
||||
setError(null);
|
||||
}, [initialDate, initialProjectId, visible]);
|
||||
}, [event, initialDate, initialProjectId, visible]);
|
||||
|
||||
const selectedProject = useMemo(
|
||||
() => projects.find((project) => Number(project.id) === selectedProjectId) || null,
|
||||
@@ -130,6 +139,11 @@ export function EventCreateModal({
|
||||
}
|
||||
}
|
||||
|
||||
function openPicker(picker: 'startDate' | 'startTime' | 'endDate' | 'endTime') {
|
||||
Keyboard.dismiss();
|
||||
setActivePicker(picker);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!token || saving) return;
|
||||
const title = name.trim();
|
||||
@@ -146,18 +160,22 @@ export function EventCreateModal({
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const event = await createEvent(token, {
|
||||
const payload = {
|
||||
name: title,
|
||||
startDate: start.toISOString(),
|
||||
endDate: end.toISOString(),
|
||||
project: selectedProjectId,
|
||||
customer: relationId(selectedProject?.customer),
|
||||
customer: relationId(selectedProject?.customer) ?? relationId(event?.customer),
|
||||
notes: notes.trim() || null,
|
||||
link: link.trim() || null,
|
||||
state: 'Final',
|
||||
repeatInterval: 'Keine Wiederholung',
|
||||
});
|
||||
await onCreated(event);
|
||||
state,
|
||||
repeatInterval: event?.repeatInterval || 'Keine Wiederholung',
|
||||
};
|
||||
const savedEvent = event
|
||||
? await updateEvent(token, Number(event.id), payload)
|
||||
: await createEvent(token, payload);
|
||||
if (event) await onUpdated?.(savedEvent);
|
||||
else await onCreated?.(savedEvent);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Der Termin konnte nicht gespeichert werden.');
|
||||
@@ -171,7 +189,7 @@ export function EventCreateModal({
|
||||
<View style={styles.overlay}>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.keyboardWrap}>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.title}>Neuer Termin</Text>
|
||||
<Text style={styles.title}>{event ? 'Termin bearbeiten' : 'Neuer Termin'}</Text>
|
||||
<ScrollView style={styles.scroll} contentContainerStyle={styles.form} keyboardShouldPersistTaps="handled">
|
||||
<TextInput
|
||||
autoFocus
|
||||
@@ -184,41 +202,37 @@ export function EventCreateModal({
|
||||
|
||||
<Text style={styles.label}>Beginn</Text>
|
||||
<View style={styles.row}>
|
||||
<Pressable style={[styles.pickerButton, styles.dateInput]} onPress={() => setActivePicker('startDate')}>
|
||||
<Pressable style={[styles.pickerButton, styles.dateInput]} onPress={() => openPicker('startDate')}>
|
||||
<Text style={styles.pickerButtonText}>{formatDate(start)}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.pickerButton, styles.timeInput]} onPress={() => setActivePicker('startTime')}>
|
||||
<Pressable style={[styles.pickerButton, styles.timeInput]} onPress={() => openPicker('startTime')}>
|
||||
<Text style={styles.pickerButtonText}>{formatTime(start)}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<Text style={styles.label}>Ende</Text>
|
||||
<View style={styles.row}>
|
||||
<Pressable style={[styles.pickerButton, styles.dateInput]} onPress={() => setActivePicker('endDate')}>
|
||||
<Pressable style={[styles.pickerButton, styles.dateInput]} onPress={() => openPicker('endDate')}>
|
||||
<Text style={styles.pickerButtonText}>{formatDate(end)}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.pickerButton, styles.timeInput]} onPress={() => setActivePicker('endTime')}>
|
||||
<Pressable style={[styles.pickerButton, styles.timeInput]} onPress={() => openPicker('endTime')}>
|
||||
<Text style={styles.pickerButtonText}>{formatTime(end)}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{activePicker ? (
|
||||
<View style={styles.pickerWrap}>
|
||||
<DateTimePicker
|
||||
value={activePicker.startsWith('start') ? start : end}
|
||||
mode={activePicker.endsWith('Date') ? 'date' : 'time'}
|
||||
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||
locale="de-DE"
|
||||
minuteInterval={5}
|
||||
onChange={updatePicker}
|
||||
/>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<Pressable style={styles.pickerDoneButton} onPress={() => setActivePicker(null)}>
|
||||
<Text style={styles.pickerDoneText}>Übernehmen</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
<Text style={styles.label}>Status</Text>
|
||||
<View style={styles.statusRow}>
|
||||
{(['Entwurf', 'Final'] as EventState[]).map((option) => (
|
||||
<Pressable
|
||||
key={option}
|
||||
style={[styles.statusButton, state === option ? styles.statusButtonActive : null]}
|
||||
onPress={() => setState(option)}>
|
||||
<Text style={[styles.statusButtonText, state === option ? styles.statusButtonTextActive : null]}>
|
||||
{option}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={styles.label}>Projekt</Text>
|
||||
{selectedProject ? (
|
||||
@@ -279,11 +293,45 @@ export function EventCreateModal({
|
||||
<Text style={styles.secondaryButtonText}>Abbrechen</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.primaryButton, saving ? styles.disabled : null]} onPress={save} disabled={saving}>
|
||||
<Text style={styles.primaryButtonText}>{saving ? 'Speichere...' : 'Anlegen'}</Text>
|
||||
<Text style={styles.primaryButtonText}>{saving ? 'Speichere...' : event ? 'Speichern' : 'Anlegen'}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
{activePicker && Platform.OS === 'ios' ? (
|
||||
<View style={styles.pickerOverlay}>
|
||||
<Pressable style={styles.pickerBackdrop} onPress={() => setActivePicker(null)} />
|
||||
<View style={styles.pickerSheet}>
|
||||
<View style={styles.pickerHeader}>
|
||||
<Text style={styles.pickerTitle}>
|
||||
{activePicker.startsWith('start') ? 'Beginn' : 'Ende'} · {activePicker.endsWith('Date') ? 'Datum' : 'Uhrzeit'}
|
||||
</Text>
|
||||
<Pressable style={styles.pickerDoneButton} onPress={() => setActivePicker(null)}>
|
||||
<Text style={styles.pickerDoneText}>Fertig</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<DateTimePicker
|
||||
value={activePicker.startsWith('start') ? start : end}
|
||||
mode={activePicker.endsWith('Date') ? 'date' : 'time'}
|
||||
display="spinner"
|
||||
locale="de-DE"
|
||||
minuteInterval={5}
|
||||
onChange={updatePicker}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{activePicker && Platform.OS === 'android' ? (
|
||||
<DateTimePicker
|
||||
value={activePicker.startsWith('start') ? start : end}
|
||||
mode={activePicker.endsWith('Date') ? 'date' : 'time'}
|
||||
display="default"
|
||||
minuteInterval={5}
|
||||
onChange={updatePicker}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
@@ -321,9 +369,38 @@ const styles = StyleSheet.create({
|
||||
pickerButtonText: { color: '#111827', fontSize: 15, fontWeight: '600' },
|
||||
dateInput: { flex: 1 },
|
||||
timeInput: { width: 92 },
|
||||
pickerWrap: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, padding: 8 },
|
||||
pickerDoneButton: { alignSelf: 'flex-end', paddingHorizontal: 10, paddingVertical: 7 },
|
||||
pickerOverlay: { ...StyleSheet.absoluteFillObject, justifyContent: 'flex-end', zIndex: 10 },
|
||||
pickerBackdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(17, 24, 39, 0.25)' },
|
||||
pickerSheet: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderTopLeftRadius: 18,
|
||||
borderTopRightRadius: 18,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 22,
|
||||
shadowColor: '#111827',
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 10,
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
},
|
||||
pickerHeader: { minHeight: 42, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
||||
pickerTitle: { color: '#374151', fontSize: 14, fontWeight: '700' },
|
||||
pickerDoneButton: { paddingHorizontal: 10, paddingVertical: 7 },
|
||||
pickerDoneText: { color: '#3d7a30', fontSize: 14, fontWeight: '700' },
|
||||
statusRow: { flexDirection: 'row', gap: 8 },
|
||||
statusButton: {
|
||||
flex: 1,
|
||||
minHeight: 42,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
statusButtonActive: { borderColor: PRIMARY, backgroundColor: '#eff9ea' },
|
||||
statusButtonText: { color: '#6b7280', fontSize: 14, fontWeight: '700' },
|
||||
statusButtonTextActive: { color: '#3d7a30' },
|
||||
multiline: { minHeight: 90, textAlignVertical: 'top' },
|
||||
selectedProject: {
|
||||
flexDirection: 'row',
|
||||
|
||||
Reference in New Issue
Block a user