import React, { useEffect, useState } from 'react'; import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import * as DocumentPicker from 'expo-document-picker'; import { api, DocCategory, DocRequirement, LocalFile, Property } from '@/api'; import { useT } from '@/i18n'; import { Button, Field, IconButton, Label, TextField } from '@/components/ui'; import { DateField } from '@/components/DateField'; import { FileGlyph } from '@/components/domain'; import { pickImageFromLibrary } from '@/lib/pickImage'; import { isCameraBlocked, openPermissionSettings, primeDocumentPermissions } from '@/lib/permissions'; import { scanDocumentToPdf } from '@/scanner/scanToPdf'; import { formatBytes } from '@/lib/format'; import { categoryColor, colors, fontSize, radii, spacing } from '@/theme'; export default function UploadScreen() { const insets = useSafeAreaInsets(); const router = useRouter(); const qc = useQueryClient(); const t = useT(); const params = useLocalSearchParams<{ propertyId?: string }>(); const propertiesQuery = useQuery({ queryKey: ['properties'], queryFn: () => api.properties.list() }); const categoriesQuery = useQuery({ queryKey: ['categories'], queryFn: () => api.reference.categories() }); const properties: Property[] = propertiesQuery.data ?? []; const categories: DocCategory[] = categoriesQuery.data ?? []; const [file, setFile] = useState(null); const [name, setName] = useState(''); const [propertyId, setPropertyId] = useState(params.propertyId ? Number(params.propertyId) : null); const [category, setCategory] = useState(''); const [requirementId, setRequirementId] = useState(null); const [pickingRequirement, setPickingRequirement] = useState(false); const [expiry, setExpiry] = useState(null); const [pickingProperty, setPickingProperty] = useState(false); const [scanning, setScanning] = useState(false); const effectivePropertyId = propertyId ?? properties[0]?.id ?? null; const effectiveCategory = category || categories[0]?.slug || ''; // Files hang off a requirement, so they load per category and are picked. const requirementsQuery = useQuery({ queryKey: ['requirements', effectiveCategory], queryFn: () => api.reference.requirements(effectiveCategory), enabled: !!effectiveCategory, }); const requirements: DocRequirement[] = requirementsQuery.data ?? []; const selectedRequirement = requirements.find((r) => r.id === requirementId) ?? null; const selectedProperty = properties.find((p) => p.id === effectivePropertyId); const uploadMutation = useMutation({ mutationFn: () => api.documents.upload({ propertyId: effectivePropertyId as number, name: name.trim(), category: effectiveCategory, requirementId: requirementId ?? undefined, expiryDate: expiry, file: file as LocalFile, }), onSuccess: () => { // Fire-and-forget: the lists update as the refetches land, and the modal // should not wait on them. qc.invalidateQueries({ queryKey: ['documents'] }); qc.invalidateQueries({ queryKey: ['all-documents'] }); qc.invalidateQueries({ queryKey: ['properties'] }); router.back(); }, onError: (e: any) => Alert.alert(t('upload.failed'), e?.message ?? t('common.tryAgain')), }); function applyFile(f: LocalFile, suggestedName?: string) { setFile(f); if (!name && suggestedName) setName(suggestedName); } async function onChooseFile() { const res = await DocumentPicker.getDocumentAsync({ type: ['application/pdf', 'image/*'], copyToCacheDirectory: true, }); if (res.canceled || !res.assets?.length) return; const a = res.assets[0]; applyFile( { uri: a.uri, name: a.name, mimeType: a.mimeType ?? 'application/octet-stream', sizeBytes: a.size ?? 0 }, a.name.replace(/\.[^.]+$/, ''), ); } async function onPickPhoto() { const picked = await pickImageFromLibrary({ quality: 0.9, fallbackName: 'photo' }); if (picked) applyFile(picked, t('upload.photoName')); } async function onScan() { setScanning(true); try { const result = await scanDocumentToPdf(name || t('upload.scannedName')); if (result) { applyFile(result.file, t('upload.scannedName')); } } catch (e: any) { // A blocked camera can't be fixed by retrying. if (await isCameraBlocked()) openPermissionSettings('permissions.cameraBody'); else Alert.alert(t('upload.scannerUnavailable'), e?.message ?? t('upload.scannerUnavailableBody')); } finally { setScanning(false); } } // Asked on open, so nobody is stopped halfway through picking a source. useEffect(() => { void primeDocumentPermissions(); }, []); function onSubmit() { if (!file) return Alert.alert(t('upload.noFileTitle'), t('upload.noFileBody')); if (!name.trim()) return Alert.alert(t('upload.nameRequired'), t('upload.nameRequiredBody')); if (!effectivePropertyId) return Alert.alert(t('upload.noPropertyTitle'), t('upload.noPropertyBody')); // Guessing the requirement files the document in the wrong place. if (requirements.length > 0 && !requirementId) { return Alert.alert(t('upload.noRequirementTitle'), t('upload.noRequirementBody')); } uploadMutation.mutate(); } return ( {t('upload.title')} router.back()} /> {/* Source */} {t('upload.scanHint')} {/* Selected file */} {file ? ( {file.name} {formatBytes(file.sizeBytes)} · {t('upload.readyToUpload')} setFile(null)} hitSlop={8}> ) : null} {/* Name */} {/* Property */} setPickingProperty((v) => !v)}> {selectedProperty?.name ?? t('upload.selectProperty')} {pickingProperty ? ( {properties.map((p) => ( { setPropertyId(p.id); setPickingProperty(false); }} > {p.name} {p.id === effectivePropertyId ? ( ) : null} ))} ) : null} {/* Category */} {categories.map((c) => { const active = c.slug === effectiveCategory; return ( { setCategory(c.slug); // Requirements belong to a category, so the old pick is stale. setRequirementId(null); setPickingRequirement(false); }} style={[styles.catChip, active && { backgroundColor: categoryColor(c.slug) + '18', borderColor: categoryColor(c.slug) }]} > {c.name} ); })} {/* Requirement — the backend's real filing unit */} {requirements.length > 0 ? ( setPickingRequirement((v) => !v)}> {selectedRequirement?.name ?? t('upload.selectRequirement')} {pickingRequirement ? ( {requirements.map((r, i) => { const newSection = i === 0 || requirements[i - 1].sectionName !== r.sectionName; return ( {newSection && r.sectionName ? ( {r.sectionName} ) : null} { setRequirementId(r.id); setPickingRequirement(false); // The requirement names it, unless the user already did. if (!name.trim()) setName(r.name); }} > {r.name} {r.id === requirementId ? ( ) : null} ); })} ) : null} {t('upload.requirementHint')} ) : null} {/* Expiry */}