Files
Cibello-app/packages/database/src/schema/meals.ts
T
Sven (AAMOS AI) 74f95daab2 feat(3d): meal-box merge, stored mutations, undo, mobile undo UI + i18n ×12
- Merge leftovers into existing meal_box when same recipeId + cookedAt date + frozen + available.
- Store mealBoxMutations JSONB on cooking_sessions for deterministic undo.
- Undo decrements portions/remaining, discards box at zero, appends correction ledger rows.
- Mobile: undo button in cooking/[id].tsx after-flow and meal-boxes.tsx within 24h window.
- i18n undo strings across all 12 locales; parity test green.
- 6 new integration tests: merge, no cross-date merge, frozen split, undo restore, undo discard, ledger invariant.
- Update FAS3 audit doc with 3d semantics.

Closes Fas 3d
2026-08-07 17:04:42 +07:00

89 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
boolean,
date,
doublePrecision,
index,
integer,
jsonb,
pgTable,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
import type { NutritionValues } from "@app/shared-types";
import { createdAt, mealLogSourceEnum, mealTypeEnum } from "./_shared.js";
import { households, storageLocations } from "./households.js";
import { recipes, cookingSessions } from "./recipes.js";
import { users } from "./users.js";
/**
* Måltidslogg (spec §23). Näringsvärden är alltid deterministiskt härledda
* (recept/produkt/användarinmatning) aldrig påhittade av AI (spec §61.1).
*/
export const meals = pgTable(
"meals",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
householdId: uuid("household_id").references(() => households.id, { onDelete: "set null" }),
date: date("date").notNull(),
mealType: mealTypeEnum("meal_type").notNull(),
source: mealLogSourceEnum("source").notNull(),
recipeId: uuid("recipe_id").references(() => recipes.id, { onDelete: "set null" }),
cookingSessionId: uuid("cooking_session_id").references(() => cookingSessions.id, {
onDelete: "set null",
}),
titleSv: text("title_sv").notNull(),
portionFraction: doublePrecision("portion_fraction").notNull().default(1),
nutrition: jsonb("nutrition").$type<NutritionValues>().notNull(),
nutritionIsEstimate: boolean("nutrition_is_estimate").notNull().default(false),
estimateMinKcal: doublePrecision("estimate_min_kcal"),
estimateMaxKcal: doublePrecision("estimate_max_kcal"),
items: jsonb("items"),
photoUrl: text("photo_url"),
scanJobId: uuid("scan_job_id"),
loggedAt: timestamp("logged_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index("meals_user_date_idx").on(t.userId, t.date),
index("meals_household_idx").on(t.householdId),
],
);
/** Matlådor (spec §24) rekommenderas före ny matlagning när rimligt. */
export const mealBoxes = pgTable(
"meal_boxes",
{
id: uuid("id").primaryKey().defaultRandom(),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
recipeId: uuid("recipe_id").references(() => recipes.id, { onDelete: "set null" }),
cookingSessionId: uuid("cooking_session_id").references(() => cookingSessions.id, {
onDelete: "set null",
}),
titleSv: text("title_sv").notNull(),
portions: integer("portions").notNull(),
portionsRemaining: integer("portions_remaining").notNull(),
nutritionPerPortion: jsonb("nutrition_per_portion").$type<NutritionValues>(),
cookedAt: date("cooked_at").notNull(),
storageLocationId: uuid("storage_location_id")
.notNull()
.references(() => storageLocations.id),
frozen: boolean("frozen").notNull().default(false),
recommendedUseBy: date("recommended_use_by").notNull(),
leftoverSource: text("leftover_source"),
reservedForUserId: uuid("reserved_for_user_id").references(() => users.id, {
onDelete: "set null",
}),
status: text("status").notNull().default("available"),
createdAt: createdAt(),
},
(t) => [
index("meal_boxes_household_idx").on(t.householdId),
index("meal_boxes_use_by_idx").on(t.householdId, t.recommendedUseBy),
],
);