import { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Pressable, RefreshControl, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'; import { useLocalSearchParams } from 'expo-router'; import { renderPrintLabel } from '@/src/lib/api'; import { connectNimbotDevice, disconnectNimbotDevice, getActiveNimbotConnection, printNimbotEncodedLabel, scanNimbotDevices, } from '@/src/lib/nimbot'; import { useAuth } from '@/src/providers/auth-provider'; const PRIMARY = '#69c350'; type ListedDevice = { id: string; name: string; rssi: number | null; }; export default function NimbotScreen() { const { token } = useAuth(); const params = useLocalSearchParams<{ itemName?: string; itemId?: string; serial?: string; customerName?: string; customerInventoryId?: string; serialNumber?: string; }>(); const [devices, setDevices] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [scanning, setScanning] = useState(false); const [connectingId, setConnectingId] = useState(null); const [printing, setPrinting] = useState(false); const [error, setError] = useState(null); const [info, setInfo] = useState(null); const [manualLabelText, setManualLabelText] = useState(''); const activeConnection = getActiveNimbotConnection(); const normalizedInventoryId = useMemo(() => String(params.customerInventoryId || params.itemId || '').trim(), [params.customerInventoryId, params.itemId]); const normalizedSerial = useMemo(() => String(params.serialNumber || params.serial || '').trim(), [params.serial, params.serialNumber]); const prefilledText = useMemo(() => { const parts = [params.itemName, normalizedInventoryId ? `ID: ${normalizedInventoryId}` : null, normalizedSerial ? `SN: ${normalizedSerial}` : null] .filter(Boolean) .map((value) => String(value)); return parts.join(' | '); }, [normalizedInventoryId, normalizedSerial, params.itemName]); useEffect(() => { if (!prefilledText) return; setManualLabelText(prefilledText); }, [prefilledText]); const loadDevices = useCallback(async () => { setError(null); setInfo(null); setScanning(true); try { const result = await scanNimbotDevices(); setDevices(result); if (result.length === 0) { setInfo('Kein Nimbot gefunden. Drucker einschalten und nah an das iPhone halten.'); } } catch (err) { setError(err instanceof Error ? err.message : 'Nimbot-Suche fehlgeschlagen.'); } finally { setScanning(false); setLoading(false); setRefreshing(false); } }, []); useEffect(() => { void loadDevices(); }, [loadDevices]); async function onRefresh() { setRefreshing(true); await loadDevices(); } async function onConnect(deviceId: string) { setConnectingId(deviceId); setError(null); setInfo(null); try { const connected = await connectNimbotDevice(deviceId); setInfo(`Verbunden mit ${connected.device.name}`); } catch (err) { setError(err instanceof Error ? err.message : 'Verbindung fehlgeschlagen.'); } finally { setConnectingId(null); } } async function onDisconnect() { setError(null); setInfo(null); try { await disconnectNimbotDevice(); setInfo('Verbindung getrennt.'); } catch (err) { setError(err instanceof Error ? err.message : 'Trennen fehlgeschlagen.'); } } async function onPrint() { setError(null); setInfo(null); setPrinting(true); try { if (!token) { throw new Error('Nicht angemeldet. Bitte erneut einloggen.'); } const text = manualLabelText.trim(); if (!text) { throw new Error('Bitte Label-Inhalt eingeben.'); } const context: Record = { text, name: params.itemName || text, }; if (normalizedInventoryId) { context.customerInventoryId = normalizedInventoryId; } if (normalizedSerial) { context.serialNumber = normalizedSerial; } if (params.customerName) { context.customerName = String(params.customerName); } const rendered = await renderPrintLabel(token, context, 584, 354); await printNimbotEncodedLabel(rendered.encoded, { density: 5, copies: 1, labelType: 1, }); setInfo('Label wurde an den Nimbot gesendet.'); } catch (err) { setError(err instanceof Error ? err.message : 'Label konnte nicht gedruckt werden.'); } finally { setPrinting(false); } } const connectedId = activeConnection?.device.id || null; return ( }> Nimbot M2 Bluetooth-Anbindung (Beta) {connectedId ? ( Verbunden: {activeConnection?.device.name || connectedId} ) : ( Nicht verbunden )} void loadDevices()} disabled={scanning}> {scanning ? 'Suche...' : 'Geräte suchen'} void onDisconnect()} disabled={!connectedId}> Trennen void onPrint()} disabled={!connectedId || printing}> {printing ? 'Drucke...' : 'Label drucken'} {info ? {info} : null} {error ? {error} : null} Gefundene Geräte {loading ? ( Suche läuft... ) : null} {!loading && devices.length === 0 ? Keine Geräte gefunden. : null} {!loading && devices.map((device) => { const isConnected = connectedId === device.id; const isConnecting = connectingId === device.id; return ( {device.name} {device.id} {typeof device.rssi === 'number' ? ` · RSSI ${device.rssi}` : ''} void onConnect(device.id)} disabled={isConnecting || isConnected}> {isConnected ? 'Verbunden' : isConnecting ? 'Verbinde...' : 'Verbinden'} ); })} ); } const styles = StyleSheet.create({ container: { padding: 16, gap: 12, backgroundColor: '#f9fafb', }, card: { backgroundColor: '#ffffff', borderRadius: 12, borderWidth: 1, borderColor: '#e5e7eb', padding: 12, gap: 10, }, title: { color: '#111827', fontSize: 18, fontWeight: '700', }, subtitle: { color: '#6b7280', fontSize: 13, }, connectedText: { color: '#166534', fontSize: 13, fontWeight: '600', }, disconnectedText: { color: '#6b7280', fontSize: 13, }, actionRow: { flexDirection: 'row', gap: 8, }, input: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, minHeight: 72, textAlignVertical: 'top', color: '#111827', backgroundColor: '#ffffff', }, primaryButton: { minHeight: 40, borderRadius: 10, backgroundColor: PRIMARY, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', }, primaryButtonText: { color: '#ffffff', fontWeight: '700', fontSize: 13, }, secondaryButton: { minHeight: 40, borderRadius: 10, borderWidth: 1, borderColor: '#d1d5db', backgroundColor: '#ffffff', paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', }, secondaryButtonText: { color: '#111827', fontWeight: '600', fontSize: 13, }, sectionTitle: { color: '#111827', fontSize: 16, fontWeight: '700', }, deviceRow: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10, padding: 10, flexDirection: 'row', alignItems: 'center', gap: 10, }, deviceInfo: { flex: 1, minWidth: 0, }, deviceName: { color: '#111827', fontSize: 14, fontWeight: '600', }, deviceMeta: { color: '#6b7280', fontSize: 12, marginTop: 2, }, connectButton: { minHeight: 36, borderRadius: 9, borderWidth: 1, borderColor: PRIMARY, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea', }, connectButtonConnected: { borderColor: '#9ca3af', backgroundColor: '#f3f4f6', }, connectButtonText: { color: '#3d7a30', fontSize: 12, fontWeight: '700', }, loadingBox: { padding: 12, alignItems: 'center', gap: 8, }, loadingText: { color: '#6b7280', }, empty: { color: '#6b7280', fontSize: 13, paddingVertical: 4, }, info: { color: '#1d4ed8', fontSize: 13, }, error: { color: '#dc2626', fontSize: 13, }, buttonDisabled: { opacity: 0.6, }, });