882 lines
26 KiB
TypeScript
882 lines
26 KiB
TypeScript
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,
|
|
},
|
|
});
|