import { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, KeyboardAvoidingView, Modal, Platform, Pressable, RefreshControl, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { router, useLocalSearchParams } from 'expo-router'; import * as DocumentPicker from 'expo-document-picker'; import * as ImagePicker from 'expo-image-picker'; import * as WebBrowser from 'expo-web-browser'; import { createProjectTask, fetchProjectById, fetchProjectFiles, fetchProjectTasks, Project, ProjectFile, Task, TaskStatus, uploadProjectFile, updateTask, } from '@/src/lib/api'; import { useAuth } from '@/src/providers/auth-provider'; const PRIMARY = '#69c350'; const TASK_STATUS_ORDER: TaskStatus[] = ['Offen', 'In Bearbeitung', 'Abgeschlossen']; function normalizeTaskStatus(status: unknown): TaskStatus { if (status === 'In Bearbeitung' || status === 'Abgeschlossen') return status; return 'Offen'; } function formatDateTime(value: unknown): string { if (!value) return '-'; const date = new Date(String(value)); if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit', }); } function getRefName(value: unknown): string { if (!value) return '-'; if (typeof value === 'object') { const record = value as { name?: string; full_name?: string; email?: string; id?: string | number }; return String(record.name || record.full_name || record.email || record.id || '-'); } return String(value); } export default function ProjectDetailScreen() { const params = useLocalSearchParams<{ id?: string }>(); const projectId = Number(params.id); const { token, user } = useAuth(); const [project, setProject] = useState(null); const [files, setFiles] = useState([]); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [uploading, setUploading] = useState(false); const [creatingTask, setCreatingTask] = useState(false); const [updatingTaskId, setUpdatingTaskId] = useState(null); const [createTaskModalOpen, setCreateTaskModalOpen] = useState(false); const [createTaskError, setCreateTaskError] = useState(null); const [newTaskName, setNewTaskName] = useState(''); const [newTaskDescription, setNewTaskDescription] = useState(''); const [showCompletedTasks, setShowCompletedTasks] = useState(false); const [error, setError] = useState(null); const validProjectId = useMemo(() => Number.isFinite(projectId) && projectId > 0, [projectId]); const currentUserId = useMemo(() => (user?.id ? String(user.id) : null), [user]); const infoRows = useMemo(() => { if (!project) return []; return [ { label: 'Projektname', value: project.name || '-' }, { label: 'Projekt-Nr.', value: String(project.projectNumber || '-') }, { label: 'Aktive Phase', value: String(project.active_phase || '-') }, { label: 'Kunde', value: getRefName(project.customer) }, { label: 'Objekt', value: getRefName(project.plant) }, { label: 'Vertrag', value: getRefName(project.contract) }, { label: 'Projekttyp', value: getRefName(project.projecttype || project.projectType) }, { label: 'Kunden-Ref.', value: String(project.customerRef || '-') }, { label: 'Leistung', value: String(project.measure || '-') }, { label: 'Erstellt', value: formatDateTime(project.createdAt || project.created_at) }, { label: 'Aktualisiert', value: formatDateTime(project.updatedAt || project.updated_at) }, ]; }, [project]); const visibleTasks = useMemo(() => { return (tasks || []) .filter((task) => { const status = normalizeTaskStatus(task.categorie); return showCompletedTasks || status !== 'Abgeschlossen'; }) .sort((a, b) => { const statusDiff = TASK_STATUS_ORDER.indexOf(normalizeTaskStatus(a.categorie)) - TASK_STATUS_ORDER.indexOf(normalizeTaskStatus(b.categorie)); if (statusDiff !== 0) return statusDiff; return Number(b.id) - Number(a.id); }); }, [showCompletedTasks, tasks]); const load = useCallback(async (showSpinner = true) => { if (!token || !validProjectId) return; if (showSpinner) setLoading(true); setError(null); try { const [projectData, fileData, taskData] = await Promise.all([ fetchProjectById(token, projectId), fetchProjectFiles(token, projectId), fetchProjectTasks(token, projectId), ]); setProject(projectData); setFiles(fileData); setTasks(taskData); } catch (err) { setError(err instanceof Error ? err.message : 'Projektdaten konnten nicht geladen werden.'); } finally { setLoading(false); setRefreshing(false); } }, [projectId, token, validProjectId]); useEffect(() => { if (!token || !validProjectId) return; void load(true); }, [load, token, validProjectId]); async function onRefresh() { setRefreshing(true); await load(false); } function closeCreateTaskModal() { setCreateTaskModalOpen(false); setCreateTaskError(null); setNewTaskName(''); setNewTaskDescription(''); } async function onCreateTask() { if (!token || !validProjectId || creatingTask) return; const name = newTaskName.trim(); if (!name) { setCreateTaskError('Bitte einen Aufgabentitel eingeben.'); return; } setCreatingTask(true); setCreateTaskError(null); setError(null); try { await createProjectTask(token, { projectId, name, description: newTaskDescription.trim() || null, userId: currentUserId, }); closeCreateTaskModal(); await load(false); } catch (err) { setCreateTaskError(err instanceof Error ? err.message : 'Aufgabe konnte nicht erstellt werden.'); } finally { setCreatingTask(false); } } async function onOpenFile(file: ProjectFile) { if (!file.url) return; await WebBrowser.openBrowserAsync(file.url, { presentationStyle: WebBrowser.WebBrowserPresentationStyle.FORM_SHEET, controlsColor: PRIMARY, showTitle: true, enableDefaultShareMenuItem: true, }); } async function onCompleteTask(task: Task) { if (!token || !task?.id) return; if (normalizeTaskStatus(task.categorie) === 'Abgeschlossen') return; setUpdatingTaskId(Number(task.id)); setError(null); try { await updateTask(token, Number(task.id), { categorie: 'Abgeschlossen' }); setTasks((prev) => prev.map((item) => (Number(item.id) === Number(task.id) ? { ...item, categorie: 'Abgeschlossen' } : item)) ); } catch (err) { setError(err instanceof Error ? err.message : 'Aufgabe konnte nicht abgeschlossen werden.'); } finally { setUpdatingTaskId(null); } } async function onPickAndUpload() { if (!token || !validProjectId || uploading) return; const result = await DocumentPicker.getDocumentAsync({ multiple: false, copyToCacheDirectory: true, type: ['image/*', 'application/pdf', '*/*'], }); if (result.canceled || !result.assets?.length) return; const asset = result.assets[0]; const filename = asset.name || `upload-${Date.now()}`; setUploading(true); setError(null); try { await uploadProjectFile(token, { projectId, uri: asset.uri, filename, mimeType: asset.mimeType || 'application/octet-stream', }); await load(false); } catch (err) { setError(err instanceof Error ? err.message : 'Upload fehlgeschlagen.'); } finally { setUploading(false); } } async function uploadImageFromUri(uri: string, filename: string, mimeType?: string) { if (!token || !validProjectId) return; setUploading(true); setError(null); try { await uploadProjectFile(token, { projectId, uri, filename, mimeType: mimeType || 'image/jpeg', }); await load(false); } catch (err) { setError(err instanceof Error ? err.message : 'Upload fehlgeschlagen.'); } finally { setUploading(false); } } async function onPickImage() { if (!token || !validProjectId || uploading) return; const permission = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!permission.granted) { setError('Bitte erlaube den Zugriff auf deine Fotos.'); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 0.85, allowsEditing: false, }); if (result.canceled || !result.assets?.length) return; const asset = result.assets[0]; const filename = asset.fileName || `bild-${Date.now()}.jpg`; await uploadImageFromUri(asset.uri, filename, asset.mimeType || 'image/jpeg'); } async function onTakePhoto() { if (!token || !validProjectId || uploading) return; const permission = await ImagePicker.requestCameraPermissionsAsync(); if (!permission.granted) { setError('Bitte erlaube den Zugriff auf die Kamera.'); return; } const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: 0.85, allowsEditing: false, }); if (result.canceled || !result.assets?.length) return; const asset = result.assets[0]; const filename = asset.fileName || `foto-${Date.now()}.jpg`; await uploadImageFromUri(asset.uri, filename, asset.mimeType || 'image/jpeg'); } if (!validProjectId) { return ( Ungültige Projekt-ID. ); } return ( }> {error ? {error} : null} {loading ? ( Projekt wird geladen... ) : null} {!loading && project ? ( <> Informationen router.push({ pathname: '/more/wiki', params: { entityType: 'projects', entityId: String(projectId), title: `Projekt-Wiki: ${String(project.name || projectId)}`, }, }) }> Wiki {infoRows.map((row) => ( {row.label} {row.value} ))} {project.notes ? ( Notizen {String(project.notes)} ) : null} Aufgaben ({tasks.length}) setShowCompletedTasks((prev) => !prev)}> Abgeschlossene anzeigen setCreateTaskModalOpen(true)}> Neue Aufgabe {visibleTasks.length === 0 ? ( Keine Aufgaben vorhanden. ) : ( visibleTasks.map((task) => { const status = normalizeTaskStatus(task.categorie); const isUpdatingTask = updatingTaskId === Number(task.id); const canComplete = status !== 'Abgeschlossen'; return ( {task.name || '-'} {task.description ? ( {String(task.description)} ) : null} {status} {canComplete ? ( onCompleteTask(task)} disabled={isUpdatingTask}> {isUpdatingTask ? '...' : 'Abhaken'} ) : null} ); }) )} Dokumente ({files.length}) {uploading ? 'Upload...' : 'Foto aufnehmen'} {uploading ? 'Upload...' : 'Bild auswählen'} {uploading ? 'Upload...' : 'Dokument hochladen'} {files.length === 0 ? ( Noch keine Dokumente vorhanden. ) : ( files.map((file) => ( onOpenFile(file)}> {file.name || file.path?.split('/').pop() || file.id} {file.mimeType || 'Datei'} )) )} ) : null} Neue Projekt-Aufgabe {createTaskError ? {createTaskError} : null} Abbrechen {creatingTask ? 'Speichere...' : 'Anlegen'} ); } const styles = StyleSheet.create({ container: { padding: 16, gap: 12, backgroundColor: '#f9fafb', }, centered: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16, }, card: { backgroundColor: '#ffffff', borderRadius: 12, borderWidth: 1, borderColor: '#e5e7eb', padding: 12, gap: 8, }, title: { color: '#111827', fontSize: 18, fontWeight: '700', }, infoTable: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, overflow: 'hidden', }, infoRow: { borderBottomWidth: 1, borderBottomColor: '#e5e7eb', paddingHorizontal: 10, paddingVertical: 8, gap: 2, backgroundColor: '#ffffff', }, infoLabel: { color: '#6b7280', fontSize: 12, textTransform: 'uppercase', fontWeight: '600', }, infoValue: { color: '#111827', fontSize: 14, fontWeight: '500', }, notesBox: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, padding: 10, backgroundColor: '#fafafa', gap: 4, }, notesLabel: { color: '#6b7280', fontSize: 12, fontWeight: '600', textTransform: 'uppercase', }, notes: { color: '#374151', fontSize: 14, }, sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', gap: 10, }, sectionTitle: { color: '#111827', fontSize: 16, fontWeight: '700', }, sectionHeaderActions: { alignItems: 'flex-end', gap: 8, }, filterChip: { borderRadius: 999, borderWidth: 1, borderColor: '#d1d5db', paddingHorizontal: 10, paddingVertical: 6, backgroundColor: '#ffffff', }, filterChipActive: { borderColor: PRIMARY, backgroundColor: '#eff9ea', }, filterChipText: { color: '#374151', fontSize: 12, fontWeight: '500', }, filterChipTextActive: { color: '#3d7a30', }, smallPrimaryButton: { minHeight: 34, borderRadius: 8, backgroundColor: PRIMARY, paddingHorizontal: 11, alignItems: 'center', justifyContent: 'center', }, smallPrimaryButtonText: { color: '#ffffff', fontWeight: '700', fontSize: 12, }, taskRow: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, padding: 10, gap: 8, }, taskRowMain: { gap: 4, }, taskTitle: { color: '#111827', fontSize: 14, fontWeight: '600', }, taskDescription: { color: '#374151', fontSize: 13, }, statusBadge: { alignSelf: 'flex-start', color: '#3d7a30', backgroundColor: '#eff9ea', borderRadius: 999, paddingHorizontal: 8, paddingVertical: 4, fontSize: 12, overflow: 'hidden', }, taskRowFooter: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8, }, completeButton: { minHeight: 28, borderRadius: 8, borderWidth: 1, borderColor: PRIMARY, backgroundColor: '#eff9ea', paddingHorizontal: 10, alignItems: 'center', justifyContent: 'center', }, completeButtonText: { color: '#3d7a30', fontSize: 12, fontWeight: '700', }, uploadButton: { minHeight: 38, borderRadius: 9, backgroundColor: PRIMARY, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', }, uploadActions: { gap: 8, }, uploadButtonText: { color: '#ffffff', fontWeight: '700', fontSize: 13, }, fileRow: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, padding: 10, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 10, }, fileInfo: { flex: 1, minWidth: 0, }, fileName: { color: '#111827', fontSize: 14, fontWeight: '600', }, fileMeta: { color: '#6b7280', fontSize: 12, marginTop: 2, }, empty: { color: '#6b7280', fontSize: 13, paddingVertical: 4, }, loadingBox: { paddingVertical: 20, alignItems: 'center', gap: 8, }, loadingText: { color: '#6b7280', }, error: { color: '#dc2626', fontSize: 13, }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.35)', justifyContent: 'center', padding: 16, }, modalKeyboardWrap: { width: '100%', }, modalCard: { backgroundColor: '#ffffff', borderRadius: 14, padding: 14, gap: 10, }, modalTitle: { fontSize: 18, fontWeight: '700', color: '#111827', }, input: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, color: '#111827', backgroundColor: '#ffffff', }, inputMultiline: { minHeight: 72, textAlignVertical: 'top', }, modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 8, marginTop: 2, }, secondaryButton: { minHeight: 42, borderRadius: 10, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', backgroundColor: '#e5e7eb', }, secondaryButtonText: { color: '#111827', fontWeight: '600', }, primaryButton: { minHeight: 42, borderRadius: 10, paddingHorizontal: 14, backgroundColor: PRIMARY, alignItems: 'center', justifyContent: 'center', }, primaryButtonText: { color: '#ffffff', fontWeight: '600', }, buttonDisabled: { opacity: 0.6, }, });