fix(veckoplan): byt-ratt-fel, navigera veckor, lagg till maltid manuellt
- Byt rätt gav 'något gick fel': limit=30 översteg schemats tak (20). Höjt taket till 50, byt rätt hämtar 40 + sök vilken rätt som helst. - Veckonavigering: pilarna byter nu VECKA (weekOffset) i stället för att bara flytta markerad dag -> går att planera nästa vecka. Titel visar veckans datumintervall; bakåt spärras före denna vecka. - Lägg till måltid: ny POST /v1/week-plans/:id/entries + knapp 'Lägg till måltid' (väljer måltidstyp -> receptväljaren i lägg-till-läge) + knapp 'Planera om veckan'. Går nu att fylla en tom dag igen. Verifierat: swap 200 (40 varierade), add-entry 201, delete 200, api 104/104.
This commit is contained in:
@@ -2,6 +2,7 @@ import type { FastifyInstance } from "fastify";
|
|||||||
import { and, desc, eq, gte, sql } from "drizzle-orm";
|
import { and, desc, eq, gte, sql } from "drizzle-orm";
|
||||||
import { schema } from "@app/database";
|
import { schema } from "@app/database";
|
||||||
import {
|
import {
|
||||||
|
addPlanEntryInputSchema,
|
||||||
generateWeekPlanInputSchema,
|
generateWeekPlanInputSchema,
|
||||||
idParamSchema,
|
idParamSchema,
|
||||||
updatePlanEntryInputSchema,
|
updatePlanEntryInputSchema,
|
||||||
@@ -178,6 +179,64 @@ export async function planningRoutes(app: FastifyInstance) {
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Lägg till en enskild måltid i planen (t.ex. på en tom dag).
|
||||||
|
app.post("/v1/week-plans/:id/entries", auth, async (req, reply) => {
|
||||||
|
const params = req.params as { id: string };
|
||||||
|
const [plan] = await app.db
|
||||||
|
.select()
|
||||||
|
.from(schema.weekPlans)
|
||||||
|
.where(eq(schema.weekPlans.id, params.id))
|
||||||
|
.limit(1);
|
||||||
|
if (!plan) throw errors.notFound("Planen finns inte.");
|
||||||
|
await requireMembership(app.db, plan.householdId, req.userId);
|
||||||
|
|
||||||
|
const input = parse(addPlanEntryInputSchema, req.body);
|
||||||
|
const [recipe] = await app.db
|
||||||
|
.select({
|
||||||
|
id: schema.recipes.id,
|
||||||
|
titleSv: schema.recipes.titleSv,
|
||||||
|
portions: schema.recipes.portions,
|
||||||
|
})
|
||||||
|
.from(schema.recipes)
|
||||||
|
.where(and(eq(schema.recipes.id, input.recipeId), eq(schema.recipes.status, "published")))
|
||||||
|
.limit(1);
|
||||||
|
if (!recipe) throw errors.notFound("Receptet finns inte.");
|
||||||
|
|
||||||
|
const sameDay = await app.db
|
||||||
|
.select({ sortOrder: schema.weekPlanEntries.sortOrder })
|
||||||
|
.from(schema.weekPlanEntries)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.weekPlanEntries.weekPlanId, params.id),
|
||||||
|
eq(schema.weekPlanEntries.date, input.date),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const nextSort = sameDay.reduce((m, e) => Math.max(m, e.sortOrder ?? 0), -1) + 1;
|
||||||
|
|
||||||
|
const [row] = await app.db
|
||||||
|
.insert(schema.weekPlanEntries)
|
||||||
|
.values({
|
||||||
|
weekPlanId: params.id,
|
||||||
|
date: input.date,
|
||||||
|
mealType: input.mealType,
|
||||||
|
recipeId: recipe.id,
|
||||||
|
titleSv: recipe.titleSv,
|
||||||
|
portions: recipe.portions ?? 2,
|
||||||
|
status: "planned",
|
||||||
|
sortOrder: nextSort,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
await emitEvent(app.db, {
|
||||||
|
type: "WEEK_PLAN_UPDATED",
|
||||||
|
payload: { weekPlanId: params.id, reason: "entry_added" },
|
||||||
|
userId: req.userId,
|
||||||
|
householdId: plan.householdId,
|
||||||
|
correlationId: req.correlationId,
|
||||||
|
});
|
||||||
|
return reply.status(201).send(row);
|
||||||
|
});
|
||||||
|
|
||||||
app.post("/v1/week-plans/:id/activate", auth, async (req) => {
|
app.post("/v1/week-plans/:id/activate", auth, async (req) => {
|
||||||
const { id } = parse(idParamSchema, req.params);
|
const { id } = parse(idParamSchema, req.params);
|
||||||
const [plan] = await app.db
|
const [plan] = await app.db
|
||||||
|
|||||||
@@ -69,21 +69,38 @@ export default function PlanScreen() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [selectedKey, setSelectedKey] = useState<string>(() => toKey(new Date()));
|
const [selectedKey, setSelectedKey] = useState<string>(() => toKey(new Date()));
|
||||||
const [actionEntry, setActionEntry] = useState<WeekPlanEntry | null>(null);
|
const [actionEntry, setActionEntry] = useState<WeekPlanEntry | null>(null);
|
||||||
|
const [addMealOpen, setAddMealOpen] = useState(false);
|
||||||
const [generating, setGenerating] = useState(false);
|
const [generating, setGenerating] = useState(false);
|
||||||
|
const [weekOffset, setWeekOffset] = useState(0); // 0 = denna vecka, 1 = nästa vecka …
|
||||||
const todayKey = toKey(new Date());
|
const todayKey = toKey(new Date());
|
||||||
|
|
||||||
const selected = useMemo(() => new Date(`${selectedKey}T00:00:00`), [selectedKey]);
|
const selected = useMemo(() => new Date(`${selectedKey}T00:00:00`), [selectedKey]);
|
||||||
// Veckoplanen startar alltid idag och går 7 dagar framåt (aldrig bakåt i tiden).
|
// Planeringsfönster: 7 dagar från veckostart (idag + veckoförskjutning).
|
||||||
const monday = useMemo(() => {
|
// Aldrig bakåt i tiden – weekOffset klampas till >= 0.
|
||||||
|
const todayMidnight = useMemo(() => {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
d.setHours(0, 0, 0, 0);
|
d.setHours(0, 0, 0, 0);
|
||||||
return d;
|
return d;
|
||||||
}, []);
|
}, []);
|
||||||
const mondayKey = toKey(monday);
|
const weekStart = useMemo(
|
||||||
const weekDays = useMemo(() => Array.from({ length: 7 }, (_, i) => addDays(monday, i)), [monday]);
|
() => addDays(todayMidnight, weekOffset * 7),
|
||||||
|
[todayMidnight, weekOffset],
|
||||||
|
);
|
||||||
|
const weekStartKey = toKey(weekStart);
|
||||||
|
const weekDays = useMemo(
|
||||||
|
() => Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)),
|
||||||
|
[weekStart],
|
||||||
|
);
|
||||||
|
// Byt vecka (framåt/bakåt). Sätter markerad dag till första dagen i veckan.
|
||||||
|
const goWeek = (delta: number) =>
|
||||||
|
setWeekOffset((o) => {
|
||||||
|
const next = Math.max(0, o + delta);
|
||||||
|
setSelectedKey(toKey(addDays(todayMidnight, next * 7)));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
// Lunch + middag varje dag som default (fler fyllda måltider), går att toggla.
|
// Lunch + middag varje dag som default (fler fyllda måltider), går att toggla.
|
||||||
const [mealSlots, setMealSlots] = useState<string[][]>(() =>
|
const [mealSlots, setMealSlots] = useState<string[][]>(() =>
|
||||||
weekDays.map(() => ["lunch", "dinner"]),
|
Array.from({ length: 7 }, () => ["lunch", "dinner"]),
|
||||||
);
|
);
|
||||||
const toggleSlot = (dayIdx: number, meal: string) =>
|
const toggleSlot = (dayIdx: number, meal: string) =>
|
||||||
setMealSlots((prev) =>
|
setMealSlots((prev) =>
|
||||||
@@ -93,8 +110,8 @@ export default function PlanScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["week-plan", mondayKey],
|
queryKey: ["week-plan", weekStartKey],
|
||||||
queryFn: () => api<WeekPlansResponse>(`/v1/week-plans?weekStartDate=${mondayKey}`),
|
queryFn: () => api<WeekPlansResponse>(`/v1/week-plans?weekStartDate=${weekStartKey}`),
|
||||||
refetchInterval: generating ? 3000 : false,
|
refetchInterval: generating ? 3000 : false,
|
||||||
});
|
});
|
||||||
const plan = query.data?.plans?.[0] ?? null;
|
const plan = query.data?.plans?.[0] ?? null;
|
||||||
@@ -129,7 +146,7 @@ export default function PlanScreen() {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [generating]);
|
}, [generating]);
|
||||||
|
|
||||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["week-plan", mondayKey] });
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["week-plan", weekStartKey] });
|
||||||
const onMutationError = (err: unknown) =>
|
const onMutationError = (err: unknown) =>
|
||||||
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error"));
|
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error"));
|
||||||
|
|
||||||
@@ -137,7 +154,7 @@ export default function PlanScreen() {
|
|||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
api("/v1/week-plans/generate", {
|
api("/v1/week-plans/generate", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: { weekStartDate: mondayKey, mealTypes: ["lunch", "dinner"], mealSlots },
|
body: { weekStartDate: weekStartKey, mealTypes: ["lunch", "dinner"], mealSlots },
|
||||||
}),
|
}),
|
||||||
onSuccess: () => setGenerating(true),
|
onSuccess: () => setGenerating(true),
|
||||||
onError: onMutationError,
|
onError: onMutationError,
|
||||||
@@ -183,9 +200,11 @@ export default function PlanScreen() {
|
|||||||
if (planId) setActionEntry(entry);
|
if (planId) setActionEntry(entry);
|
||||||
};
|
};
|
||||||
|
|
||||||
const monthLabel = cap(
|
const weekRangeLabel = `${new Intl.DateTimeFormat(locale, { day: "numeric" }).format(
|
||||||
new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(selected),
|
weekStart,
|
||||||
);
|
)}–${new Intl.DateTimeFormat(locale, { day: "numeric", month: "short" }).format(
|
||||||
|
addDays(weekStart, 6),
|
||||||
|
)}`;
|
||||||
const dayHeader = cap(
|
const dayHeader = cap(
|
||||||
new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long" }).format(
|
new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long" }).format(
|
||||||
selected,
|
selected,
|
||||||
@@ -196,13 +215,18 @@ export default function PlanScreen() {
|
|||||||
return (
|
return (
|
||||||
<Screen>
|
<Screen>
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
|
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
|
||||||
<Pressable onPress={() => setSelectedKey(toKey(addDays(selected, -7)))} hitSlop={8}>
|
<Pressable
|
||||||
|
onPress={() => goWeek(-1)}
|
||||||
|
hitSlop={8}
|
||||||
|
disabled={weekOffset === 0}
|
||||||
|
style={{ opacity: weekOffset === 0 ? 0.3 : 1 }}
|
||||||
|
>
|
||||||
<Text style={{ fontSize: 22, color: colors.textMuted, paddingHorizontal: spacing.sm }}>
|
<Text style={{ fontSize: 22, color: colors.textMuted, paddingHorizontal: spacing.sm }}>
|
||||||
‹
|
‹
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Title>{monthLabel}</Title>
|
<Title>{weekRangeLabel}</Title>
|
||||||
<Pressable onPress={() => setSelectedKey(toKey(addDays(selected, 7)))} hitSlop={8}>
|
<Pressable onPress={() => goWeek(1)} hitSlop={8}>
|
||||||
<Text style={{ fontSize: 22, color: colors.textMuted, paddingHorizontal: spacing.sm }}>
|
<Text style={{ fontSize: 22, color: colors.textMuted, paddingHorizontal: spacing.sm }}>
|
||||||
›
|
›
|
||||||
</Text>
|
</Text>
|
||||||
@@ -210,7 +234,10 @@ export default function PlanScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => setSelectedKey(todayKey)}
|
onPress={() => {
|
||||||
|
setWeekOffset(0);
|
||||||
|
setSelectedKey(todayKey);
|
||||||
|
}}
|
||||||
style={{ alignSelf: "flex-start", paddingVertical: spacing.xs }}
|
style={{ alignSelf: "flex-start", paddingVertical: spacing.xs }}
|
||||||
>
|
>
|
||||||
<Text style={{ color: colors.primary, fontSize: 13, fontWeight: "600" }}>
|
<Text style={{ color: colors.primary, fontSize: 13, fontWeight: "600" }}>
|
||||||
@@ -356,6 +383,18 @@ export default function PlanScreen() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Lägg till en måltid på vald dag (även tom dag), så länge en plan finns. */}
|
||||||
|
{planId && !generating ? (
|
||||||
|
<>
|
||||||
|
<Spacer size={spacing.xs} />
|
||||||
|
<Button
|
||||||
|
label={t("plan.addMeal")}
|
||||||
|
variant="secondary"
|
||||||
|
onPress={() => setAddMealOpen(true)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{hasEntries ? (
|
{hasEntries ? (
|
||||||
<>
|
<>
|
||||||
<Spacer size={spacing.sm} />
|
<Spacer size={spacing.sm} />
|
||||||
@@ -365,9 +404,59 @@ export default function PlanScreen() {
|
|||||||
onPress={() => planId && createList.mutate(planId)}
|
onPress={() => planId && createList.mutate(planId)}
|
||||||
loading={createList.isPending}
|
loading={createList.isPending}
|
||||||
/>
|
/>
|
||||||
|
<Spacer size={spacing.xs} />
|
||||||
|
<Button
|
||||||
|
label={t("plan.regenerate")}
|
||||||
|
variant="ghost"
|
||||||
|
onPress={() => generate.mutate()}
|
||||||
|
loading={generate.isPending}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{/* Välj måltidstyp att lägga till – navigerar till receptväljaren i "lägg till"-läge. */}
|
||||||
|
<Modal
|
||||||
|
visible={addMealOpen}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setAddMealOpen(false)}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setAddMealOpen(false)}
|
||||||
|
style={{ flex: 1, justifyContent: "flex-end", backgroundColor: "rgba(0,0,0,0.4)" }}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {}}
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
borderTopLeftRadius: radius.lg,
|
||||||
|
borderTopRightRadius: radius.lg,
|
||||||
|
padding: spacing.lg,
|
||||||
|
paddingBottom: spacing.xl,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading>{t("plan.addMeal")}</Heading>
|
||||||
|
<Small>{dayHeader}</Small>
|
||||||
|
<Spacer size={spacing.sm} />
|
||||||
|
{(["breakfast", "lunch", "dinner", "snack", "dessert"] as const).map((meal) => (
|
||||||
|
<View key={meal}>
|
||||||
|
<Button
|
||||||
|
label={mealTypeLabel(meal)}
|
||||||
|
variant="secondary"
|
||||||
|
onPress={() => {
|
||||||
|
setAddMealOpen(false);
|
||||||
|
router.push(
|
||||||
|
`/swap-meal/new?planId=${planId}&mealType=${meal}&date=${selectedKey}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Spacer size={spacing.xs} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</Pressable>
|
||||||
|
</Pressable>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Åtgärdspanel för en måltid – tryck utanför för att stänga, ingen Avbryt. */}
|
{/* Åtgärdspanel för en måltid – tryck utanför för att stänga, ingen Avbryt. */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={actionEntry !== null}
|
visible={actionEntry !== null}
|
||||||
|
|||||||
@@ -30,10 +30,17 @@ interface WhatToEatResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SwapMealScreen() {
|
export default function SwapMealScreen() {
|
||||||
const params = useLocalSearchParams<{ entryId: string; planId: string; mealType: string }>();
|
const params = useLocalSearchParams<{
|
||||||
|
entryId: string;
|
||||||
|
planId: string;
|
||||||
|
mealType: string;
|
||||||
|
date?: string;
|
||||||
|
}>();
|
||||||
const entryId = String(params.entryId);
|
const entryId = String(params.entryId);
|
||||||
const planId = String(params.planId);
|
const planId = String(params.planId);
|
||||||
const mealType = String(params.mealType || "dinner");
|
const mealType = String(params.mealType || "dinner");
|
||||||
|
const date = typeof params.date === "string" ? params.date : "";
|
||||||
|
const addMode = date.length > 0; // "lägg till" i stället för "byt ut"
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
@@ -44,7 +51,7 @@ export default function SwapMealScreen() {
|
|||||||
// Med sökning: byt till VILKEN rätt som helst (mealType filtreras då bort på
|
// Med sökning: byt till VILKEN rätt som helst (mealType filtreras då bort på
|
||||||
// servern). Utan sökning: breda förslag för slotten (view=swap, låg täckningsvikt).
|
// servern). Utan sökning: breda förslag för slotten (view=swap, låg täckningsvikt).
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const base = "/v1/recommendations/what-to-eat?limit=30&view=swap";
|
const base = "/v1/recommendations/what-to-eat?limit=40&view=swap";
|
||||||
const path = submitted
|
const path = submitted
|
||||||
? `${base}&search=${encodeURIComponent(submitted)}`
|
? `${base}&search=${encodeURIComponent(submitted)}`
|
||||||
: `${base}&mealType=${mealType}`;
|
: `${base}&mealType=${mealType}`;
|
||||||
@@ -54,7 +61,15 @@ export default function SwapMealScreen() {
|
|||||||
|
|
||||||
const swap = useMutation({
|
const swap = useMutation({
|
||||||
mutationFn: (recipeId: string) =>
|
mutationFn: (recipeId: string) =>
|
||||||
api(`/v1/week-plans/${planId}/entries/${entryId}`, { method: "PATCH", body: { recipeId } }),
|
addMode
|
||||||
|
? api(`/v1/week-plans/${planId}/entries`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { date, mealType, recipeId },
|
||||||
|
})
|
||||||
|
: api(`/v1/week-plans/${planId}/entries/${entryId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: { recipeId },
|
||||||
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ["week-plan"] });
|
void queryClient.invalidateQueries({ queryKey: ["week-plan"] });
|
||||||
router.back();
|
router.back();
|
||||||
@@ -69,8 +84,11 @@ export default function SwapMealScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Screen>
|
<Screen>
|
||||||
<Title>Byt ut måltiden</Title>
|
<Title>{addMode ? "Lägg till måltid" : "Byt ut måltiden"}</Title>
|
||||||
<Small>Välj en annan rätt för {mealLabel} – eller sök efter vilken rätt som helst.</Small>
|
<Small>
|
||||||
|
{addMode ? `Välj en rätt för ${mealLabel}` : `Välj en annan rätt för ${mealLabel}`} – eller
|
||||||
|
sök efter vilken rätt som helst.
|
||||||
|
</Small>
|
||||||
<Spacer size={spacing.sm} />
|
<Spacer size={spacing.sm} />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Sök bland alla recept …"
|
placeholder="Sök bland alla recept …"
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Ja, jeg lavede den",
|
"cookedLog.button": "Ja, jeg lavede den",
|
||||||
"cookedLog.confirm": "Træk råvarer for {count} portioner fra dit lager?",
|
"cookedLog.confirm": "Træk råvarer for {count} portioner fra dit lager?",
|
||||||
"cookedLog.doneTitle": "Registreret!",
|
"cookedLog.doneTitle": "Registreret!",
|
||||||
"cookedLog.doneBody": "Råvarerne er trukket fra dit lager, og måltidet er registreret."
|
"cookedLog.doneBody": "Råvarerne er trukket fra dit lager, og måltidet er registreret.",
|
||||||
|
"plan.addMeal": "Tilføj et måltid",
|
||||||
|
"plan.regenerate": "Planlæg ugen igen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Ja, ich habe es gekocht",
|
"cookedLog.button": "Ja, ich habe es gekocht",
|
||||||
"cookedLog.confirm": "Zutaten für {count} Portionen von deinem Vorrat abziehen?",
|
"cookedLog.confirm": "Zutaten für {count} Portionen von deinem Vorrat abziehen?",
|
||||||
"cookedLog.doneTitle": "Erfasst!",
|
"cookedLog.doneTitle": "Erfasst!",
|
||||||
"cookedLog.doneBody": "Die Zutaten wurden von deinem Vorrat abgezogen und die Mahlzeit ist erfasst."
|
"cookedLog.doneBody": "Die Zutaten wurden von deinem Vorrat abgezogen und die Mahlzeit ist erfasst.",
|
||||||
|
"plan.addMeal": "Mahlzeit hinzufügen",
|
||||||
|
"plan.regenerate": "Woche neu planen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Yes, I cooked it",
|
"cookedLog.button": "Yes, I cooked it",
|
||||||
"cookedLog.confirm": "Deduct ingredients for {count} servings from your stock?",
|
"cookedLog.confirm": "Deduct ingredients for {count} servings from your stock?",
|
||||||
"cookedLog.doneTitle": "Logged!",
|
"cookedLog.doneTitle": "Logged!",
|
||||||
"cookedLog.doneBody": "The ingredients have been deducted from your stock and the meal is logged."
|
"cookedLog.doneBody": "The ingredients have been deducted from your stock and the meal is logged.",
|
||||||
|
"plan.addMeal": "Add a meal",
|
||||||
|
"plan.regenerate": "Re-plan the week"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Sí, lo cociné",
|
"cookedLog.button": "Sí, lo cociné",
|
||||||
"cookedLog.confirm": "¿Descontar ingredientes para {count} raciones de tu despensa?",
|
"cookedLog.confirm": "¿Descontar ingredientes para {count} raciones de tu despensa?",
|
||||||
"cookedLog.doneTitle": "¡Registrado!",
|
"cookedLog.doneTitle": "¡Registrado!",
|
||||||
"cookedLog.doneBody": "Los ingredientes se han descontado de tu despensa y la comida queda registrada."
|
"cookedLog.doneBody": "Los ingredientes se han descontado de tu despensa y la comida queda registrada.",
|
||||||
|
"plan.addMeal": "Añadir una comida",
|
||||||
|
"plan.regenerate": "Replanificar la semana"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Kyllä, tein sen",
|
"cookedLog.button": "Kyllä, tein sen",
|
||||||
"cookedLog.confirm": "Vähennetäänkö ainekset {count} annokselle varastostasi?",
|
"cookedLog.confirm": "Vähennetäänkö ainekset {count} annokselle varastostasi?",
|
||||||
"cookedLog.doneTitle": "Merkitty!",
|
"cookedLog.doneTitle": "Merkitty!",
|
||||||
"cookedLog.doneBody": "Ainekset on vähennetty varastostasi ja ateria on kirjattu."
|
"cookedLog.doneBody": "Ainekset on vähennetty varastostasi ja ateria on kirjattu.",
|
||||||
|
"plan.addMeal": "Lisää ateria",
|
||||||
|
"plan.regenerate": "Suunnittele viikko uudelleen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Oui, je l'ai cuisiné",
|
"cookedLog.button": "Oui, je l'ai cuisiné",
|
||||||
"cookedLog.confirm": "Déduire les ingrédients pour {count} portions de votre stock ?",
|
"cookedLog.confirm": "Déduire les ingrédients pour {count} portions de votre stock ?",
|
||||||
"cookedLog.doneTitle": "Enregistré !",
|
"cookedLog.doneTitle": "Enregistré !",
|
||||||
"cookedLog.doneBody": "Les ingrédients ont été déduits de votre stock et le repas est enregistré."
|
"cookedLog.doneBody": "Les ingrédients ont été déduits de votre stock et le repas est enregistré.",
|
||||||
|
"plan.addMeal": "Ajouter un repas",
|
||||||
|
"plan.regenerate": "Replanifier la semaine"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Sì, l'ho cucinato",
|
"cookedLog.button": "Sì, l'ho cucinato",
|
||||||
"cookedLog.confirm": "Scalare gli ingredienti per {count} porzioni dalla tua dispensa?",
|
"cookedLog.confirm": "Scalare gli ingredienti per {count} porzioni dalla tua dispensa?",
|
||||||
"cookedLog.doneTitle": "Registrato!",
|
"cookedLog.doneTitle": "Registrato!",
|
||||||
"cookedLog.doneBody": "Gli ingredienti sono stati scalati dalla tua dispensa e il pasto è registrato."
|
"cookedLog.doneBody": "Gli ingredienti sono stati scalati dalla tua dispensa e il pasto è registrato.",
|
||||||
|
"plan.addMeal": "Aggiungi un pasto",
|
||||||
|
"plan.regenerate": "Ripianifica la settimana"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Ja, jeg laget den",
|
"cookedLog.button": "Ja, jeg laget den",
|
||||||
"cookedLog.confirm": "Trekke råvarer for {count} porsjoner fra lageret ditt?",
|
"cookedLog.confirm": "Trekke råvarer for {count} porsjoner fra lageret ditt?",
|
||||||
"cookedLog.doneTitle": "Registrert!",
|
"cookedLog.doneTitle": "Registrert!",
|
||||||
"cookedLog.doneBody": "Råvarene er trukket fra lageret ditt, og måltidet er logget."
|
"cookedLog.doneBody": "Råvarene er trukket fra lageret ditt, og måltidet er logget.",
|
||||||
|
"plan.addMeal": "Legg til et måltid",
|
||||||
|
"plan.regenerate": "Planlegg uken på nytt"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Ja, ik heb het gemaakt",
|
"cookedLog.button": "Ja, ik heb het gemaakt",
|
||||||
"cookedLog.confirm": "Ingrediënten voor {count} porties van je voorraad aftrekken?",
|
"cookedLog.confirm": "Ingrediënten voor {count} porties van je voorraad aftrekken?",
|
||||||
"cookedLog.doneTitle": "Vastgelegd!",
|
"cookedLog.doneTitle": "Vastgelegd!",
|
||||||
"cookedLog.doneBody": "De ingrediënten zijn van je voorraad afgetrokken en de maaltijd is vastgelegd."
|
"cookedLog.doneBody": "De ingrediënten zijn van je voorraad afgetrokken en de maaltijd is vastgelegd.",
|
||||||
|
"plan.addMeal": "Maaltijd toevoegen",
|
||||||
|
"plan.regenerate": "Week opnieuw plannen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -500,5 +500,7 @@
|
|||||||
"cookedLog.button": "Tak, ugotowałem",
|
"cookedLog.button": "Tak, ugotowałem",
|
||||||
"cookedLog.confirm": "Odjąć składniki na {count} porcji z Twojej spiżarni?",
|
"cookedLog.confirm": "Odjąć składniki na {count} porcji z Twojej spiżarni?",
|
||||||
"cookedLog.doneTitle": "Zapisano!",
|
"cookedLog.doneTitle": "Zapisano!",
|
||||||
"cookedLog.doneBody": "Składniki zostały odjęte z Twojej spiżarni, a posiłek zapisano."
|
"cookedLog.doneBody": "Składniki zostały odjęte z Twojej spiżarni, a posiłek zapisano.",
|
||||||
|
"plan.addMeal": "Dodaj posiłek",
|
||||||
|
"plan.regenerate": "Zaplanuj tydzień ponownie"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Sim, cozinhei",
|
"cookedLog.button": "Sim, cozinhei",
|
||||||
"cookedLog.confirm": "Descontar ingredientes para {count} porções do seu stock?",
|
"cookedLog.confirm": "Descontar ingredientes para {count} porções do seu stock?",
|
||||||
"cookedLog.doneTitle": "Registado!",
|
"cookedLog.doneTitle": "Registado!",
|
||||||
"cookedLog.doneBody": "Os ingredientes foram descontados do seu stock e a refeição fica registada."
|
"cookedLog.doneBody": "Os ingredientes foram descontados do seu stock e a refeição fica registada.",
|
||||||
|
"plan.addMeal": "Adicionar uma refeição",
|
||||||
|
"plan.regenerate": "Replanear a semana"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,5 +486,7 @@
|
|||||||
"cookedLog.button": "Ja, jag lagade den",
|
"cookedLog.button": "Ja, jag lagade den",
|
||||||
"cookedLog.confirm": "Räkna av råvaror för {count} portioner från ditt lager?",
|
"cookedLog.confirm": "Räkna av råvaror för {count} portioner från ditt lager?",
|
||||||
"cookedLog.doneTitle": "Registrerat!",
|
"cookedLog.doneTitle": "Registrerat!",
|
||||||
"cookedLog.doneBody": "Råvarorna är avräknade från ditt lager och måltiden är loggad."
|
"cookedLog.doneBody": "Råvarorna är avräknade från ditt lager och måltiden är loggad.",
|
||||||
|
"plan.addMeal": "Lägg till måltid",
|
||||||
|
"plan.regenerate": "Planera om veckan"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ export const updatePlanEntryInputSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type UpdatePlanEntryInput = z.infer<typeof updatePlanEntryInputSchema>;
|
export type UpdatePlanEntryInput = z.infer<typeof updatePlanEntryInputSchema>;
|
||||||
|
|
||||||
|
/** Lägg till en enskild måltid manuellt i en plan (t.ex. på en tom dag). */
|
||||||
|
export const addPlanEntryInputSchema = z.object({
|
||||||
|
date: dateStringSchema,
|
||||||
|
mealType: z.enum(MEAL_TYPES),
|
||||||
|
recipeId: uuidSchema,
|
||||||
|
});
|
||||||
|
export type AddPlanEntryInput = z.infer<typeof addPlanEntryInputSchema>;
|
||||||
|
|
||||||
export const weekPlanQuerySchema = z.object({
|
export const weekPlanQuerySchema = z.object({
|
||||||
weekStartDate: dateStringSchema.optional(),
|
weekStartDate: dateStringSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const whatToEatQuerySchema = z.object({
|
|||||||
/** Filtrera på en högtid/säsong (recept-holidayTag-slug, t.ex. "jul"). */
|
/** Filtrera på en högtid/säsong (recept-holidayTag-slug, t.ex. "jul"). */
|
||||||
holiday: z.string().max(60).optional(),
|
holiday: z.string().max(60).optional(),
|
||||||
includeLeftovers: z.coerce.boolean().default(true),
|
includeLeftovers: z.coerce.boolean().default(true),
|
||||||
limit: z.coerce.number().int().min(1).max(20).default(5),
|
limit: z.coerce.number().int().min(1).max(50).default(5),
|
||||||
/** Personaliseringsvy: smak, hälsa eller lager. Kräver personalization-samtycke. */
|
/** Personaliseringsvy: smak, hälsa eller lager. Kräver personalization-samtycke. */
|
||||||
view: z.enum(["default", "taste", "health", "pantry", "swap"]).default("default"),
|
view: z.enum(["default", "taste", "health", "pantry", "swap"]).default("default"),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user