import { useCallback, useEffect, useMemo, useState } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
copyYesterdayMeals,
createReminder,
deleteDrink,
deleteMeal,
deleteReminder,
fetchDailySummary,
fetchDrinkCatalog,
fetchFavoriteCatalogItems,
fetchFavoriteRecipes,
fetchGoals,
fetchHistory,
fetchMealSuggestions,
fetchRecentMeals,
fetchReminders,
logDrink,
logMeal,
previewMeal,
saveGoals,
updateReminder,
} from '@/src/api/diet.api';
import { fetchIngredientCatalogItems } from '@/src/api/ingredient-catalog.api';
import { fetchRecipes } from '@/src/api/recipes.api';
import { Button } from '@/src/components/Button';
import { DateNavigator } from '@/src/components/DateNavigator';
import { ErrorView } from '@/src/components/ErrorView';
import { Input } from '@/src/components/Input';
import { Screen } from '@/src/components/Screen';
import { SimpleBarChart } from '@/src/components/SimpleBarChart';
import { useAppearance } from '@/src/context/AppearanceContext';
import {
CONSUMPTION_CATEGORIES,
DietReminderType,
REMINDER_DAYS_MASK_ALL,
todayIso,
type DietSuggestion,
type LogMealInput,
type MealPreview,
type UserDailyGoals,
} from '@/src/types/diet';
import { extractApiError } from '@/src/utils/apiError';
import { historyRange } from '@/src/utils/date';
import { ingredientDisplayName } from '@/src/utils/ingredient-catalog.util';
import { nextSort, sortBy, sortIndicator, type SortDirection } from '@/src/utils/sort';
type MealLogSortColumn = 'name' | 'calories';
type DrinkLogSortColumn = 'name' | 'volume';
type ReminderSortColumn = 'title' | 'time';
type Tab = 'today' | 'goals' | 'reminders' | 'history';
function mealMacroLine(m: {
calories?: number | null;
proteinG?: number | null;
fatG?: number | null;
carbsG?: number | null;
}): string {
if (m.calories == null) return '';
return `${m.calories} kcal · P ${Number(m.proteinG ?? 0).toFixed(1)}g · F ${Number(m.fatG ?? 0).toFixed(1)}g · C ${Number(m.carbsG ?? 0).toFixed(1)}g`;
}
function ProgressCard({
label,
consumed,
goal,
remaining,
unit,
remainingLabel,
}: {
label: string;
consumed: number;
goal?: number | null;
remaining?: number | null;
unit: string;
remainingLabel: string;
}) {
const { colors } = useAppearance();
const pct = goal && goal > 0 ? Math.min((consumed / goal) * 100, 100) : 0;
return (
{label}
{consumed}
{goal != null ? (
{' '}
/ {goal}
{unit}
) : null}
{remaining != null ? (
{remainingLabel}: {remaining}
{unit}
) : null}
{goal != null ? (
) : null}
);
}
export default function DietScreen() {
const { t, i18n } = useTranslation();
const { colors } = useAppearance();
const queryClient = useQueryClient();
const [tab, setTab] = useState('today');
const [date, setDate] = useState(todayIso());
const [mealCategory, setMealCategory] = useState(2);
const [recipeSearch, setRecipeSearch] = useState('');
const [selectedRecipeId, setSelectedRecipeId] = useState(null);
const [customMealName, setCustomMealName] = useState('');
const [portions, setPortions] = useState('1');
const [catalogItemId, setCatalogItemId] = useState(null);
const [grams, setGrams] = useState('150');
const [mealPreview, setMealPreview] = useState(null);
const [goalsForm, setGoalsForm] = useState({});
const [reminderTitle, setReminderTitle] = useState('');
const [reminderTime, setReminderTime] = useState('09:00');
const [reminderType, setReminderType] = useState(DietReminderType.Water);
const [editingReminderId, setEditingReminderId] = useState(null);
const [mealSort, setMealSort] = useState<{ column: MealLogSortColumn; direction: SortDirection }>({
column: 'name',
direction: 'asc',
});
const [drinkSort, setDrinkSort] = useState<{ column: DrinkLogSortColumn; direction: SortDirection }>({
column: 'name',
direction: 'asc',
});
const [reminderSort, setReminderSort] = useState<{ column: ReminderSortColumn; direction: SortDirection }>({
column: 'time',
direction: 'asc',
});
const summaryQuery = useQuery({
queryKey: ['diet', 'summary', date],
queryFn: () => fetchDailySummary(date),
});
const suggestionsQuery = useQuery({
queryKey: ['diet', 'suggestions', date],
queryFn: () => fetchMealSuggestions(date),
enabled: tab === 'today' && !!summaryQuery.data,
});
const catalogQuery = useQuery({
queryKey: ['diet', 'catalog'],
queryFn: fetchDrinkCatalog,
});
const ingredientItemsQuery = useQuery({
queryKey: ['ingredient-catalog', 'all'],
queryFn: () => fetchIngredientCatalogItems(),
});
const recipesQuery = useQuery({
queryKey: ['recipes', 'diet', recipeSearch],
queryFn: () => fetchRecipes({ search: recipeSearch }),
enabled: recipeSearch.trim().length >= 2,
});
const goalsQuery = useQuery({
queryKey: ['diet', 'goals'],
queryFn: fetchGoals,
enabled: tab === 'goals',
});
const remindersQuery = useQuery({
queryKey: ['diet', 'reminders'],
queryFn: fetchReminders,
enabled: tab === 'reminders',
});
const recentsQuery = useQuery({
queryKey: ['diet', 'recents'],
queryFn: () => fetchRecentMeals(8),
enabled: tab === 'today',
});
const favoriteRecipesQuery = useQuery({
queryKey: ['diet', 'favorites', 'recipes'],
queryFn: fetchFavoriteRecipes,
enabled: tab === 'today',
});
const favoriteCatalogQuery = useQuery({
queryKey: ['diet', 'favorites', 'catalog'],
queryFn: fetchFavoriteCatalogItems,
enabled: tab === 'today',
});
const historyRangeValues = historyRange(14);
const historyQuery = useQuery({
queryKey: ['diet', 'history', historyRangeValues.from, historyRangeValues.to],
queryFn: () => fetchHistory(historyRangeValues.from, historyRangeValues.to),
enabled: tab === 'history',
});
useEffect(() => {
if (goalsQuery.data) setGoalsForm(goalsQuery.data);
}, [goalsQuery.data]);
const invalidate = () => void queryClient.invalidateQueries({ queryKey: ['diet'] });
const resetMealForm = () => {
setRecipeSearch('');
setSelectedRecipeId(null);
setCustomMealName('');
setCatalogItemId(null);
setPortions('1');
setGrams('150');
setMealPreview(null);
};
const buildMealPayload = useCallback((): LogMealInput | null => {
if (selectedRecipeId) {
return {
recipeId: selectedRecipeId,
mealCategory,
logDate: date,
portions: Number(portions) || 1,
};
}
const gramsNum = Number(grams);
if (catalogItemId && gramsNum > 0) {
return { catalogItemId, grams: gramsNum, mealCategory, logDate: date };
}
if (customMealName.trim()) {
return { name: customMealName.trim(), mealCategory, logDate: date };
}
return null;
}, [catalogItemId, customMealName, date, grams, mealCategory, portions, selectedRecipeId]);
useEffect(() => {
const payload = buildMealPayload();
if (!payload) {
setMealPreview(null);
return;
}
const timer = setTimeout(() => {
void previewMeal(payload, date)
.then(setMealPreview)
.catch(() => setMealPreview(null));
}, 250);
return () => clearTimeout(timer);
}, [buildMealPayload, date]);
const logMealMutation = useMutation({
mutationFn: logMeal,
onSuccess: () => {
resetMealForm();
invalidate();
},
});
const logDrinkMutation = useMutation({ mutationFn: logDrink, onSuccess: invalidate });
const saveGoalsMutation = useMutation({ mutationFn: saveGoals, onSuccess: invalidate });
const createReminderMutation = useMutation({
mutationFn: createReminder,
onSuccess: () => {
setReminderTitle('');
setEditingReminderId(null);
void queryClient.invalidateQueries({ queryKey: ['diet', 'reminders'] });
},
});
const updateReminderMutation = useMutation({
mutationFn: ({ id, input }: { id: number; input: Parameters[1] }) => updateReminder(id, input),
onSuccess: () => {
setReminderTitle('');
setEditingReminderId(null);
void queryClient.invalidateQueries({ queryKey: ['diet', 'reminders'] });
},
});
const copyYesterdayMutation = useMutation({
mutationFn: () => copyYesterdayMeals(date),
onSuccess: () => invalidate(),
});
const deleteMealMutation = useMutation({ mutationFn: deleteMeal, onSuccess: invalidate });
const deleteDrinkMutation = useMutation({ mutationFn: deleteDrink, onSuccess: invalidate });
const deleteReminderMutation = useMutation({
mutationFn: deleteReminder,
onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['diet', 'reminders'] }),
});
const tabs: { id: Tab; label: string }[] = [
{ id: 'today', label: t('diet.tabToday') },
{ id: 'history', label: t('diet.tabHistory') },
{ id: 'goals', label: t('diet.tabGoals') },
{ id: 'reminders', label: t('diet.tabReminders') },
];
const summary = summaryQuery.data;
const suggestions = suggestionsQuery.data ?? [];
const ingredientItems = ingredientItemsQuery.data ?? [];
const suggestionName = (s: DietSuggestion) =>
i18n.language.startsWith('pl') && s.namePl ? s.namePl : s.name;
const canLogMeal = buildMealPayload() != null;
const sortedMeals = useMemo(() => {
if (!summary) return [];
const { column, direction } = mealSort;
return sortBy(summary.meals, direction, (meal) => (column === 'name' ? meal.name : meal.calories));
}, [summary, mealSort]);
const sortedDrinks = useMemo(() => {
if (!summary) return [];
const { column, direction } = drinkSort;
return sortBy(summary.drinks, direction, (drink) =>
column === 'name' ? drink.name : drink.volumeMl,
);
}, [summary, drinkSort]);
const sortedReminders = useMemo(() => {
const { column, direction } = reminderSort;
return sortBy(remindersQuery.data ?? [], direction, (reminder) =>
column === 'title' ? reminder.title : reminder.timeOfDay,
);
}, [remindersQuery.data, reminderSort]);
return (
{tabs.map(({ id, label }) => (
setTab(id)}
style={[
styles.tab,
{
backgroundColor: tab === id ? colors.brand : colors.surface,
borderColor: tab === id ? colors.brand : colors.border,
},
]}
>
{label}
))}
{tab === 'today' && (
<>
{summaryQuery.isLoading ? (
{t('common.loading')}
) : summaryQuery.isError ? (
summaryQuery.refetch()}
/>
) : summary ? (
<>
{t('diet.quickLog')}
);
}
function SortChip({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
const { colors } = useAppearance();
return (
{label}
);
}
const styles = StyleSheet.create({
scroll: { gap: 12, paddingBottom: 32 },
tabs: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
tab: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 14, paddingVertical: 8 },
section: { fontSize: 17, fontWeight: '700', marginTop: 8 },
label: { fontSize: 14, fontWeight: '600' },
chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
chip: { borderWidth: 1, borderRadius: 12, paddingHorizontal: 10, paddingVertical: 8 },
recipePick: { borderWidth: 1, borderRadius: 10, padding: 10 },
suggestionRow: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 12,
padding: 10,
gap: 8,
},
previewBox: { borderWidth: 1, borderRadius: 12, padding: 12 },
sortRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
sortChip: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 6 },
logRow: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 12,
padding: 10,
gap: 8,
},
progressCard: { borderWidth: 1, borderRadius: 14, padding: 14, gap: 6 },
progressLabel: { fontSize: 13, fontWeight: '600' },
progressValue: { fontSize: 22, fontWeight: '700' },
progressTrack: { height: 8, borderRadius: 999, overflow: 'hidden', marginTop: 4 },
progressFill: { height: '100%', borderRadius: 999 },
});