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 WebBrowser from 'expo-web-browser'; import { BarcodeScanningResult, BarcodeType, CameraView, useCameraPermissions } from 'expo-camera'; import { createCustomerInventoryItem, CreatedDocument, Customer, CustomerInventoryItem, fetchCustomerById, fetchCustomerCreatedDocuments, fetchCreatedDocumentFiles, fetchCustomerFiles, fetchCustomerInventoryItems, ProjectFile, uploadCustomerFile, } from '@/src/lib/api'; import { useAuth } from '@/src/providers/auth-provider'; const PRIMARY = '#69c350'; const SCAN_TYPES: BarcodeType[] = [ 'datamatrix', 'code128', 'code39', 'code93', 'ean13', 'ean8', 'upc_a', 'upc_e', 'itf14', 'qr', ]; 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 formatDocType(value: unknown): string { const type = String(value || ''); if (!type) return 'Beleg'; const labels: Record = { invoices: 'Rechnung', advanceInvoices: 'Abschlagsrechnung', cancellationInvoices: 'Stornorechnung', quotes: 'Angebot', costEstimates: 'Kostenschätzung', confirmationOrders: 'Auftragsbestätigung', deliveryNotes: 'Lieferschein', packingSlips: 'Packschein', }; return labels[type] || type; } export default function CustomerDetailScreen() { const params = useLocalSearchParams<{ id?: string }>(); const customerId = Number(params.id); const { token } = useAuth(); const [cameraPermission, requestCameraPermission] = useCameraPermissions(); const [customer, setCustomer] = useState(null); const [createdDocuments, setCreatedDocuments] = useState([]); const [files, setFiles] = useState([]); const [inventoryItems, setInventoryItems] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [inventorySearch, setInventorySearch] = useState(''); const [showArchivedInventory, setShowArchivedInventory] = useState(false); const [createInventoryModalOpen, setCreateInventoryModalOpen] = useState(false); const [creatingInventory, setCreatingInventory] = useState(false); const [createInventoryError, setCreateInventoryError] = useState(null); const [newInventoryName, setNewInventoryName] = useState(''); const [newInventoryId, setNewInventoryId] = useState(''); const [newInventorySerial, setNewInventorySerial] = useState(''); const [newInventoryDescription, setNewInventoryDescription] = useState(''); const [scannerOpen, setScannerOpen] = useState(false); const [hasScanned, setHasScanned] = useState(false); const [scanTarget, setScanTarget] = useState<'customerInventoryId' | 'serialNumber' | 'search'>('serialNumber'); const validId = useMemo(() => Number.isFinite(customerId) && customerId > 0, [customerId]); const rows = useMemo(() => { if (!customer) return []; const infoData = customer.infoData || {}; return [ { label: 'Name', value: String(customer.name || '-') }, { label: 'Kundennummer', value: String(customer.customerNumber || '-') }, { label: 'Typ', value: String(customer.type || '-') }, { label: 'E-Mail', value: String(infoData.email || customer.email || '-') }, { label: 'Telefon', value: String(infoData.tel || customer.phone || '-') }, { label: 'Mobilnummer', value: String(infoData.mobileTel || '-') }, { label: 'Erstellt', value: formatDateTime(customer.createdAt || customer.created_at) }, { label: 'Aktualisiert', value: formatDateTime(customer.updatedAt || customer.updated_at) }, ]; }, [customer]); const filteredInventoryItems = useMemo(() => { const terms = inventorySearch.trim().toLowerCase().split(/\s+/).filter(Boolean); return (inventoryItems || []) .filter((item) => { if (!showArchivedInventory && item.archived) return false; if (terms.length === 0) return true; const haystack = [ item.name, item.customerInventoryId, item.serialNumber, item.description, item.manufacturer, item.manufacturerNumber, ] .map((value) => String(value || '').toLowerCase()) .join(' '); return terms.every((term) => haystack.includes(term)); }) .sort((a, b) => Number(b.id) - Number(a.id)); }, [inventoryItems, inventorySearch, showArchivedInventory]); const load = useCallback( async (showSpinner = true) => { if (!token || !validId) return; if (showSpinner) setLoading(true); setError(null); try { const [customerData, createdDocumentData, fileData, customerInventoryData] = await Promise.all([ fetchCustomerById(token, customerId), fetchCustomerCreatedDocuments(token, customerId), fetchCustomerFiles(token, customerId), fetchCustomerInventoryItems(token, customerId, true), ]); setCustomer(customerData); setCreatedDocuments(createdDocumentData); setFiles(fileData); setInventoryItems(customerInventoryData); } catch (err) { setError(err instanceof Error ? err.message : 'Kundendaten konnten nicht geladen werden.'); } finally { setLoading(false); setRefreshing(false); } }, [customerId, token, validId] ); useEffect(() => { if (!token || !validId) return; void load(true); }, [load, token, validId]); async function onRefresh() { setRefreshing(true); await load(false); } function closeCreateInventoryModal() { setCreateInventoryModalOpen(false); setCreateInventoryError(null); setNewInventoryName(''); setNewInventoryId(''); setNewInventorySerial(''); setNewInventoryDescription(''); } async function onCreateInventoryItem() { if (!token || !validId || creatingInventory) return; const name = newInventoryName.trim(); if (!name) { setCreateInventoryError('Bitte einen Namen eingeben.'); return; } setCreatingInventory(true); setCreateInventoryError(null); setError(null); try { await createCustomerInventoryItem(token, { customer: customerId, name, customerInventoryId: newInventoryId || null, serialNumber: newInventorySerial || null, description: newInventoryDescription || null, }); closeCreateInventoryModal(); await load(false); } catch (err) { setCreateInventoryError(err instanceof Error ? err.message : 'Kundeninventar konnte nicht erstellt werden.'); } finally { setCreatingInventory(false); } } async function openScanner(target: 'customerInventoryId' | 'serialNumber' | 'search') { const granted = cameraPermission?.granted; if (!granted) { const request = await requestCameraPermission(); if (!request.granted) { setCreateInventoryError('Bitte erlaube den Zugriff auf die Kamera zum Scannen.'); return; } } setScanTarget(target); setHasScanned(false); setScannerOpen(true); } function closeScanner() { setScannerOpen(false); setHasScanned(false); } function onBarcodeScanned(result: BarcodeScanningResult) { if (hasScanned) return; const code = String(result.data || '').trim(); if (!code) return; setHasScanned(true); if (scanTarget === 'customerInventoryId') { setNewInventoryId(code); } else if (scanTarget === 'search') { setInventorySearch(code); } else { setNewInventorySerial(code); } closeScanner(); } 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 onPickAndUpload() { if (!token || !validId || 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 uploadCustomerFile(token, { customerId, 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 onOpenCreatedDocument(doc: CreatedDocument) { if (!token) return; try { const linkedFiles = await fetchCreatedDocumentFiles(token, doc.id); const fileToOpen = linkedFiles.find((file) => Boolean(file.url)); if (!fileToOpen?.url) { setError('Kein verknüpftes Dokument zum Öffnen gefunden.'); return; } await onOpenFile(fileToOpen); } catch (err) { setError(err instanceof Error ? err.message : 'Dokument konnte nicht geöffnet werden.'); } } return ( <> }> {error ? {error} : null} {loading ? ( Kunde wird geladen... ) : null} {!loading && customer ? ( <> Informationen router.push({ pathname: '/more/wiki', params: { entityType: 'customers', entityId: String(customerId), title: `Kunden-Wiki: ${String(customer.name || customerId)}`, }, }) }> Wiki {rows.map((row) => ( {row.label} {row.value} ))} {customer.notes ? ( Notizen {String(customer.notes)} ) : null} Kundeninventar ({inventoryItems.length}) openScanner('search')}> Scannen setCreateInventoryModalOpen(true)}> Neu setShowArchivedInventory((prev) => !prev)}> Abgeschlossene anzeigen {filteredInventoryItems.length === 0 ? ( Kein Kundeninventar gefunden. ) : ( filteredInventoryItems.map((item) => ( {item.name || '-'} {item.archived ? Abgeschlossen : null} ID: {item.customerInventoryId || '-'} Seriennummer: {item.serialNumber || '-'} {item.description ? ( {String(item.description)} ) : null} )) )} Ausgangsbelege ({createdDocuments.length}) {createdDocuments.length === 0 ? ( Keine Ausgangsbelege vorhanden. ) : ( createdDocuments.map((doc) => ( onOpenCreatedDocument(doc)}> {doc.documentNumber || doc.title || `#${doc.id}`} {formatDocType(doc.type)} {doc.state ? ` · ${String(doc.state)}` : ''} {doc.documentDate ? ` · ${formatDateTime(doc.documentDate)}` : ''} )) )} Dokumente ({files.length}) {uploading ? 'Upload...' : '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} Neuer Kundeninventarartikel openScanner('customerInventoryId')}> Scannen openScanner('serialNumber')}> Scannen {createInventoryError ? {createInventoryError} : null} Abbrechen {creatingInventory ? 'Speichere...' : 'Anlegen'} {scanTarget === 'customerInventoryId' ? 'Kundeninventar-ID scannen' : 'Seriennummer scannen'} Schließen Barcode oder DataMatrix mittig ins Bild halten. ); } const styles = StyleSheet.create({ container: { padding: 16, gap: 12, backgroundColor: '#f9fafb', }, card: { backgroundColor: '#ffffff', borderRadius: 12, borderWidth: 1, borderColor: '#e5e7eb', padding: 12, gap: 8, }, title: { color: '#111827', fontSize: 18, fontWeight: '700', }, table: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, overflow: 'hidden', }, row: { borderBottomWidth: 1, borderBottomColor: '#e5e7eb', paddingHorizontal: 10, paddingVertical: 8, gap: 2, }, label: { color: '#6b7280', fontSize: 12, textTransform: 'uppercase', fontWeight: '600', }, value: { color: '#111827', fontSize: 14, fontWeight: '500', }, notesWrap: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, padding: 10, gap: 4, backgroundColor: '#fafafa', }, notesLabel: { color: '#6b7280', fontSize: 12, textTransform: 'uppercase', fontWeight: '600', }, notes: { color: '#374151', fontSize: 14, }, sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 10, }, sectionTitle: { color: '#111827', fontSize: 16, fontWeight: '700', }, inventoryHeaderActions: { flexDirection: 'row', alignItems: 'center', gap: 8, }, searchInput: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, color: '#111827', backgroundColor: '#ffffff', }, uploadButton: { minHeight: 38, borderRadius: 9, backgroundColor: PRIMARY, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', }, uploadButtonText: { color: '#ffffff', fontWeight: '700', fontSize: 13, }, filterButton: { alignSelf: 'flex-start', borderWidth: 1, borderColor: '#d1d5db', borderRadius: 999, paddingHorizontal: 10, paddingVertical: 6, backgroundColor: '#ffffff', }, filterButtonActive: { borderColor: PRIMARY, backgroundColor: '#eff9ea', }, filterButtonText: { color: '#374151', fontSize: 12, fontWeight: '500', }, filterButtonTextActive: { color: '#3d7a30', }, badge: { color: '#3d7a30', backgroundColor: '#eff9ea', borderRadius: 999, paddingHorizontal: 8, paddingVertical: 4, fontSize: 12, overflow: 'hidden', }, fileRow: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, padding: 10, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 10, }, fileInfo: { flex: 1, minWidth: 0, }, inventoryTitleRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8, }, fileName: { color: '#111827', fontSize: 14, fontWeight: '600', flex: 1, minWidth: 0, }, 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', }, fieldGroup: { gap: 8, }, secondaryActionButton: { minHeight: 36, borderRadius: 9, borderWidth: 1, borderColor: '#d1d5db', paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ffffff', }, secondaryActionButtonText: { color: '#111827', fontWeight: '600', fontSize: 13, }, multilineInput: { 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', }, scannerScreen: { flex: 1, backgroundColor: '#000000', }, scannerHeader: { paddingTop: 56, paddingHorizontal: 16, paddingBottom: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 10, }, scannerTitle: { color: '#ffffff', fontSize: 16, fontWeight: '700', flex: 1, minWidth: 0, }, scannerCloseButton: { minHeight: 34, borderRadius: 8, borderWidth: 1, borderColor: '#ffffff', paddingHorizontal: 10, alignItems: 'center', justifyContent: 'center', }, scannerCloseButtonText: { color: '#ffffff', fontWeight: '600', }, scannerCamera: { flex: 1, }, scannerHint: { color: '#ffffff', fontSize: 13, textAlign: 'center', paddingHorizontal: 16, paddingVertical: 14, backgroundColor: 'rgba(17,24,39,0.85)', }, buttonDisabled: { opacity: 0.6, }, });