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:
Claude
2026-08-20 00:54:32 +00:00
parent 9b3456b080
commit ceceb63e8e
17 changed files with 232 additions and 34 deletions
+59
View File
@@ -2,6 +2,7 @@ import type { FastifyInstance } from "fastify";
import { and, desc, eq, gte, sql } from "drizzle-orm";
import { schema } from "@app/database";
import {
addPlanEntryInputSchema,
generateWeekPlanInputSchema,
idParamSchema,
updatePlanEntryInputSchema,
@@ -178,6 +179,64 @@ export async function planningRoutes(app: FastifyInstance) {
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) => {
const { id } = parse(idParamSchema, req.params);
const [plan] = await app.db