* Extended functionalities
* Added different UIs
This commit is contained in:
126
meal-plan-frontend-mobile/app/(main)/(tabs)/shopping.tsx
Normal file
126
meal-plan-frontend-mobile/app/(main)/(tabs)/shopping.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as Print from 'expo-print';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { fetchShoppingList } from '@/src/api/meal-plan.api';
|
||||
import { Button } from '@/src/components/Button';
|
||||
import { ErrorView } from '@/src/components/ErrorView';
|
||||
import { Screen } from '@/src/components/Screen';
|
||||
import { useAppearance } from '@/src/context/AppearanceContext';
|
||||
import { addDays, formatDisplayDate, weekRange } from '@/src/utils/date';
|
||||
import { buildShoppingListHtml, formatShoppingQuantity } from '@/src/utils/pdfExport';
|
||||
|
||||
export default function ShoppingScreen() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { colors } = useAppearance();
|
||||
const [weekAnchor, setWeekAnchor] = useState(() => weekRange().from);
|
||||
const { from, to } = useMemo(() => weekRange(weekAnchor), [weekAnchor]);
|
||||
const [checked, setChecked] = useState<Record<string, boolean>>({});
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: ['shopping-list', from, to],
|
||||
queryFn: () => fetchShoppingList(from, to),
|
||||
});
|
||||
|
||||
async function handleExportPdf() {
|
||||
const items = listQuery.data ?? [];
|
||||
if (items.length === 0) {
|
||||
Alert.alert(t('errors.pdfNothingToExport'));
|
||||
return;
|
||||
}
|
||||
setExporting(true);
|
||||
try {
|
||||
const html = buildShoppingListHtml(
|
||||
items,
|
||||
t('pdf.shoppingList'),
|
||||
t('pdf.generatedOn', { date: new Date().toLocaleDateString(), count: items.length }),
|
||||
t('pdf.toTaste'),
|
||||
);
|
||||
const { uri } = await Print.printToFileAsync({ html });
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(uri, { UTI: '.pdf', mimeType: 'application/pdf' });
|
||||
}
|
||||
} catch {
|
||||
Alert.alert(t('errors.pdfGenerateFailed'));
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen title={t('shopping.title')} subtitle={t('shopping.subtitle')}>
|
||||
<View style={styles.header}>
|
||||
<Pressable
|
||||
onPress={() => setWeekAnchor(addDays(from, -7))}
|
||||
style={[styles.navBtn, { borderColor: colors.border, backgroundColor: colors.surface }]}
|
||||
>
|
||||
<Text style={{ color: colors.text }}>‹</Text>
|
||||
</Pressable>
|
||||
<Text style={{ color: colors.text, fontWeight: '700', flex: 1, textAlign: 'center' }}>
|
||||
{formatDisplayDate(from, i18n.language)} – {formatDisplayDate(to, i18n.language)}
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() => setWeekAnchor(addDays(from, 7))}
|
||||
style={[styles.navBtn, { borderColor: colors.border, backgroundColor: colors.surface }]}
|
||||
>
|
||||
<Text style={{ color: colors.text }}>›</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={exporting ? t('pdf.generating') : t('pdf.generate')}
|
||||
variant="secondary"
|
||||
loading={exporting}
|
||||
onPress={() => void handleExportPdf()}
|
||||
/>
|
||||
|
||||
{listQuery.isLoading ? (
|
||||
<Text style={{ color: colors.textMuted }}>{t('common.loading')}</Text>
|
||||
) : listQuery.isError ? (
|
||||
<ErrorView message={t('errors.loadShoppingFailed')} onRetry={() => listQuery.refetch()} />
|
||||
) : (listQuery.data?.length ?? 0) === 0 ? (
|
||||
<Text style={{ color: colors.textMuted }}>{t('shopping.empty')}</Text>
|
||||
) : (
|
||||
<ScrollView contentContainerStyle={styles.list}>
|
||||
{listQuery.data!.map((item) => {
|
||||
const key = item.name;
|
||||
const isChecked = checked[key] ?? false;
|
||||
return (
|
||||
<Pressable
|
||||
key={key}
|
||||
onPress={() => setChecked((prev) => ({ ...prev, [key]: !isChecked }))}
|
||||
style={[styles.row, { borderColor: colors.border, backgroundColor: colors.surface }]}
|
||||
>
|
||||
<View style={[styles.checkbox, { borderColor: colors.brand, backgroundColor: isChecked ? colors.brand : 'transparent' }]}>
|
||||
{isChecked ? <Text style={{ color: '#fff', fontSize: 12 }}>✓</Text> : null}
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ color: isChecked ? colors.textMuted : colors.text, textDecorationLine: isChecked ? 'line-through' : 'none', fontWeight: '600' }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.breakdown.length > 0 ? (
|
||||
<Text style={{ color: colors.textMuted, fontSize: 11 }}>{item.breakdown.join(' · ')}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<Text style={{ color: colors.textMuted, fontSize: 13 }}>
|
||||
{formatShoppingQuantity(item, t('pdf.toTaste'))}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
)}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8 },
|
||||
navBtn: { borderWidth: 1, borderRadius: 8, width: 36, height: 36, alignItems: 'center', justifyContent: 'center' },
|
||||
list: { gap: 8, paddingBottom: 24 },
|
||||
row: { flexDirection: 'row', alignItems: 'center', gap: 10, borderWidth: 1, borderRadius: 12, padding: 12 },
|
||||
checkbox: { width: 22, height: 22, borderRadius: 6, borderWidth: 2, alignItems: 'center', justifyContent: 'center' },
|
||||
});
|
||||
Reference in New Issue
Block a user