411 lines
11 KiB
TypeScript
411 lines
11 KiB
TypeScript
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<ListedDevice[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [scanning, setScanning] = useState(false);
|
|
const [connectingId, setConnectingId] = useState<string | null>(null);
|
|
const [printing, setPrinting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [info, setInfo] = useState<string | null>(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<string, unknown> = {
|
|
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 (
|
|
<ScrollView
|
|
contentContainerStyle={styles.container}
|
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
|
|
<View style={styles.card}>
|
|
<Text style={styles.title}>Nimbot M2</Text>
|
|
<Text style={styles.subtitle}>Bluetooth-Anbindung (Beta)</Text>
|
|
|
|
{connectedId ? (
|
|
<Text style={styles.connectedText}>Verbunden: {activeConnection?.device.name || connectedId}</Text>
|
|
) : (
|
|
<Text style={styles.disconnectedText}>Nicht verbunden</Text>
|
|
)}
|
|
|
|
<View style={styles.actionRow}>
|
|
<Pressable
|
|
style={[styles.primaryButton, scanning ? styles.buttonDisabled : null]}
|
|
onPress={() => void loadDevices()}
|
|
disabled={scanning}>
|
|
<Text style={styles.primaryButtonText}>{scanning ? 'Suche...' : 'Geräte suchen'}</Text>
|
|
</Pressable>
|
|
|
|
<Pressable
|
|
style={[styles.secondaryButton, !connectedId ? styles.buttonDisabled : null]}
|
|
onPress={() => void onDisconnect()}
|
|
disabled={!connectedId}>
|
|
<Text style={styles.secondaryButtonText}>Trennen</Text>
|
|
</Pressable>
|
|
</View>
|
|
|
|
<TextInput
|
|
value={manualLabelText}
|
|
onChangeText={setManualLabelText}
|
|
placeholder="Label-Inhalt für nächsten Druck"
|
|
placeholderTextColor="#9ca3af"
|
|
style={styles.input}
|
|
multiline
|
|
/>
|
|
|
|
<Pressable
|
|
style={[styles.primaryButton, (!connectedId || printing) ? styles.buttonDisabled : null]}
|
|
onPress={() => void onPrint()}
|
|
disabled={!connectedId || printing}>
|
|
<Text style={styles.primaryButtonText}>{printing ? 'Drucke...' : 'Label drucken'}</Text>
|
|
</Pressable>
|
|
|
|
{info ? <Text style={styles.info}>{info}</Text> : null}
|
|
{error ? <Text style={styles.error}>{error}</Text> : null}
|
|
</View>
|
|
|
|
<View style={styles.card}>
|
|
<Text style={styles.sectionTitle}>Gefundene Geräte</Text>
|
|
|
|
{loading ? (
|
|
<View style={styles.loadingBox}>
|
|
<ActivityIndicator />
|
|
<Text style={styles.loadingText}>Suche läuft...</Text>
|
|
</View>
|
|
) : null}
|
|
|
|
{!loading && devices.length === 0 ? <Text style={styles.empty}>Keine Geräte gefunden.</Text> : null}
|
|
|
|
{!loading &&
|
|
devices.map((device) => {
|
|
const isConnected = connectedId === device.id;
|
|
const isConnecting = connectingId === device.id;
|
|
|
|
return (
|
|
<View key={device.id} style={styles.deviceRow}>
|
|
<View style={styles.deviceInfo}>
|
|
<Text style={styles.deviceName} numberOfLines={1}>
|
|
{device.name}
|
|
</Text>
|
|
<Text style={styles.deviceMeta} numberOfLines={1}>
|
|
{device.id}
|
|
{typeof device.rssi === 'number' ? ` · RSSI ${device.rssi}` : ''}
|
|
</Text>
|
|
</View>
|
|
|
|
<Pressable
|
|
style={[
|
|
styles.connectButton,
|
|
isConnected ? styles.connectButtonConnected : null,
|
|
isConnecting ? styles.buttonDisabled : null,
|
|
]}
|
|
onPress={() => void onConnect(device.id)}
|
|
disabled={isConnecting || isConnected}>
|
|
<Text style={styles.connectButtonText}>
|
|
{isConnected ? 'Verbunden' : isConnecting ? 'Verbinde...' : 'Verbinden'}
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
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,
|
|
},
|
|
});
|