import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Modal, Pressable, RefreshControl, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { router, useLocalSearchParams } from 'expo-router'; import { BarcodeScanningResult, BarcodeType, CameraView, useCameraPermissions } from 'expo-camera'; import { Customer, CustomerInventoryItem, fetchAllCustomerInventoryItems, fetchCustomers, renderPrintLabel } from '@/src/lib/api'; import { getActiveNimbotConnection, printNimbotEncodedLabel } from '@/src/lib/nimbot'; 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 resolveCustomerId(item: CustomerInventoryItem): number | null { const raw = item.customer; if (!raw) return null; if (typeof raw === 'object') { return raw.id ? Number(raw.id) : null; } return Number(raw); } function resolveCustomerName(item: CustomerInventoryItem, customersById: Map): string { const raw = item.customer; if (raw && typeof raw === 'object' && raw.name) { return String(raw.name); } const id = resolveCustomerId(item); if (!id) return '-'; return String(customersById.get(id)?.name || `#${id}`); } export default function InventoryScreen() { const { token } = useAuth(); const params = useLocalSearchParams<{ action?: string | string[] }>(); const [cameraPermission, requestCameraPermission] = useCameraPermissions(); const scanTriggeredRef = useRef(false); const [items, setItems] = useState([]); const [customers, setCustomers] = useState([]); const [search, setSearch] = useState(''); const [showArchived, setShowArchived] = useState(false); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const [printingItemId, setPrintingItemId] = useState(null); const [printInfo, setPrintInfo] = useState(null); const [printError, setPrintError] = useState(null); const [scannerOpen, setScannerOpen] = useState(false); const [hasScanned, setHasScanned] = useState(false); const incomingAction = useMemo(() => { const raw = Array.isArray(params.action) ? params.action[0] : params.action; return String(raw || '').toLowerCase(); }, [params.action]); const customersById = useMemo(() => { const map = new Map(); (customers || []).forEach((customer) => { if (Number.isFinite(Number(customer.id))) { map.set(Number(customer.id), customer); } }); return map; }, [customers]); const filteredItems = useMemo(() => { const terms = search.trim().toLowerCase().split(/\s+/).filter(Boolean); return (items || []) .filter((item) => { if (!showArchived && item.archived) return false; if (terms.length === 0) return true; const customerName = resolveCustomerName(item, customersById); const haystack = [ item.name, item.customerInventoryId, item.serialNumber, item.description, item.manufacturer, item.manufacturerNumber, customerName, ] .map((value) => String(value || '').toLowerCase()) .join(' '); return terms.every((term) => haystack.includes(term)); }) .sort((a, b) => Number(b.id) - Number(a.id)); }, [customersById, items, search, showArchived]); const load = useCallback( async (showSpinner = true) => { if (!token) return; if (showSpinner) setLoading(true); setError(null); try { const [inventoryRows, customerRows] = await Promise.all([ fetchAllCustomerInventoryItems(token, true), fetchCustomers(token, true), ]); setItems(inventoryRows || []); setCustomers(customerRows || []); } catch (err) { setError(err instanceof Error ? err.message : 'Kundeninventar konnte nicht geladen werden.'); } finally { setLoading(false); setRefreshing(false); } }, [token] ); useEffect(() => { if (!token) return; void load(true); }, [load, token]); const openScanner = useCallback(async () => { const granted = cameraPermission?.granted; if (!granted) { const request = await requestCameraPermission(); if (!request.granted) { setError('Bitte erlaube den Zugriff auf die Kamera zum Scannen.'); return; } } setHasScanned(false); setScannerOpen(true); }, [cameraPermission?.granted, requestCameraPermission]); useEffect(() => { if (incomingAction !== 'scan') return; if (scanTriggeredRef.current) return; scanTriggeredRef.current = true; void openScanner(); }, [incomingAction, openScanner]); async function onRefresh() { setRefreshing(true); await load(false); } function closeScanner() { setScannerOpen(false); setHasScanned(false); } function onBarcodeScanned(result: BarcodeScanningResult) { if (hasScanned) return; const code = String(result.data || '').trim(); if (!code) return; setHasScanned(true); setSearch(code); closeScanner(); } function openNimbotForItem(item: CustomerInventoryItem, customerName: string) { router.push({ pathname: '/more/nimbot', params: { itemName: item.name || '', itemId: item.customerInventoryId || '', serial: item.serialNumber || '', customerInventoryId: item.customerInventoryId || '', serialNumber: item.serialNumber || '', customerName, }, }); } async function printItemLabel(item: CustomerInventoryItem, customerName: string) { setPrintError(null); setPrintInfo(null); const connected = getActiveNimbotConnection(); if (!connected) { openNimbotForItem(item, customerName); return; } if (!token) { setPrintError('Nicht angemeldet. Bitte erneut einloggen.'); return; } setPrintingItemId(Number(item.id)); try { const context: Record = { id: item.id, customerInventoryId: item.customerInventoryId || null, name: item.name || null, customerName: customerName === '-' ? null : customerName, serialNumber: item.serialNumber || null, }; const rendered = await renderPrintLabel(token, context, 584, 354); await printNimbotEncodedLabel(rendered.encoded, { density: 5, copies: 1, labelType: 1 }); setPrintInfo(`Label gedruckt: ${item.name || item.customerInventoryId || `#${item.id}`}`); } catch (err) { setPrintError(err instanceof Error ? err.message : 'Label konnte nicht gedruckt werden.'); } finally { setPrintingItemId(null); } } const isPrinterConnected = Boolean(getActiveNimbotConnection()?.device.id); return ( <> Scannen setShowArchived((prev) => !prev)}> Abgeschlossene anzeigen }> {error ? {error} : null} {printError ? {printError} : null} {printInfo ? {printInfo} : null} {loading ? ( Kundeninventar wird geladen... ) : null} {!loading && filteredItems.length === 0 ? Kein Kundeninventar gefunden. : null} {!loading && filteredItems.map((item) => { const customerId = resolveCustomerId(item); const customerName = resolveCustomerName(item, customersById); return ( [styles.row, pressed ? styles.rowPressed : null]} onPress={() => { if (customerId) { router.push(`/more/customer/${customerId}`); } }}> {item.name || '-'} {item.archived ? Abgeschlossen : null} Kunde: {customerName} ID: {item.customerInventoryId || '-'} · Seriennummer: {item.serialNumber || '-'} {(() => { const isPrinting = printingItemId === Number(item.id); return ( { event.stopPropagation(); void printItemLabel(item, customerName); }} disabled={isPrinting}> {isPrinting ? 'Drucke...' : isPrinterConnected ? 'Label drucken' : 'Mit Nimbot verbinden'} ); })()} ); })} Kundeninventar scannen Schließen Barcode oder DataMatrix mittig ins Bild halten. ); } const styles = StyleSheet.create({ screen: { flex: 1, backgroundColor: '#ffffff' }, searchWrap: { paddingHorizontal: 16, paddingTop: 12, paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', backgroundColor: '#ffffff', gap: 8, }, searchInput: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, color: '#111827', backgroundColor: '#ffffff', }, searchActions: { flexDirection: 'row', alignItems: 'center', gap: 8, flexWrap: 'wrap', }, scanButton: { minHeight: 36, borderRadius: 9, backgroundColor: PRIMARY, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', }, scanButtonText: { color: '#ffffff', fontWeight: '700', fontSize: 13, }, toggleButton: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 999, paddingHorizontal: 10, paddingVertical: 6, backgroundColor: '#ffffff', }, toggleButtonActive: { borderColor: PRIMARY, backgroundColor: '#eff9ea', }, toggleButtonText: { color: '#374151', fontSize: 12, fontWeight: '500', }, toggleButtonTextActive: { color: '#3d7a30', }, list: { flex: 1, backgroundColor: '#ffffff' }, listContent: { paddingBottom: 96 }, row: { paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', backgroundColor: '#ffffff', gap: 2, }, rowPressed: { backgroundColor: '#f3f4f6' }, rowHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', gap: 8, }, rowTitle: { color: '#111827', fontSize: 15, fontWeight: '600', flex: 1, minWidth: 0, }, rowSubtitle: { color: '#6b7280', fontSize: 13, }, rowActions: { marginTop: 8, }, printButton: { minHeight: 34, borderRadius: 8, borderWidth: 1, borderColor: PRIMARY, backgroundColor: '#eff9ea', paddingHorizontal: 10, alignItems: 'center', justifyContent: 'center', alignSelf: 'flex-start', }, printButtonText: { color: '#3d7a30', fontSize: 12, fontWeight: '700', }, badge: { color: '#3d7a30', backgroundColor: '#eff9ea', borderRadius: 999, paddingHorizontal: 8, paddingVertical: 4, fontSize: 12, overflow: 'hidden', }, empty: { color: '#6b7280', textAlign: 'center', paddingVertical: 16, }, loadingBox: { padding: 16, alignItems: 'center', gap: 8, }, loadingText: { color: '#6b7280', }, error: { color: '#dc2626', fontSize: 13, paddingHorizontal: 16, paddingTop: 10, }, info: { color: '#166534', fontSize: 13, paddingHorizontal: 16, paddingTop: 10, }, printButtonDisabled: { opacity: 0.65, }, 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)', }, });