Fixes
This commit is contained in:
881
mobile/app/more/customer/[id].tsx
Normal file
881
mobile/app/more/customer/[id].tsx
Normal file
@@ -0,0 +1,881 @@
|
||||
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<string, string> = {
|
||||
invoices: 'Rechnung',
|
||||
advanceInvoices: 'Abschlagsrechnung',
|
||||
cancellationInvoices: 'Stornorechnung',
|
||||
quotes: 'Angebot',
|
||||
confirmationOrders: 'Auftragsbestätigung',
|
||||
deliveryNotes: 'Lieferschein',
|
||||
};
|
||||
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<Customer | null>(null);
|
||||
const [createdDocuments, setCreatedDocuments] = useState<CreatedDocument[]>([]);
|
||||
const [files, setFiles] = useState<ProjectFile[]>([]);
|
||||
const [inventoryItems, setInventoryItems] = useState<CustomerInventoryItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [inventorySearch, setInventorySearch] = useState('');
|
||||
const [showArchivedInventory, setShowArchivedInventory] = useState(false);
|
||||
|
||||
const [createInventoryModalOpen, setCreateInventoryModalOpen] = useState(false);
|
||||
const [creatingInventory, setCreatingInventory] = useState(false);
|
||||
const [createInventoryError, setCreateInventoryError] = useState<string | null>(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 [];
|
||||
return [
|
||||
{ label: 'Name', value: String(customer.name || '-') },
|
||||
{ label: 'Kundennummer', value: String(customer.customerNumber || '-') },
|
||||
{ label: 'Typ', value: String(customer.type || '-') },
|
||||
{ label: 'E-Mail', value: String(customer.email || '-') },
|
||||
{ label: 'Telefon', value: String(customer.phone || '-') },
|
||||
{ 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 (
|
||||
<>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.container}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
|
||||
{error ? <Text style={styles.error}>{error}</Text> : null}
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.loadingBox}>
|
||||
<ActivityIndicator />
|
||||
<Text style={styles.loadingText}>Kunde wird geladen...</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{!loading && customer ? (
|
||||
<>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.title}>Informationen</Text>
|
||||
<Pressable
|
||||
style={styles.uploadButton}
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: '/more/wiki',
|
||||
params: {
|
||||
entityType: 'customers',
|
||||
entityId: String(customerId),
|
||||
title: `Kunden-Wiki: ${String(customer.name || customerId)}`,
|
||||
},
|
||||
})
|
||||
}>
|
||||
<Text style={styles.uploadButtonText}>Wiki</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.table}>
|
||||
{rows.map((row) => (
|
||||
<View key={row.label} style={styles.row}>
|
||||
<Text style={styles.label}>{row.label}</Text>
|
||||
<Text style={styles.value}>{row.value}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{customer.notes ? (
|
||||
<View style={styles.notesWrap}>
|
||||
<Text style={styles.notesLabel}>Notizen</Text>
|
||||
<Text style={styles.notes}>{String(customer.notes)}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>Kundeninventar ({inventoryItems.length})</Text>
|
||||
<View style={styles.inventoryHeaderActions}>
|
||||
<Pressable style={styles.secondaryActionButton} onPress={() => openScanner('search')}>
|
||||
<Text style={styles.secondaryActionButtonText}>Scannen</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.uploadButton} onPress={() => setCreateInventoryModalOpen(true)}>
|
||||
<Text style={styles.uploadButtonText}>Neu</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
placeholder="Inventar suchen"
|
||||
placeholderTextColor="#9ca3af"
|
||||
style={styles.searchInput}
|
||||
value={inventorySearch}
|
||||
onChangeText={setInventorySearch}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
style={[styles.filterButton, showArchivedInventory ? styles.filterButtonActive : null]}
|
||||
onPress={() => setShowArchivedInventory((prev) => !prev)}>
|
||||
<Text style={[styles.filterButtonText, showArchivedInventory ? styles.filterButtonTextActive : null]}>
|
||||
Abgeschlossene anzeigen
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
{filteredInventoryItems.length === 0 ? (
|
||||
<Text style={styles.empty}>Kein Kundeninventar gefunden.</Text>
|
||||
) : (
|
||||
filteredInventoryItems.map((item) => (
|
||||
<View key={String(item.id)} style={styles.fileRow}>
|
||||
<View style={styles.fileInfo}>
|
||||
<View style={styles.inventoryTitleRow}>
|
||||
<Text style={styles.fileName} numberOfLines={1}>
|
||||
{item.name || '-'}
|
||||
</Text>
|
||||
{item.archived ? <Text style={styles.badge}>Abgeschlossen</Text> : null}
|
||||
</View>
|
||||
<Text style={styles.fileMeta} numberOfLines={1}>
|
||||
ID: {item.customerInventoryId || '-'}
|
||||
</Text>
|
||||
<Text style={styles.fileMeta} numberOfLines={1}>
|
||||
Seriennummer: {item.serialNumber || '-'}
|
||||
</Text>
|
||||
{item.description ? (
|
||||
<Text style={styles.fileMeta} numberOfLines={2}>
|
||||
{String(item.description)}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.sectionTitle}>Ausgangsbelege ({createdDocuments.length})</Text>
|
||||
|
||||
{createdDocuments.length === 0 ? (
|
||||
<Text style={styles.empty}>Keine Ausgangsbelege vorhanden.</Text>
|
||||
) : (
|
||||
createdDocuments.map((doc) => (
|
||||
<Pressable key={String(doc.id)} style={styles.fileRow} onPress={() => onOpenCreatedDocument(doc)}>
|
||||
<View style={styles.fileInfo}>
|
||||
<Text style={styles.fileName} numberOfLines={1}>
|
||||
{doc.documentNumber || doc.title || `#${doc.id}`}
|
||||
</Text>
|
||||
<Text style={styles.fileMeta} numberOfLines={1}>
|
||||
{formatDocType(doc.type)}
|
||||
{doc.state ? ` · ${String(doc.state)}` : ''}
|
||||
{doc.documentDate ? ` · ${formatDateTime(doc.documentDate)}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>Dokumente ({files.length})</Text>
|
||||
<Pressable
|
||||
style={[styles.uploadButton, uploading ? styles.buttonDisabled : null]}
|
||||
onPress={onPickAndUpload}
|
||||
disabled={uploading}>
|
||||
<Text style={styles.uploadButtonText}>{uploading ? 'Upload...' : 'Hochladen'}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{files.length === 0 ? (
|
||||
<Text style={styles.empty}>Noch keine Dokumente vorhanden.</Text>
|
||||
) : (
|
||||
files.map((file) => (
|
||||
<Pressable key={file.id} style={styles.fileRow} onPress={() => onOpenFile(file)}>
|
||||
<View style={styles.fileInfo}>
|
||||
<Text style={styles.fileName} numberOfLines={2}>
|
||||
{file.name || file.path?.split('/').pop() || file.id}
|
||||
</Text>
|
||||
<Text style={styles.fileMeta}>{file.mimeType || 'Datei'}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
|
||||
<Modal
|
||||
visible={createInventoryModalOpen}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={closeCreateInventoryModal}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
style={styles.modalKeyboardWrap}>
|
||||
<View style={styles.modalCard}>
|
||||
<Text style={styles.modalTitle}>Neuer Kundeninventarartikel</Text>
|
||||
|
||||
<TextInput
|
||||
placeholder="Name"
|
||||
placeholderTextColor="#9ca3af"
|
||||
style={styles.searchInput}
|
||||
value={newInventoryName}
|
||||
onChangeText={setNewInventoryName}
|
||||
/>
|
||||
|
||||
<View style={styles.fieldGroup}>
|
||||
<TextInput
|
||||
placeholder="Kundeninventar-ID (optional)"
|
||||
placeholderTextColor="#9ca3af"
|
||||
style={styles.searchInput}
|
||||
value={newInventoryId}
|
||||
onChangeText={setNewInventoryId}
|
||||
/>
|
||||
<Pressable style={styles.secondaryActionButton} onPress={() => openScanner('customerInventoryId')}>
|
||||
<Text style={styles.secondaryActionButtonText}>Scannen</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.fieldGroup}>
|
||||
<TextInput
|
||||
placeholder="Seriennummer (optional)"
|
||||
placeholderTextColor="#9ca3af"
|
||||
style={styles.searchInput}
|
||||
value={newInventorySerial}
|
||||
onChangeText={setNewInventorySerial}
|
||||
/>
|
||||
<Pressable style={styles.secondaryActionButton} onPress={() => openScanner('serialNumber')}>
|
||||
<Text style={styles.secondaryActionButtonText}>Scannen</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
placeholder="Beschreibung (optional)"
|
||||
placeholderTextColor="#9ca3af"
|
||||
style={[styles.searchInput, styles.multilineInput]}
|
||||
value={newInventoryDescription}
|
||||
onChangeText={setNewInventoryDescription}
|
||||
multiline
|
||||
/>
|
||||
|
||||
{createInventoryError ? <Text style={styles.error}>{createInventoryError}</Text> : null}
|
||||
|
||||
<View style={styles.modalActions}>
|
||||
<Pressable
|
||||
style={[styles.secondaryButton, creatingInventory ? styles.buttonDisabled : null]}
|
||||
onPress={closeCreateInventoryModal}
|
||||
disabled={creatingInventory}>
|
||||
<Text style={styles.secondaryButtonText}>Abbrechen</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.primaryButton, creatingInventory ? styles.buttonDisabled : null]}
|
||||
onPress={onCreateInventoryItem}
|
||||
disabled={creatingInventory}>
|
||||
<Text style={styles.primaryButtonText}>{creatingInventory ? 'Speichere...' : 'Anlegen'}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<Modal visible={scannerOpen} animationType="slide" onRequestClose={closeScanner}>
|
||||
<View style={styles.scannerScreen}>
|
||||
<View style={styles.scannerHeader}>
|
||||
<Text style={styles.scannerTitle}>
|
||||
{scanTarget === 'customerInventoryId' ? 'Kundeninventar-ID scannen' : 'Seriennummer scannen'}
|
||||
</Text>
|
||||
<Pressable style={styles.scannerCloseButton} onPress={closeScanner}>
|
||||
<Text style={styles.scannerCloseButtonText}>Schließen</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<CameraView
|
||||
style={styles.scannerCamera}
|
||||
facing="back"
|
||||
barcodeScannerSettings={{ barcodeTypes: SCAN_TYPES }}
|
||||
onBarcodeScanned={onBarcodeScanned}
|
||||
/>
|
||||
|
||||
<Text style={styles.scannerHint}>Barcode oder DataMatrix mittig ins Bild halten.</Text>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user