import { useEffect, useMemo, useState } from "react"; import { Alert, Pressable, Text, View } from "react-native"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { api } from "@/lib/api"; import { getLanguageTag, t } from "@/lib/i18n"; import { colors, radius, spacing } from "@/lib/theme"; import { Body, Button, Card, EmptyState, ErrorView, LoadingView, Row, Screen, Small, Spacer, Tag, Title, } from "@/components/ui"; /** Veckoplanerare (spec §25): dagsvy + veckostrip, kopplad till /v1/week-plans. */ interface WeekPlanEntry { id: string; date: string; mealType: string; recipeId: string | null; mealBoxId: string | null; titleSv: string; portions: number; status: string; rescheduleReasonSv: string | null; sortOrder: number; } interface WeekPlan { id: string; weekStartDate: string; status: string; entries: WeekPlanEntry[]; } interface WeekPlansResponse { plans: WeekPlan[]; } const MEAL_ORDER = ["breakfast", "lunch", "dinner", "snack", "dessert"]; const mealTypeLabel = (mealType: string): string => MEAL_ORDER.includes(mealType) ? t(`myday.mealType.${mealType}`) : mealType; function toKey(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } function addDays(d: Date, n: number): Date { const copy = new Date(d); copy.setDate(copy.getDate() + n); return copy; } function startOfWeekMonday(d: Date): Date { const copy = new Date(d); const dow = (copy.getDay() + 6) % 7; copy.setDate(copy.getDate() - dow); copy.setHours(0, 0, 0, 0); return copy; } function cap(s: string): string { return s.length ? s[0]!.toUpperCase() + s.slice(1) : s; } export default function PlanScreen() { const locale = getLanguageTag(); const queryClient = useQueryClient(); const [selectedKey, setSelectedKey] = useState(() => toKey(new Date())); const [generating, setGenerating] = useState(false); const todayKey = toKey(new Date()); const selected = useMemo(() => new Date(`${selectedKey}T00:00:00`), [selectedKey]); const monday = useMemo(() => startOfWeekMonday(selected), [selected]); const mondayKey = toKey(monday); const weekDays = useMemo(() => Array.from({ length: 7 }, (_, i) => addDays(monday, i)), [monday]); const query = useQuery({ queryKey: ["week-plan", mondayKey], queryFn: () => api(`/v1/week-plans?weekStartDate=${mondayKey}`), refetchInterval: generating ? 3000 : false, }); const plan = query.data?.plans?.[0] ?? null; const planId = plan?.id ?? null; const draftPlanId = plan && plan.status === "draft" ? plan.id : null; const entriesByDate = useMemo(() => { const map: Record = {}; for (const e of plan?.entries ?? []) { (map[e.date] ??= []).push(e); } for (const list of Object.values(map)) { list.sort( (a, b) => MEAL_ORDER.indexOf(a.mealType) - MEAL_ORDER.indexOf(b.mealType) || a.sortOrder - b.sortOrder, ); } return map; }, [plan]); const selectedEntries = entriesByDate[selectedKey] ?? []; useEffect(() => { if (generating && (plan?.entries?.length ?? 0) > 0) setGenerating(false); }, [generating, plan]); useEffect(() => { if (!generating) return; const timer = setTimeout(() => setGenerating(false), 45000); return () => clearTimeout(timer); }, [generating]); const invalidate = () => queryClient.invalidateQueries({ queryKey: ["week-plan", mondayKey] }); const onMutationError = (err: unknown) => Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")); const generate = useMutation({ mutationFn: () => api("/v1/week-plans/generate", { method: "POST", body: { weekStartDate: mondayKey, mealTypes: ["lunch", "dinner"] }, }), onSuccess: () => setGenerating(true), onError: onMutationError, }); const patchEntry = useMutation({ mutationFn: (vars: { planId: string; entryId: string; status: string }) => api(`/v1/week-plans/${vars.planId}/entries/${vars.entryId}`, { method: "PATCH", body: { status: vars.status }, }), onSuccess: () => invalidate(), onError: onMutationError, }); const activate = useMutation({ mutationFn: (id: string) => api(`/v1/week-plans/${id}/activate`, { method: "POST" }), onSuccess: () => invalidate(), onError: onMutationError, }); const openActions = (entry: WeekPlanEntry) => { if (!planId) return; Alert.alert(entry.titleSv, undefined, [ { text: t("recipe.iCookedThis"), onPress: () => patchEntry.mutate({ planId, entryId: entry.id, status: "cooked" }), }, { text: t("common.skip"), onPress: () => patchEntry.mutate({ planId, entryId: entry.id, status: "skipped" }), }, { text: t("common.cancel"), style: "cancel" }, ]); }; const monthLabel = cap( new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(selected), ); const dayHeader = cap( new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long" }).format( selected, ), ); const weekdayShort = new Intl.DateTimeFormat(locale, { weekday: "short" }); return ( setSelectedKey(toKey(addDays(selected, -7)))} hitSlop={8}> {monthLabel} setSelectedKey(toKey(addDays(selected, 7)))} hitSlop={8}> setSelectedKey(todayKey)} style={{ alignSelf: "flex-start", paddingVertical: spacing.xs }} > {t("plan.today")} {weekDays.map((d) => { const key = toKey(d); const isSelected = key === selectedKey; const isToday = key === todayKey; const count = entriesByDate[key]?.length ?? 0; return ( setSelectedKey(key)} accessibilityRole="button" accessibilityState={{ selected: isSelected }} style={{ flex: 1, alignItems: "center", paddingVertical: spacing.xs, marginHorizontal: 2, borderRadius: radius.md, backgroundColor: isSelected ? colors.primary : isToday ? colors.primarySoft : "transparent", }} > {cap(weekdayShort.format(d))} {d.getDate()} {count > 0 ? "•".repeat(Math.min(count, 3)) : " "} ); })} {dayHeader} {draftPlanId ? (