diff --git a/apps/mobile/src/app/(tabs)/scan.tsx b/apps/mobile/src/app/(tabs)/scan.tsx
index 7815998..6e1963d 100644
--- a/apps/mobile/src/app/(tabs)/scan.tsx
+++ b/apps/mobile/src/app/(tabs)/scan.tsx
@@ -89,7 +89,7 @@ export default function ScanScreen() {
await api(`/v1/scans/${created.scan.id}/start`, { method: "POST" });
// 4. Vidare till granskningsvyn som pollar tills resultatet kommer
- router.push(`/scan-review/${created.scan.id}`);
+ router.push(`${scanType === "plate" ? "/meal-review" : "/scan-review"}/${created.scan.id}`);
} catch (err) {
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error"));
} finally {
diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx
index a51e925..16a5e42 100644
--- a/apps/mobile/src/app/_layout.tsx
+++ b/apps/mobile/src/app/_layout.tsx
@@ -75,6 +75,7 @@ export default function RootLayout() {
options={{ title: "", presentation: "fullScreenModal" }}
/>
+
diff --git a/apps/mobile/src/app/meal-review/[jobId].tsx b/apps/mobile/src/app/meal-review/[jobId].tsx
new file mode 100644
index 0000000..84b2dd2
--- /dev/null
+++ b/apps/mobile/src/app/meal-review/[jobId].tsx
@@ -0,0 +1,137 @@
+import { useState } from "react";
+import { Alert } from "react-native";
+import { router, useLocalSearchParams } from "expo-router";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { api } from "@/lib/api";
+import { t } from "@/lib/i18n";
+import {
+ Body, Button, Card, ErrorView, Heading, Input,
+ LoadingView, Row, Screen, Small, Spacer, Tag, Title,
+} from "@/components/ui";
+import { spacing } from "@/lib/theme";
+
+/**
+ * Granska tallriksfoto (spec §22): AI:n uppskattar ett kalori-INTERVALL, aldrig
+ * exakt. Användaren väljer måltidstyp, justerar och loggar. Näringen sparas som
+ * uppskattning (nutritionIsEstimate) och markeras tydligt i "Min dag".
+ */
+const MEAL_TYPES = ["breakfast", "lunch", "dinner", "snack"] as const;
+
+interface MealComponent { name: string; estimatedGrams: number | null; confidence: number }
+interface ScanJob {
+ id: string; status: string; scanType: string; error: string | null;
+ result: {
+ kcalRange?: { min: number; max: number; mostLikely: number } | null;
+ components?: MealComponent[];
+ } | null;
+}
+
+export default function MealReviewScreen() {
+ const { jobId } = useLocalSearchParams<{ jobId: string }>();
+ const queryClient = useQueryClient();
+ const [mealType, setMealType] = useState("lunch");
+ const [title, setTitle] = useState("");
+ const [busy, setBusy] = useState(false);
+
+ const query = useQuery({
+ queryKey: ["scan", jobId],
+ queryFn: () => api(`/v1/scans/${jobId}`),
+ refetchInterval: (q) => {
+ const s = q.state.data?.status;
+ return s === "queued" || s === "running" ? 1500 : false;
+ },
+ });
+
+ const job = query.data;
+ const kcal = job?.result?.kcalRange ?? null;
+ const components = job?.result?.components ?? [];
+
+ const logMeal = async () => {
+ setBusy(true);
+ try {
+ await api("/v1/meals", {
+ method: "POST",
+ body: {
+ date: new Date().toISOString().slice(0, 10),
+ mealType,
+ source: "plate_photo",
+ titleSv: title.trim() || components[0]?.name || t("scan.plate"),
+ scanJobId: jobId,
+ },
+ });
+ await queryClient.invalidateQueries({ queryKey: ["day"] });
+ router.back();
+ } catch (err) {
+ Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error"));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ if (query.isLoading || job?.status === "queued" || job?.status === "running") {
+ return (
+
+
+ {t("scan.analyzing")}
+
+ );
+ }
+ if (query.isError || !job) return void query.refetch()} />;
+ if (job.status === "failed") {
+ return (
+
+ router.back()} />
+
+ );
+ }
+
+ return (
+
+ {t("scan.meal.title")}
+
+ {t("scan.meal.estimateNote")}
+
+
+
+ {t("scan.meal.calories")}
+ {kcal ? (
+ <>
+ ≈ {Math.round(kcal.mostLikely)} kcal
+ {Math.round(kcal.min)}–{Math.round(kcal.max)} kcal
+ >
+ ) : (
+ {t("scan.meal.noEstimate")}
+ )}
+
+
+ {components.length > 0 && (
+
+ {t("scan.meal.components")}
+ {components.map((c, i) => (
+
+ {c.name}
+ {c.estimatedGrams != null && {Math.round(c.estimatedGrams)} g}
+
+ ))}
+
+ )}
+
+
+ {t("logmeal.mealTypeTitle")}
+
+ {MEAL_TYPES.map((id) => (
+
+
+
+
+ {kcal &&
+ );
+}
diff --git a/apps/mobile/src/locales/da/common.json b/apps/mobile/src/locales/da/common.json
index daab968..2a48dfc 100644
--- a/apps/mobile/src/locales/da/common.json
+++ b/apps/mobile/src/locales/da/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Læs Brugervilkårene",
"consent.privacyLink": "Læs Privatlivspolitikken",
"consent.checkbox": "Jeg har læst og accepterer Brugervilkårene og Privatlivspolitikken.",
- "consent.submit": "Accepter og fortsæt"
+ "consent.submit": "Accepter og fortsæt",
+ "scan.meal.title": "Log måltid fra foto",
+ "scan.meal.estimateNote": "Estimeret ud fra dit foto – juster før du gemmer.",
+ "scan.meal.calories": "Estimerede kalorier",
+ "scan.meal.namePlaceholder": "Hvad spiste du? (valgfrit)",
+ "scan.meal.components": "Genkendt på tallerkenen",
+ "scan.meal.noEstimate": "Kunne ikke estimere kalorierne ud fra fotoet."
}
diff --git a/apps/mobile/src/locales/de/common.json b/apps/mobile/src/locales/de/common.json
index baa8f98..1a60d99 100644
--- a/apps/mobile/src/locales/de/common.json
+++ b/apps/mobile/src/locales/de/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Nutzungsbedingungen lesen",
"consent.privacyLink": "Datenschutzerklärung lesen",
"consent.checkbox": "Ich habe die Nutzungsbedingungen und die Datenschutzerklärung gelesen und akzeptiere sie.",
- "consent.submit": "Akzeptieren und fortfahren"
+ "consent.submit": "Akzeptieren und fortfahren",
+ "scan.meal.title": "Mahlzeit vom Foto loggen",
+ "scan.meal.estimateNote": "Geschätzt aus deinem Foto – vor dem Speichern anpassen.",
+ "scan.meal.calories": "Geschätzte Kalorien",
+ "scan.meal.namePlaceholder": "Was hast du gegessen? (optional)",
+ "scan.meal.components": "Auf dem Teller erkannt",
+ "scan.meal.noEstimate": "Kalorien konnten nicht aus dem Foto geschätzt werden."
}
diff --git a/apps/mobile/src/locales/en/common.json b/apps/mobile/src/locales/en/common.json
index 657e3b2..a84b0c8 100644
--- a/apps/mobile/src/locales/en/common.json
+++ b/apps/mobile/src/locales/en/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Read the Terms of Use",
"consent.privacyLink": "Read the Privacy Policy",
"consent.checkbox": "I have read and accept the Terms of Use and the Privacy Policy.",
- "consent.submit": "Accept and continue"
+ "consent.submit": "Accept and continue",
+ "scan.meal.title": "Log meal from photo",
+ "scan.meal.estimateNote": "Estimated from your photo – adjust before saving.",
+ "scan.meal.calories": "Estimated calories",
+ "scan.meal.namePlaceholder": "What did you eat? (optional)",
+ "scan.meal.components": "Recognised on the plate",
+ "scan.meal.noEstimate": "Couldn't estimate calories from the photo."
}
diff --git a/apps/mobile/src/locales/es/common.json b/apps/mobile/src/locales/es/common.json
index 3ab287e..994142b 100644
--- a/apps/mobile/src/locales/es/common.json
+++ b/apps/mobile/src/locales/es/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Leer los Términos de uso",
"consent.privacyLink": "Leer la Política de privacidad",
"consent.checkbox": "He leído y acepto los Términos de uso y la Política de privacidad.",
- "consent.submit": "Aceptar y continuar"
+ "consent.submit": "Aceptar y continuar",
+ "scan.meal.title": "Registrar comida desde la foto",
+ "scan.meal.estimateNote": "Estimado a partir de tu foto: ajústalo antes de guardar.",
+ "scan.meal.calories": "Calorías estimadas",
+ "scan.meal.namePlaceholder": "¿Qué comiste? (opcional)",
+ "scan.meal.components": "Reconocido en el plato",
+ "scan.meal.noEstimate": "No se pudieron estimar las calorías de la foto."
}
diff --git a/apps/mobile/src/locales/fi/common.json b/apps/mobile/src/locales/fi/common.json
index 9dbbaa8..b858700 100644
--- a/apps/mobile/src/locales/fi/common.json
+++ b/apps/mobile/src/locales/fi/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Lue käyttöehdot",
"consent.privacyLink": "Lue tietosuojaseloste",
"consent.checkbox": "Olen lukenut käyttöehdot ja tietosuojaselosteen ja hyväksyn ne.",
- "consent.submit": "Hyväksy ja jatka"
+ "consent.submit": "Hyväksy ja jatka",
+ "scan.meal.title": "Kirjaa ateria kuvasta",
+ "scan.meal.estimateNote": "Arvioitu kuvastasi – säädä ennen tallennusta.",
+ "scan.meal.calories": "Arvioidut kalorit",
+ "scan.meal.namePlaceholder": "Mitä söit? (valinnainen)",
+ "scan.meal.components": "Tunnistettu lautaselta",
+ "scan.meal.noEstimate": "Kaloreita ei voitu arvioida kuvasta."
}
diff --git a/apps/mobile/src/locales/fr/common.json b/apps/mobile/src/locales/fr/common.json
index b521920..e3cc279 100644
--- a/apps/mobile/src/locales/fr/common.json
+++ b/apps/mobile/src/locales/fr/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Lire les Conditions d'utilisation",
"consent.privacyLink": "Lire la Politique de confidentialité",
"consent.checkbox": "J'ai lu et j'accepte les Conditions d'utilisation et la Politique de confidentialité.",
- "consent.submit": "Accepter et continuer"
+ "consent.submit": "Accepter et continuer",
+ "scan.meal.title": "Enregistrer le repas depuis la photo",
+ "scan.meal.estimateNote": "Estimé à partir de ta photo – ajuste avant d'enregistrer.",
+ "scan.meal.calories": "Calories estimées",
+ "scan.meal.namePlaceholder": "Qu'as-tu mangé ? (facultatif)",
+ "scan.meal.components": "Reconnu dans l'assiette",
+ "scan.meal.noEstimate": "Impossible d'estimer les calories à partir de la photo."
}
diff --git a/apps/mobile/src/locales/it/common.json b/apps/mobile/src/locales/it/common.json
index c16cfd4..e7e7d60 100644
--- a/apps/mobile/src/locales/it/common.json
+++ b/apps/mobile/src/locales/it/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Leggi i Termini di servizio",
"consent.privacyLink": "Leggi l'Informativa sulla privacy",
"consent.checkbox": "Ho letto e accetto i Termini di servizio e l'Informativa sulla privacy.",
- "consent.submit": "Accetta e continua"
+ "consent.submit": "Accetta e continua",
+ "scan.meal.title": "Registra il pasto dalla foto",
+ "scan.meal.estimateNote": "Stimato dalla tua foto – modifica prima di salvare.",
+ "scan.meal.calories": "Calorie stimate",
+ "scan.meal.namePlaceholder": "Cosa hai mangiato? (facoltativo)",
+ "scan.meal.components": "Riconosciuto nel piatto",
+ "scan.meal.noEstimate": "Impossibile stimare le calorie dalla foto."
}
diff --git a/apps/mobile/src/locales/nb/common.json b/apps/mobile/src/locales/nb/common.json
index f7cc049..b1ca0e2 100644
--- a/apps/mobile/src/locales/nb/common.json
+++ b/apps/mobile/src/locales/nb/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Les Brukervilkårene",
"consent.privacyLink": "Les Personvernerklæringen",
"consent.checkbox": "Jeg har lest og godtar Brukervilkårene og Personvernerklæringen.",
- "consent.submit": "Godta og fortsett"
+ "consent.submit": "Godta og fortsett",
+ "scan.meal.title": "Logg måltid fra foto",
+ "scan.meal.estimateNote": "Estimert fra bildet ditt – juster før du lagrer.",
+ "scan.meal.calories": "Estimerte kalorier",
+ "scan.meal.namePlaceholder": "Hva spiste du? (valgfritt)",
+ "scan.meal.components": "Gjenkjent på tallerkenen",
+ "scan.meal.noEstimate": "Kunne ikke estimere kaloriene fra bildet."
}
diff --git a/apps/mobile/src/locales/nl/common.json b/apps/mobile/src/locales/nl/common.json
index 857e46d..236bd45 100644
--- a/apps/mobile/src/locales/nl/common.json
+++ b/apps/mobile/src/locales/nl/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Lees de Gebruiksvoorwaarden",
"consent.privacyLink": "Lees het Privacybeleid",
"consent.checkbox": "Ik heb de Gebruiksvoorwaarden en het Privacybeleid gelezen en ga ermee akkoord.",
- "consent.submit": "Accepteren en doorgaan"
+ "consent.submit": "Accepteren en doorgaan",
+ "scan.meal.title": "Maaltijd loggen vanaf foto",
+ "scan.meal.estimateNote": "Geschat op basis van je foto – pas aan voordat je opslaat.",
+ "scan.meal.calories": "Geschatte calorieën",
+ "scan.meal.namePlaceholder": "Wat heb je gegeten? (optioneel)",
+ "scan.meal.components": "Herkend op het bord",
+ "scan.meal.noEstimate": "Kon de calorieën niet schatten op basis van de foto."
}
diff --git a/apps/mobile/src/locales/pl/common.json b/apps/mobile/src/locales/pl/common.json
index a7f3d96..b15256a 100644
--- a/apps/mobile/src/locales/pl/common.json
+++ b/apps/mobile/src/locales/pl/common.json
@@ -409,5 +409,11 @@
"consent.termsLink": "Przeczytaj Regulamin",
"consent.privacyLink": "Przeczytaj Politykę prywatności",
"consent.checkbox": "Przeczytałem(-am) i akceptuję Regulamin oraz Politykę prywatności.",
- "consent.submit": "Akceptuję i kontynuuj"
+ "consent.submit": "Akceptuję i kontynuuj",
+ "scan.meal.title": "Zapisz posiłek ze zdjęcia",
+ "scan.meal.estimateNote": "Oszacowano na podstawie zdjęcia – dostosuj przed zapisaniem.",
+ "scan.meal.calories": "Szacowane kalorie",
+ "scan.meal.namePlaceholder": "Co zjadłeś(-aś)? (opcjonalnie)",
+ "scan.meal.components": "Rozpoznano na talerzu",
+ "scan.meal.noEstimate": "Nie udało się oszacować kalorii ze zdjęcia."
}
diff --git a/apps/mobile/src/locales/pt/common.json b/apps/mobile/src/locales/pt/common.json
index 89470a8..e28d4ca 100644
--- a/apps/mobile/src/locales/pt/common.json
+++ b/apps/mobile/src/locales/pt/common.json
@@ -395,5 +395,11 @@
"consent.termsLink": "Ler os Termos de Utilização",
"consent.privacyLink": "Ler a Política de Privacidade",
"consent.checkbox": "Li e aceito os Termos de Utilização e a Política de Privacidade.",
- "consent.submit": "Aceitar e continuar"
+ "consent.submit": "Aceitar e continuar",
+ "scan.meal.title": "Registar refeição a partir da foto",
+ "scan.meal.estimateNote": "Estimado a partir da tua foto – ajusta antes de guardar.",
+ "scan.meal.calories": "Calorias estimadas",
+ "scan.meal.namePlaceholder": "O que comeste? (opcional)",
+ "scan.meal.components": "Reconhecido no prato",
+ "scan.meal.noEstimate": "Não foi possível estimar as calorias a partir da foto."
}
diff --git a/apps/mobile/src/locales/sv/common.json b/apps/mobile/src/locales/sv/common.json
index 9e411bb..7ed5f7d 100644
--- a/apps/mobile/src/locales/sv/common.json
+++ b/apps/mobile/src/locales/sv/common.json
@@ -395,5 +395,11 @@
"cooked.leftoverLessThanBox": "Rester måste vara minst lika många som antalet matlådeportioner.",
"mealbox.undoConfirmTitle": "Ångra matlagning?",
"mealbox.undoConfirmBody": "Detta tar tillbaka lagerförbrukningen och matlådan för den här matlagningen.",
- "mealbox.undoSuccess": "Matlagningen är ångrad."
+ "mealbox.undoSuccess": "Matlagningen är ångrad.",
+ "scan.meal.title": "Logga måltid från foto",
+ "scan.meal.estimateNote": "Uppskattat från ditt foto – justera innan du sparar.",
+ "scan.meal.calories": "Uppskattade kalorier",
+ "scan.meal.namePlaceholder": "Vad åt du? (valfritt)",
+ "scan.meal.components": "Igenkänt på tallriken",
+ "scan.meal.noEstimate": "Kunde inte uppskatta kalorierna från fotot."
}