import { useEffect, useMemo, useState } from 'react'; import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { createIngredientCatalogItem, deleteIngredientCatalogItem, fetchIngredientCatalogItems, fetchIngredientCategories, updateIngredientCatalogItem, } from '@/src/api/ingredient-catalog.api'; import { Button } from '@/src/components/Button'; import { ErrorView } from '@/src/components/ErrorView'; import { Input } from '@/src/components/Input'; import { Screen } from '@/src/components/Screen'; import { useAppearance } from '@/src/context/AppearanceContext'; import type { IngredientCategory, IngredientNutritionItem, UpsertIngredientNutritionItemInput, } from '@/src/types/ingredient-catalog'; import { extractApiError } from '@/src/utils/apiError'; import { ingredientDisplayName } from '@/src/utils/ingredient-catalog.util'; import { nextSort, sortBy, sortIndicator, type SortDirection } from '@/src/utils/sort'; type IngredientSortColumn = 'name' | 'category' | 'calories' | 'protein' | 'fat' | 'carbs' | 'fiber'; const EMPTY_FORM: UpsertIngredientNutritionItemInput = { categoryId: 0, name: '', namePl: '', caloriesPer100G: 0, proteinGPer100G: 0, fatGPer100G: 0, carbsGPer100G: 0, fiberGPer100G: null, sortOrder: 0, isActive: true, }; export default function IngredientsScreen() { const { t, i18n } = useTranslation(); const { colors } = useAppearance(); const queryClient = useQueryClient(); const [category, setCategory] = useState(''); const [search, setSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState({ ...EMPTY_FORM }); const [formError, setFormError] = useState(null); const [sort, setSort] = useState<{ column: IngredientSortColumn; direction: SortDirection }>({ column: 'name', direction: 'asc', }); useEffect(() => { const timer = setTimeout(() => setDebouncedSearch(search.trim()), 300); return () => clearTimeout(timer); }, [search]); const categoriesQuery = useQuery({ queryKey: ['ingredient-categories'], queryFn: fetchIngredientCategories, }); const itemsQuery = useQuery({ queryKey: ['ingredient-catalog', category, debouncedSearch], queryFn: () => fetchIngredientCatalogItems({ category: category || undefined, search: debouncedSearch || undefined, }), }); const saveMutation = useMutation({ mutationFn: async () => { const payload = { ...form, name: form.name.trim(), namePl: form.namePl?.trim() || null, fiberGPer100G: form.fiberGPer100G ?? null, }; if (editingId) return updateIngredientCatalogItem(editingId, payload); return createIngredientCatalogItem(payload); }, onSuccess: () => { setShowForm(false); setEditingId(null); setFormError(null); void queryClient.invalidateQueries({ queryKey: ['ingredient-categories'] }); void queryClient.invalidateQueries({ queryKey: ['ingredient-catalog'] }); }, onError: (err) => { setFormError(extractApiError(err, t('errors.saveIngredientFailed'))); }, }); const deleteMutation = useMutation({ mutationFn: deleteIngredientCatalogItem, onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['ingredient-categories'] }); void queryClient.invalidateQueries({ queryKey: ['ingredient-catalog'] }); }, }); const categoryLabel = (code: string, fallback: string) => t(`ingredientCatalog.categories.${code}`, { defaultValue: fallback }); const totalCount = useMemo( () => (categoriesQuery.data ?? []).reduce((sum, c) => sum + c.itemCount, 0), [categoriesQuery.data], ); const openCreateForm = () => { setEditingId(null); setForm({ ...EMPTY_FORM, categoryId: categoriesQuery.data?.[0]?.id ?? 0 }); setFormError(null); setShowForm(true); }; const openEditForm = (item: IngredientNutritionItem) => { setEditingId(item.id); setForm({ categoryId: item.categoryId, name: item.name, namePl: item.namePl ?? '', caloriesPer100G: item.caloriesPer100G, proteinGPer100G: item.proteinGPer100G, fatGPer100G: item.fatGPer100G, carbsGPer100G: item.carbsGPer100G, fiberGPer100G: item.fiberGPer100G, sortOrder: 0, isActive: true, }); setFormError(null); setShowForm(true); }; const saveForm = () => { if (!form.categoryId || !form.name.trim()) { setFormError(t('ingredientCatalog.formInvalid')); return; } saveMutation.mutate(); }; const deleteItem = (id: number) => { Alert.alert(t('common.delete'), t('ingredientCatalog.deleteConfirm'), [ { text: t('common.cancel'), style: 'cancel' }, { text: t('common.delete'), style: 'destructive', onPress: () => deleteMutation.mutate(id) }, ]); }; const loading = categoriesQuery.isLoading || itemsQuery.isLoading; const error = categoriesQuery.error ?? itemsQuery.error; const items = itemsQuery.data ?? []; const sortedItems = useMemo(() => { const { column, direction } = sort; return sortBy(items, direction, (item) => { switch (column) { case 'name': return ingredientDisplayName(item, i18n.language); case 'category': return item.categoryCode; case 'calories': return item.caloriesPer100G; case 'protein': return Number(item.proteinGPer100G); case 'fat': return Number(item.fatGPer100G); case 'carbs': return Number(item.carbsGPer100G); case 'fiber': return item.fiberGPer100G != null ? Number(item.fiberGPer100G) : null; } }); }, [items, sort, i18n.language]); const toggleSort = (column: IngredientSortColumn) => { setSort((current) => nextSort(current, column)); }; return (