Fas 3 steg 3b: minimala efterfrågor + hushållsantagandeprofiler per ingrediens
This commit is contained in:
@@ -26,6 +26,7 @@ export interface CompleteCookingResult {
|
||||
mealIds: string[];
|
||||
mealBoxId: string | null;
|
||||
inventoryDeductions: Array<{ itemId: string; quantity: number; unit: string; name: string }>;
|
||||
recipeIngredients: Array<{ canonicalIngredientId: string; displayName: string; quantity: number; unit: string; optional: boolean }>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,5 +252,17 @@ export async function completeCookingSessionCore(
|
||||
await markMilestone(app.db, householdId, "inventoryUpdatedAfterCookingAt");
|
||||
}
|
||||
|
||||
return { ok: true, mealIds, mealBoxId, inventoryDeductions: deductions };
|
||||
return {
|
||||
ok: true,
|
||||
mealIds,
|
||||
mealBoxId,
|
||||
inventoryDeductions: deductions,
|
||||
recipeIngredients: recipe.ingredients.map((ing) => ({
|
||||
canonicalIngredientId: ing.canonicalIngredientId,
|
||||
displayName: ing.displayNameSv,
|
||||
quantity: ing.quantity,
|
||||
unit: ing.unit,
|
||||
optional: ing.optional,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "@app/analytics";
|
||||
import { trackProductAnalytics } from "../lib/helpers.js";
|
||||
import { completeCookingSessionCore } from "../lib/cooking.js";
|
||||
import { updateCookingAssumptionProfile } from "@app/inventory-engine";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
@@ -137,6 +138,7 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
|
||||
/**
|
||||
* Complete en session.
|
||||
* I steg 3a: anropar samma logik som gamla /cook, men länkar allt till cookingSessionId.
|
||||
* I steg 3b: sparar svar på max 2–3 frågor och uppdaterar hushållsantagandeprofiler.
|
||||
*/
|
||||
app.post("/v1/cooking-sessions/:id/complete", auth, async (req) => {
|
||||
const { id } = parse(idParamSchema, req.params);
|
||||
@@ -146,7 +148,83 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
|
||||
throw errors.conflict("Sessionen måste vara startad för att avslutas.");
|
||||
}
|
||||
|
||||
const result = await completeCookingSessionCore(app, session, req.userId, input, req.correlationId);
|
||||
const mealBoxPortions = input.mealBoxPortions ?? 0;
|
||||
const plannedPortions = input.portionsCooked ?? session.plannedPortions;
|
||||
const actualPortionsEaten = input.actualPortionsEaten ?? Math.max(0, plannedPortions - mealBoxPortions);
|
||||
const leftoverEstimatePortions = input.leftoverEstimatePortions ?? mealBoxPortions;
|
||||
|
||||
if (actualPortionsEaten + leftoverEstimatePortions > plannedPortions) {
|
||||
throw errors.badRequest("Åtna portioner + rester får inte överstiga totalt antal portioner.");
|
||||
}
|
||||
|
||||
const result = await completeCookingSessionCore(
|
||||
app,
|
||||
session,
|
||||
req.userId,
|
||||
{ ...input, portionsCooked: plannedPortions, mealBoxPortions },
|
||||
req.correlationId,
|
||||
);
|
||||
|
||||
await app.db
|
||||
.update(schema.cookingSessions)
|
||||
.set({
|
||||
actualPortionsEaten,
|
||||
leftoverEstimatePortions,
|
||||
leftoverNote: input.leftoverNote ?? null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.cookingSessions.id, id));
|
||||
|
||||
// Uppdatera antagandeprofiler per ingrediens (hushållsnivå).
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
for (const ing of result.recipeIngredients) {
|
||||
if (ing.optional) continue;
|
||||
const [existing] = await app.db
|
||||
.select()
|
||||
.from(schema.cookingAssumptionProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.cookingAssumptionProfiles.householdId, session.householdId),
|
||||
eq(schema.cookingAssumptionProfiles.canonicalIngredientId, ing.canonicalIngredientId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const updatedProfile = updateCookingAssumptionProfile(
|
||||
{
|
||||
householdId: session.householdId,
|
||||
canonicalIngredientId: ing.canonicalIngredientId,
|
||||
plannedPortions,
|
||||
actualPortionsEaten,
|
||||
leftoverEstimatePortions,
|
||||
sessionId: id,
|
||||
date,
|
||||
},
|
||||
existing ?? { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 },
|
||||
);
|
||||
|
||||
await app.db
|
||||
.insert(schema.cookingAssumptionProfiles)
|
||||
.values({
|
||||
householdId: session.householdId,
|
||||
canonicalIngredientId: ing.canonicalIngredientId,
|
||||
...updatedProfile,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
schema.cookingAssumptionProfiles.householdId,
|
||||
schema.cookingAssumptionProfiles.canonicalIngredientId,
|
||||
],
|
||||
set: {
|
||||
averageEatenPortions: updatedProfile.averageEatenPortions,
|
||||
averageLeftoverPortions: updatedProfile.averageLeftoverPortions,
|
||||
observationCount: updatedProfile.observationCount,
|
||||
lastSessionAnswers: updatedProfile.lastSessionAnswers,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [updated] = await app.db
|
||||
.select()
|
||||
@@ -162,8 +240,10 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
|
||||
properties: {
|
||||
cookingSessionId: id,
|
||||
recipeId: session.recipeId,
|
||||
portionsCooked: session.plannedPortions,
|
||||
mealBoxPortions: input.mealBoxPortions ?? 0,
|
||||
portionsCooked: plannedPortions,
|
||||
actualPortionsEaten,
|
||||
leftoverEstimatePortions,
|
||||
mealBoxPortions,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -171,6 +251,39 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
|
||||
return { ...result, session: updated };
|
||||
});
|
||||
|
||||
/** Hämta antagandeprofil för ett recept (per hushåll + ingrediens). */
|
||||
app.get("/v1/recipes/:id/cooking-assumptions", auth, async (req) => {
|
||||
const { id } = parse(idParamSchema, req.params);
|
||||
const householdId = await requireActiveHousehold(app.db, req.userId);
|
||||
await requireMembership(app.db, householdId, req.userId);
|
||||
|
||||
const ings = await app.db
|
||||
.select({ canonicalIngredientId: schema.recipeIngredients.canonicalIngredientId })
|
||||
.from(schema.recipeIngredients)
|
||||
.where(eq(schema.recipeIngredients.recipeId, id));
|
||||
if (ings.length === 0) throw errors.notFound("Receptet finns inte.");
|
||||
|
||||
const profiles = await app.db
|
||||
.select()
|
||||
.from(schema.cookingAssumptionProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.cookingAssumptionProfiles.householdId, householdId),
|
||||
eq(schema.cookingAssumptionProfiles.canonicalIngredientId, ings[0]!.canonicalIngredientId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const p = profiles[0];
|
||||
return {
|
||||
householdId,
|
||||
recipeId: id,
|
||||
defaultActualPortionsEaten: p?.averageEatenPortions ?? null,
|
||||
defaultLeftoverEstimatePortions: p?.averageLeftoverPortions ?? null,
|
||||
observationCount: p?.observationCount ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
/** Lista hushållets aktiva sessioner. */
|
||||
app.get("/v1/cooking-sessions", auth, async (req) => {
|
||||
const householdId = await requireActiveHousehold(app.db, req.userId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./setup-env.js";
|
||||
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
||||
import { eq, inArray, count } from "drizzle-orm";
|
||||
import { and, eq, inArray, count } from "drizzle-orm";
|
||||
import { buildServer } from "../src/server.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import { createDatabase, closeDatabase, schema } from "@app/database";
|
||||
@@ -43,6 +43,7 @@ describe("cooking sessions", () => {
|
||||
for (const m of memberships) {
|
||||
await testDb.db.delete(schema.inventoryItems).where(eq(schema.inventoryItems.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.storageLocations).where(eq(schema.storageLocations.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.householdId, m.householdId));
|
||||
await testDb.db.delete(schema.households).where(eq(schema.households.id, m.householdId));
|
||||
}
|
||||
@@ -234,6 +235,88 @@ describe("cooking sessions", () => {
|
||||
expect(body.mealBoxId).toBeDefined();
|
||||
});
|
||||
|
||||
it("stores actual portions and leftover estimate on complete", async () => {
|
||||
const start = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook/start`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { startNow: true, portions: 4 },
|
||||
});
|
||||
const sessionId = (JSON.parse(start.body) as { session: { id: string } }).session.id;
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/cooking-sessions/${sessionId}/complete`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { mealBoxPortions: 1, actualPortionsEaten: 2, leftoverEstimatePortions: 1 },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body) as { session: { actualPortionsEaten: number; leftoverEstimatePortions: number } };
|
||||
expect(body.session.actualPortionsEaten).toBe(2);
|
||||
expect(body.session.leftoverEstimatePortions).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects complete when eaten + leftovers exceed planned portions", async () => {
|
||||
const start = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook/start`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { startNow: true, portions: 4 },
|
||||
});
|
||||
const sessionId = (JSON.parse(start.body) as { session: { id: string } }).session.id;
|
||||
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/cooking-sessions/${sessionId}/complete`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { mealBoxPortions: 0, actualPortionsEaten: 3, leftoverEstimatePortions: 2 },
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it("updates cooking assumption profiles per household and ingredient", async () => {
|
||||
await testDb.db.delete(schema.cookingAssumptionProfiles).where(eq(schema.cookingAssumptionProfiles.householdId, householdId));
|
||||
|
||||
const start = await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/recipes/${recipeId}/cook/start`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { startNow: true, portions: 4 },
|
||||
});
|
||||
const sessionId = (JSON.parse(start.body) as { session: { id: string } }).session.id;
|
||||
|
||||
const recipe = (await app.inject({
|
||||
method: "GET",
|
||||
url: `/v1/recipes/${recipeId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})).json() as { ingredients: Array<{ canonicalIngredientId: string }> };
|
||||
const firstIngredientId = recipe.ingredients.find((i) => i.canonicalIngredientId)?.canonicalIngredientId;
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: `/v1/cooking-sessions/${sessionId}/complete`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { mealBoxPortions: 1, actualPortionsEaten: 2, leftoverEstimatePortions: 1 },
|
||||
});
|
||||
|
||||
if (firstIngredientId) {
|
||||
const profile = await testDb.db
|
||||
.select()
|
||||
.from(schema.cookingAssumptionProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.cookingAssumptionProfiles.householdId, householdId),
|
||||
eq(schema.cookingAssumptionProfiles.canonicalIngredientId, firstIngredientId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
expect(profile.length).toBe(1);
|
||||
expect(profile[0]!.observationCount).toBe(1);
|
||||
expect(profile[0]!.averageEatenPortions).toBe(2);
|
||||
expect(profile[0]!.averageLeftoverPortions).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("emits cooking_session_completed on complete and cooking_session_cancelled on cancel", async () => {
|
||||
// complete
|
||||
const startComplete = await app.inject({
|
||||
|
||||
@@ -49,6 +49,8 @@ export default function CookingScreen() {
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [finishing, setFinishing] = useState(false);
|
||||
const [mealBoxPortions, setMealBoxPortions] = useState(0);
|
||||
const [actualPortionsEaten, setActualPortionsEaten] = useState<number | null>(null);
|
||||
const [leftoverEstimatePortions, setLeftoverEstimatePortions] = useState<number | null>(null);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["recipe", id],
|
||||
@@ -100,6 +102,9 @@ export default function CookingScreen() {
|
||||
};
|
||||
|
||||
if (finishing) {
|
||||
const defaultEaten = actualPortionsEaten ?? Math.max(0, portionsCooked - mealBoxPortions);
|
||||
const defaultLeftovers = leftoverEstimatePortions ?? mealBoxPortions;
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<Heading>{t("cooked.title")}</Heading>
|
||||
@@ -127,6 +132,44 @@ export default function CookingScreen() {
|
||||
</Row>
|
||||
</Row>
|
||||
</Card>
|
||||
<Card>
|
||||
<Body>{t("cooked.actualPortionsEaten")}</Body>
|
||||
<Row>
|
||||
<Button
|
||||
label="−"
|
||||
variant="ghost"
|
||||
onPress={() => setActualPortionsEaten(Math.max(0, defaultEaten - 1))}
|
||||
/>
|
||||
<Body>{defaultEaten}</Body>
|
||||
<Button
|
||||
label="+"
|
||||
variant="ghost"
|
||||
onPress={() => setActualPortionsEaten(Math.min(portionsCooked, defaultEaten + 1))}
|
||||
/>
|
||||
</Row>
|
||||
<Small>{t("cooked.actualPortionsEatenHint")}</Small>
|
||||
</Card>
|
||||
<Card>
|
||||
<Body>{t("cooked.leftoverEstimatePortions")}</Body>
|
||||
<Row>
|
||||
<Button
|
||||
label="−"
|
||||
variant="ghost"
|
||||
onPress={() => setLeftoverEstimatePortions(Math.max(0, defaultLeftovers - 1))}
|
||||
/>
|
||||
<Body>{defaultLeftovers}</Body>
|
||||
<Button
|
||||
label="+"
|
||||
variant="ghost"
|
||||
onPress={() =>
|
||||
setLeftoverEstimatePortions(
|
||||
Math.min(portionsCooked - defaultEaten, defaultLeftovers + 1),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
<Small>{t("cooked.leftoverEstimateHint")}</Small>
|
||||
</Card>
|
||||
<Small>{t("cooked.deductPantryNote")}</Small>
|
||||
<Spacer size={spacing.sm} />
|
||||
<Button
|
||||
@@ -136,12 +179,25 @@ export default function CookingScreen() {
|
||||
cook.mutate({
|
||||
portionsCooked,
|
||||
mealBoxPortions,
|
||||
actualPortionsEaten: actualPortionsEaten ?? defaultEaten,
|
||||
leftoverEstimatePortions: leftoverEstimatePortions ?? defaultLeftovers,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
mealType: "dinner",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button label={t("common.skip")} variant="ghost" onPress={() =>
|
||||
cook.mutate({
|
||||
portionsCooked,
|
||||
mealBoxPortions,
|
||||
actualPortionsEaten: defaultEaten,
|
||||
leftoverEstimatePortions: defaultLeftovers,
|
||||
mealBoxFrozen: false,
|
||||
deductInventory: true,
|
||||
mealType: "dinner",
|
||||
})
|
||||
} />
|
||||
<Button label={t("common.back")} variant="ghost" onPress={() => setFinishing(false)} />
|
||||
</Screen>
|
||||
);
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Portioner lavet",
|
||||
"cooked.rate": "Bedøm",
|
||||
"cooked.title": "Hvordan gik det?",
|
||||
"cooked.actualPortionsEaten": "Hvor mange portioner spiste I?",
|
||||
"cooked.actualPortionsEatenHint": "Resten bliver til rester eller madpakker.",
|
||||
"cooked.leftoverEstimatePortions": "Hvor mange restportioner blev der?",
|
||||
"cooked.leftoverEstimateHint": "Gemmes som madpakke, hvis du vil.",
|
||||
"cooked.whoAte": "Hvem spiste?",
|
||||
"cooking.finish": "Færdig – registrér måltidet",
|
||||
"cooking.keepAwake": "Skærmen forbliver tændt, mens du laver mad",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Gekochte Portionen",
|
||||
"cooked.rate": "Bewerten",
|
||||
"cooked.title": "Wie ist es gelaufen?",
|
||||
"cooked.actualPortionsEaten": "Wie viele Portionen habt ihr gegessen?",
|
||||
"cooked.actualPortionsEatenHint": "Der Rest wird zu Resten oder Lunchboxen.",
|
||||
"cooked.leftoverEstimatePortions": "Wie viele Restportionen?",
|
||||
"cooked.leftoverEstimateHint": "Wird als Lunchbox gespeichert, wenn du möchtest.",
|
||||
"cooked.whoAte": "Wer hat gegessen?",
|
||||
"cooking.finish": "Fertig – Mahlzeit erfassen",
|
||||
"cooking.keepAwake": "Der Bildschirm bleibt beim Kochen an",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Servings cooked",
|
||||
"cooked.rate": "Rate it",
|
||||
"cooked.title": "How did it go?",
|
||||
"cooked.actualPortionsEaten": "How many portions did you eat?",
|
||||
"cooked.actualPortionsEatenHint": "The rest becomes leftovers or meal boxes.",
|
||||
"cooked.leftoverEstimatePortions": "How many leftover portions?",
|
||||
"cooked.leftoverEstimateHint": "Saved as a meal box if you want.",
|
||||
"cooked.whoAte": "Who ate?",
|
||||
"cooking.finish": "Done – log the meal",
|
||||
"cooking.keepAwake": "The screen stays awake while you cook",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Raciones cocinadas",
|
||||
"cooked.rate": "Valorar",
|
||||
"cooked.title": "¿Qué tal ha ido?",
|
||||
"cooked.actualPortionsEaten": "¿Cuántas raciones comisteis?",
|
||||
"cooked.actualPortionsEatenHint": "El resto se convierte en sobras o tuppers.",
|
||||
"cooked.leftoverEstimatePortions": "¿Cuántas raciones de sobras?",
|
||||
"cooked.leftoverEstimateHint": "Se guarda como tupper si quieres.",
|
||||
"cooked.whoAte": "¿Quiénes comieron?",
|
||||
"cooking.finish": "Hecho – registrar la comida",
|
||||
"cooking.keepAwake": "La pantalla permanece encendida mientras cocinas",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Valmistetut annokset",
|
||||
"cooked.rate": "Arvioi",
|
||||
"cooked.title": "Miten meni?",
|
||||
"cooked.actualPortionsEaten": "Montako annosta söitte?",
|
||||
"cooked.actualPortionsEatenHint": "Loput tulevat tähteiksi tai eväsrasioiksi.",
|
||||
"cooked.leftoverEstimatePortions": "Montako tähteannosta tuli?",
|
||||
"cooked.leftoverEstimateHint": "Tallennetaan eväsrasiaksi, jos haluat.",
|
||||
"cooked.whoAte": "Ketkä söivät?",
|
||||
"cooking.finish": "Valmis – kirjaa ateria",
|
||||
"cooking.keepAwake": "Näyttö pysyy päällä, kun kokkaat",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Portions cuisinées",
|
||||
"cooked.rate": "Noter",
|
||||
"cooked.title": "Alors, verdict ?",
|
||||
"cooked.actualPortionsEaten": "Combien de portions avez-vous mangées?",
|
||||
"cooked.actualPortionsEatenHint": "Le reste devient des restes ou des lunchboxes.",
|
||||
"cooked.leftoverEstimatePortions": "Combien de portions de restes?",
|
||||
"cooked.leftoverEstimateHint": "Enregistré comme lunchbox si vous voulez.",
|
||||
"cooked.whoAte": "Qui a mangé ?",
|
||||
"cooking.finish": "Terminé – enregistrer le repas",
|
||||
"cooking.keepAwake": "L'écran reste allumé pendant que vous cuisinez",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Porzioni cucinate",
|
||||
"cooked.rate": "Valuta",
|
||||
"cooked.title": "Com'è andata?",
|
||||
"cooked.actualPortionsEaten": "Quante porzioni avete mangiato?",
|
||||
"cooked.actualPortionsEatenHint": "Il resto diventa avanzi o contenitori.",
|
||||
"cooked.leftoverEstimatePortions": "Quante porzioni di avanzi?",
|
||||
"cooked.leftoverEstimateHint": "Salvato come contenitore se vuoi.",
|
||||
"cooked.whoAte": "Chi ha mangiato?",
|
||||
"cooking.finish": "Fatto – registra il pasto",
|
||||
"cooking.keepAwake": "Lo schermo resta acceso mentre cucini",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Porsjoner laget",
|
||||
"cooked.rate": "Vurder",
|
||||
"cooked.title": "Hvordan gikk det?",
|
||||
"cooked.actualPortionsEaten": "Hvor mange porsjoner spiste dere?",
|
||||
"cooked.actualPortionsEatenHint": "Resten blir til rester eller matbokser.",
|
||||
"cooked.leftoverEstimatePortions": "Hvor mange restporsjoner ble det?",
|
||||
"cooked.leftoverEstimateHint": "Lagres som matboks hvis du vil.",
|
||||
"cooked.whoAte": "Hvem spiste?",
|
||||
"cooking.finish": "Ferdig – loggfør måltidet",
|
||||
"cooking.keepAwake": "Skjermen holdes våken mens du lager mat",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Gekookte porties",
|
||||
"cooked.rate": "Beoordelen",
|
||||
"cooked.title": "Hoe ging het?",
|
||||
"cooked.actualPortionsEaten": "Hoeveel porties hebben jullie gegeten?",
|
||||
"cooked.actualPortionsEatenHint": "De rest wordt restjes of lunchboxen.",
|
||||
"cooked.leftoverEstimatePortions": "Hoeveel restporties?",
|
||||
"cooked.leftoverEstimateHint": "Opgeslagen als lunchbox als je wilt.",
|
||||
"cooked.whoAte": "Wie hebben er gegeten?",
|
||||
"cooking.finish": "Klaar – maaltijd registreren",
|
||||
"cooking.keepAwake": "Het scherm blijft aan tijdens het koken",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Ugotowane porcje",
|
||||
"cooked.rate": "Oceń",
|
||||
"cooked.title": "Jak poszło?",
|
||||
"cooked.actualPortionsEaten": "Ile porcji zjedliście?",
|
||||
"cooked.actualPortionsEatenHint": "Reszta stanie się resztkami lub lunchboxami.",
|
||||
"cooked.leftoverEstimatePortions": "Ile porcji resztek?",
|
||||
"cooked.leftoverEstimateHint": "Zapisane jako lunchbox, jeśli chcesz.",
|
||||
"cooked.whoAte": "Kto jadł?",
|
||||
"cooking.finish": "Gotowe – zarejestruj posiłek",
|
||||
"cooking.keepAwake": "Ekran pozostaje włączony podczas gotowania",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Doses cozinhadas",
|
||||
"cooked.rate": "Avaliar",
|
||||
"cooked.title": "Como correu?",
|
||||
"cooked.actualPortionsEaten": "Quantas porções comeram?",
|
||||
"cooked.actualPortionsEatenHint": "O resto fica como sobras ou marmitas.",
|
||||
"cooked.leftoverEstimatePortions": "Quantas porções de sobras?",
|
||||
"cooked.leftoverEstimateHint": "Guardado como marmita se quiseres.",
|
||||
"cooked.whoAte": "Quem comeu?",
|
||||
"cooking.finish": "Concluído – registar a refeição",
|
||||
"cooking.keepAwake": "O ecrã fica ligado enquanto cozinha",
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
"cooked.portionsCooked": "Portioner lagade",
|
||||
"cooked.rate": "Betygsätt",
|
||||
"cooked.title": "Hur gick det?",
|
||||
"cooked.actualPortionsEaten": "Hur många portioner åt ni?",
|
||||
"cooked.actualPortionsEatenHint": "Resten blir rester eller matlådor.",
|
||||
"cooked.leftoverEstimatePortions": "Hur många restportioner blev det?",
|
||||
"cooked.leftoverEstimateHint": "Sparas som matlåda om du vill.",
|
||||
"cooked.whoAte": "Vilka åt?",
|
||||
"cooking.finish": "Klart – logga måltiden",
|
||||
"cooking.keepAwake": "Skärmen hålls vaken medan du lagar",
|
||||
|
||||
Reference in New Issue
Block a user