import { useEffect, useMemo, useState } from 'react'; import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker'; import { KeyboardAvoidingView, Keyboard, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { CalendarEvent, createEvent, EventState, Project, updateEvent } from '@/src/lib/api'; import { useAuth } from '@/src/providers/auth-provider'; const PRIMARY = '#69c350'; type EventCreateModalProps = { visible: boolean; projects?: Project[]; initialProjectId?: number | null; initialDate?: Date | null; event?: CalendarEvent | null; onClose: () => void; onCreated?: (event: CalendarEvent) => void | Promise; onUpdated?: (event: CalendarEvent) => void | Promise; }; function pad(value: number): string { return String(value).padStart(2, '0'); } function formatDate(date: Date): string { return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()}`; } function formatTime(date: Date): string { return `${pad(date.getHours())}:${pad(date.getMinutes())}`; } function createDefaults(initialDate?: Date | null) { const start = initialDate ? new Date(initialDate) : new Date(); if (!initialDate) { start.setMinutes(Math.ceil(start.getMinutes() / 15) * 15, 0, 0); } else { start.setHours(9, 0, 0, 0); } const end = new Date(start.getTime() + 60 * 60 * 1000); return { start, end }; } function relationId(value: unknown): number | null { if (!value) return null; if (typeof value === 'object') { const id = (value as { id?: number | string }).id; return id ? Number(id) : null; } const id = Number(value); return Number.isFinite(id) ? id : null; } export function EventCreateModal({ visible, 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('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); const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId); const [projectSearch, setProjectSearch] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!visible) return; const defaults = createDefaults(initialDate); 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(relationId(event?.project) ?? initialProjectId); setProjectSearch(''); setError(null); }, [event, initialDate, initialProjectId, visible]); const selectedProject = useMemo( () => projects.find((project) => Number(project.id) === selectedProjectId) || null, [projects, selectedProjectId] ); const projectOptions = useMemo(() => { const terms = projectSearch.trim().toLowerCase().split(/\s+/).filter(Boolean); return projects .filter((project) => { if (terms.length === 0) return true; const haystack = `${project.name || ''} ${project.projectNumber || ''}`.toLowerCase(); return terms.every((term) => haystack.includes(term)); }) .slice(0, 8); }, [projectSearch, projects]); function updatePicker(event: DateTimePickerEvent, selected?: Date) { if (Platform.OS === 'android') setActivePicker(null); if (event.type === 'dismissed' || !selected || !activePicker) return; const isStart = activePicker.startsWith('start'); const isDate = activePicker.endsWith('Date'); const current = new Date(isStart ? start : end); if (isDate) { current.setFullYear(selected.getFullYear(), selected.getMonth(), selected.getDate()); } else { current.setHours(selected.getHours(), selected.getMinutes(), 0, 0); } if (isStart) { setStart(current); if (end <= current) setEnd(new Date(current.getTime() + 60 * 60 * 1000)); } else { setEnd(current); } } function openPicker(picker: 'startDate' | 'startTime' | 'endDate' | 'endTime') { Keyboard.dismiss(); setActivePicker(picker); } async function save() { if (!token || saving) return; const title = name.trim(); if (!title) { setError('Bitte einen Namen für den Termin eingeben.'); return; } if (end <= start) { setError('Das Ende muss nach dem Beginn liegen.'); return; } setSaving(true); setError(null); try { const payload = { name: title, startDate: start.toISOString(), endDate: end.toISOString(), project: selectedProjectId, customer: relationId(selectedProject?.customer) ?? relationId(event?.customer), notes: notes.trim() || null, link: link.trim() || null, 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.'); } finally { setSaving(false); } } return ( {event ? 'Termin bearbeiten' : 'Neuer Termin'} Beginn openPicker('startDate')}> {formatDate(start)} openPicker('startTime')}> {formatTime(start)} Ende openPicker('endDate')}> {formatDate(end)} openPicker('endTime')}> {formatTime(end)} Status {(['Entwurf', 'Final'] as EventState[]).map((option) => ( setState(option)}> {option} ))} Projekt {selectedProject ? ( {selectedProject.name} {selectedProject.projectNumber ? ( Nr. {selectedProject.projectNumber} ) : null} setSelectedProjectId(null)}> Ändern ) : ( <> {projectOptions.map((project) => ( setSelectedProjectId(Number(project.id))}> {project.name} {project.projectNumber || `#${project.id}`} ))} )} {error ? {error} : null} Abbrechen {saving ? 'Speichere...' : event ? 'Speichern' : 'Anlegen'} {activePicker && Platform.OS === 'ios' ? ( setActivePicker(null)} /> {activePicker.startsWith('start') ? 'Beginn' : 'Ende'} · {activePicker.endsWith('Date') ? 'Datum' : 'Uhrzeit'} setActivePicker(null)}> Fertig ) : null} {activePicker && Platform.OS === 'android' ? ( ) : null} ); } const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(17, 24, 39, 0.45)', justifyContent: 'center', padding: 20 }, keyboardWrap: { width: '100%' }, card: { maxHeight: '92%', backgroundColor: '#ffffff', borderRadius: 14, padding: 16, gap: 12 }, title: { color: '#111827', fontSize: 18, fontWeight: '700' }, scroll: { flexShrink: 1 }, form: { gap: 10, paddingBottom: 4 }, label: { color: '#374151', fontSize: 13, fontWeight: '700' }, input: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, color: '#111827', backgroundColor: '#ffffff', }, row: { flexDirection: 'row', gap: 8 }, pickerButton: { minHeight: 44, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ffffff', }, pickerButtonText: { color: '#111827', fontSize: 15, fontWeight: '600' }, dateInput: { flex: 1 }, timeInput: { width: 92 }, 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', alignItems: 'center', justifyContent: 'space-between', borderWidth: 1, borderColor: '#b9e3ae', backgroundColor: '#eff9ea', borderRadius: 10, padding: 10, gap: 8, }, selectedProjectMain: { flex: 1 }, selectedProjectName: { color: '#111827', fontSize: 14, fontWeight: '700' }, selectedProjectNumber: { color: '#6b7280', fontSize: 12 }, removeProject: { color: '#3d7a30', fontSize: 13, fontWeight: '700' }, projectOption: { flexDirection: 'row', justifyContent: 'space-between', borderBottomWidth: 1, borderBottomColor: '#e5e7eb', paddingVertical: 8, gap: 8, }, projectOptionName: { flex: 1, color: '#111827', fontSize: 14, fontWeight: '600' }, projectOptionNumber: { color: '#6b7280', fontSize: 12 }, error: { color: '#dc2626', fontSize: 13 }, actions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 8 }, secondaryButton: { minHeight: 40, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', }, secondaryButtonText: { color: '#374151', fontWeight: '600' }, primaryButton: { minHeight: 40, borderRadius: 10, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', backgroundColor: PRIMARY, }, primaryButtonText: { color: '#ffffff', fontWeight: '700' }, disabled: { opacity: 0.6 }, });