121 lines
4.9 KiB
TypeScript
121 lines
4.9 KiB
TypeScript
import React, { useMemo, useState } from 'react';
|
|
import { FlatList, ScrollView, StyleSheet, View } from 'react-native';
|
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { api, DocCategory, Document } from '@/api';
|
|
import { useT } from '@/i18n';
|
|
import { AppBar } from '@/components/AppBar';
|
|
import { Chip, EmptyState, IconButton, Loading } from '@/components/ui';
|
|
import { DocumentRow } from '@/components/domain';
|
|
import { colors, spacing } from '@/theme';
|
|
|
|
type StatusFilter = 'all' | 'attention' | 'valid' | 'expiring' | 'expired';
|
|
|
|
const STATUSES: StatusFilter[] = ['all', 'attention', 'valid', 'expiring', 'expired'];
|
|
|
|
export default function DocumentsScreen() {
|
|
const { propertyId, category, status: initialStatus } = useLocalSearchParams<{
|
|
propertyId: string;
|
|
category?: string;
|
|
status?: string;
|
|
}>();
|
|
const pid = Number(propertyId);
|
|
const router = useRouter();
|
|
const t = useT();
|
|
// Links can preselect a filter (e.g. the "needs attention" banner).
|
|
const [status, setStatus] = useState<StatusFilter>(
|
|
STATUSES.includes(initialStatus as StatusFilter) ? (initialStatus as StatusFilter) : 'all',
|
|
);
|
|
|
|
const docsQuery = useQuery({ queryKey: ['documents', pid], queryFn: () => api.documents.listByProperty(pid) });
|
|
const propertyQuery = useQuery({ queryKey: ['property', pid], queryFn: () => api.properties.get(pid) });
|
|
const catsQuery = useQuery({ queryKey: ['categories'], queryFn: () => api.reference.categories() });
|
|
|
|
const categories: DocCategory[] = catsQuery.data ?? [];
|
|
const catName = (slug: string) => categories.find((c) => c.slug === slug)?.name ?? slug;
|
|
|
|
const inCategory = useMemo<Document[]>(
|
|
() => ((docsQuery.data ?? []) as Document[]).filter((d) => !category || d.category === category),
|
|
[docsQuery.data, category],
|
|
);
|
|
|
|
const counts = useMemo(() => {
|
|
const c = { all: inCategory.length, attention: 0, valid: 0, expiring: 0, expired: 0 };
|
|
for (const d of inCategory) {
|
|
if (d.expiryStatus === 'valid') c.valid += 1;
|
|
else if (d.expiryStatus === 'expiring') c.expiring += 1;
|
|
else if (d.expiryStatus === 'expired') c.expired += 1;
|
|
}
|
|
c.attention = c.expiring + c.expired;
|
|
return c;
|
|
}, [inCategory]);
|
|
|
|
const visible = useMemo(() => {
|
|
if (status === 'all') return inCategory;
|
|
if (status === 'attention') {
|
|
return inCategory.filter((d) => d.expiryStatus === 'expiring' || d.expiryStatus === 'expired');
|
|
}
|
|
return inCategory.filter((d) => d.expiryStatus === status);
|
|
}, [inCategory, status]);
|
|
|
|
const title = category ? catName(category) : t('documents.allTitle');
|
|
const subtitle = `${propertyQuery.data?.name ?? ''} · ${t('properties.docCount', { count: inCategory.length })}`;
|
|
|
|
return (
|
|
<View style={{ flex: 1, backgroundColor: colors.bgApp }}>
|
|
<AppBar
|
|
title={title}
|
|
subtitle={subtitle}
|
|
back
|
|
right={<IconButton name="search" onPress={() => router.push('/search')} />}
|
|
/>
|
|
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={styles.chips}
|
|
style={{ flexGrow: 0 }}
|
|
>
|
|
<Chip label={t('common.all')} count={counts.all} active={status === 'all'} onPress={() => setStatus('all')} />
|
|
<Chip
|
|
label={t('documents.filterAttention')}
|
|
count={counts.attention}
|
|
active={status === 'attention'}
|
|
onPress={() => setStatus('attention')}
|
|
/>
|
|
<Chip label={t('documents.filterValid')} count={counts.valid} active={status === 'valid'} onPress={() => setStatus('valid')} />
|
|
<Chip label={t('documents.filterExpiring')} count={counts.expiring} active={status === 'expiring'} onPress={() => setStatus('expiring')} />
|
|
<Chip label={t('documents.filterExpired')} count={counts.expired} active={status === 'expired'} onPress={() => setStatus('expired')} />
|
|
</ScrollView>
|
|
|
|
{docsQuery.isLoading ? (
|
|
<Loading />
|
|
) : (
|
|
<FlatList
|
|
data={visible as Document[]}
|
|
keyExtractor={(d) => String(d.id)}
|
|
contentContainerStyle={styles.list}
|
|
renderItem={({ item }) => (
|
|
<View style={{ marginBottom: spacing[3] }}>
|
|
<DocumentRow
|
|
document={item}
|
|
categoryName={catName(item.category)}
|
|
showCategory={!category}
|
|
onPress={() => router.push(`/document/${item.id}?propertyId=${pid}`)}
|
|
/>
|
|
</View>
|
|
)}
|
|
ListEmptyComponent={
|
|
<EmptyState icon="documents-outline" title={t('documents.emptyTitle')} subtitle={t('documents.emptySubtitle')} />
|
|
}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
chips: { gap: 8, paddingHorizontal: spacing[4], paddingVertical: spacing[2] },
|
|
list: { padding: spacing[4], paddingTop: spacing[2], paddingBottom: spacing[9] },
|
|
});
|