import React, { useState } from 'react';
import { Alert, Image, ScrollView, StyleSheet, Text, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { api, DocCategory, Document } from '@/api';
import { useT } from '@/i18n';
import { AppBar } from '@/components/AppBar';
import { Badge, Button, Card, IconButton, Loading } from '@/components/ui';
import { CategoryPill, ExpiryBadge } from '@/components/domain';
import { PdfView } from '@/components/PdfView';
import { getLocalFile, openInSystemViewer, shareDocument } from '@/lib/documentFile';
import { formatBytes, formatDate } from '@/lib/format';
import { colors, fontSize, radii, spacing } from '@/theme';
export default function DocumentDetailScreen() {
const { id, propertyId } = useLocalSearchParams<{ id: string; propertyId?: string }>();
const docId = Number(id);
const pid = propertyId ? Number(propertyId) : undefined;
const router = useRouter();
const qc = useQueryClient();
const t = useT();
const [deleting, setDeleting] = useState(false);
const [busy, setBusy] = useState<'open' | 'share' | null>(null);
// Prefer the cached list entry; fetch only if we arrived without one.
const listQuery = useQuery({
queryKey: ['documents', pid],
queryFn: () => api.documents.listByProperty(pid as number),
enabled: pid != null,
});
const cached = listQuery.data?.find((d: Document) => d.id === docId);
const singleQuery = useQuery({
queryKey: ['document', docId],
queryFn: () => api.documents.get(docId),
enabled: !cached && pid == null,
});
const doc: Document | undefined = cached ?? singleQuery.data;
const catsQuery = useQuery({ queryKey: ['categories'], queryFn: () => api.reference.categories() });
const catName = (slug: string) =>
catsQuery.data?.find((c: DocCategory) => c.slug === slug)?.name ?? slug;
// Downloaded with the bearer header, for the preview and the phone's viewer.
const fileQuery = useQuery({
queryKey: ['document-file', docId],
queryFn: () => getLocalFile(doc as Document),
enabled: !!doc?.fileUri,
retry: false,
});
const removeMutation = useMutation({
mutationFn: () => api.documents.remove(docId),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['documents'] });
qc.invalidateQueries({ queryKey: ['all-documents'] });
qc.invalidateQueries({ queryKey: ['properties'] });
router.back();
},
});
function confirmDelete() {
Alert.alert(t('document.deleteTitle'), t('document.deleteBody'), [
{ text: t('common.cancel'), style: 'cancel' },
{
text: t('common.delete'),
style: 'destructive',
onPress: () => {
setDeleting(true);
removeMutation.mutate(undefined, { onSettled: () => setDeleting(false) });
},
},
]);
}
async function onOpen() {
if (!doc) return;
setBusy('open');
try {
await openInSystemViewer(doc);
} catch (e: any) {
Alert.alert(t('document.cannotOpen'), e?.message ?? t('document.cannotOpenBody'));
} finally {
setBusy(null);
}
}
async function onShare() {
if (!doc) return;
setBusy('share');
try {
await shareDocument(doc);
} catch (e: any) {
Alert.alert(t('document.cannotSave'), e?.message ?? t('document.cannotSaveBody'));
} finally {
setBusy(null);
}
}
if (!doc) {
return (
);
}
const isImage = doc.mimeType.startsWith('image/');
const fileReady = !!fileQuery.data;
const fileError = fileQuery.error as Error | undefined;
return (
router.push(`/document/edit/${docId}?propertyId=${pid ?? ''}`)}
/>
}
/>
{doc.expiryStatus === 'expiring' || doc.expiryStatus === 'expired' ? (
router.push(`/document/edit/${docId}?propertyId=${pid ?? ''}`)}
style={[
styles.banner,
doc.expiryStatus === 'expired'
? { backgroundColor: colors.dangerBg, borderColor: '#f0c2c9' }
: { backgroundColor: colors.warningBg, borderColor: '#f0dcae' },
]}
>
{doc.expiryStatus === 'expired' ? t('document.expired') : t('document.expiringSoon')}
{doc.expiryDate ? t('document.expiresOn', { date: formatDate(doc.expiryDate) }) : t('document.checkExpiry')}
) : null}
{/* Preview — always from the downloaded local copy, never a raw URL. */}
{fileQuery.isLoading ? (
) : fileError ? (
{fileError.message}
) : isImage && fileReady ? (
) : fileReady ? (
) : (
)}
{doc.pageCount ? (
) : null}
{doc.name}
);
}
function Meta({ k, v }: { k: string; v: string }) {
return (
{k}
{v}
);
}
function SkeletonPage() {
return (
{[0.9, 0.6, 0.95, 0.8, 0.95, 0.7, 0.85].map((w, i) => (
))}
);
}
const styles = StyleSheet.create({
body: { padding: spacing[4], paddingBottom: spacing[9] },
banner: { flexDirection: 'row', alignItems: 'flex-start', gap: spacing[3], marginBottom: spacing[4] },
bannerTitle: { fontSize: fontSize.md, fontWeight: '700' },
bannerText: { fontSize: fontSize.sm, opacity: 0.85, marginTop: 2 },
preview: { alignItems: 'center', justifyContent: 'center', marginBottom: spacing[4] },
previewBox: {
width: '100%',
height: 220,
borderRadius: radii.md,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
alignItems: 'center',
justifyContent: 'center',
gap: spacing[2],
padding: spacing[5],
},
previewErr: { fontSize: fontSize.sm, color: colors.textMuted, textAlign: 'center' },
previewImage: {
width: '100%',
height: 320,
borderRadius: radii.md,
backgroundColor: colors.surface,
},
pageBadge: { marginTop: spacing[3] },
name: { fontSize: fontSize.xl, fontWeight: '700', color: colors.ink },
tagRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: spacing[2] },
metaGrid: { flexDirection: 'row', flexWrap: 'wrap' },
metaItem: { width: '50%', paddingVertical: spacing[2] },
metaK: { fontSize: fontSize.xs, color: colors.textMuted2, textTransform: 'uppercase', letterSpacing: 0.3 },
metaV: { fontSize: fontSize.base, color: colors.ink, fontWeight: '600', marginTop: 2 },
actions: { flexDirection: 'row', gap: spacing[3], marginTop: spacing[4] },
skeleton: {
width: 230,
height: 300,
backgroundColor: colors.surface,
borderRadius: radii.md,
borderWidth: 1,
borderColor: colors.border,
padding: spacing[5],
justifyContent: 'flex-start',
},
skelBar: { height: 8, borderRadius: 4, backgroundColor: colors.surfaceSunken, marginBottom: 9 },
});