Fixes
This commit is contained in:
410
mobile/app/more/nimbot.tsx
Normal file
410
mobile/app/more/nimbot.tsx
Normal file
@@ -0,0 +1,410 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user