fix(veckoplan+streckkod): byt ratt, planera om, variation, man-son + fota fram/bak
Veckoplan/kalender: - Byt ratt: PATCH satter nu titeln (och portioner/status) efter det nya receptet, inte bara recipeId - annars visade kalendern kvar gamla ratten. - Planera om veckan: ersatter nu veckans plan (raderar tidigare + entries via kaskad) sa man aldrig far dubbletter och alltid ser den nya planen. Nytt plan-id ger ocksa ny variation. - Variation: liten deterministisk jitter per recept, seedad pa plan-id + veckostart, sa olika veckor OCH omgenereringar blir olika. Tackning/utgang dominerar fortfarande. Skalas av varietyLevel. - Veckoplaneraren visar alltid mandag-sondag (mondayOf), oavsett dagens dag. Streckkod (fota okand produkt): - Fotar nu fram- OCH baksida (multi-foto) sa AI:n far bade namn (fram) och naring/innehall (bak). Prompt uppdaterad for flera bilder. - Worker uppdaterar en befintlig delad produkt aven utan nytt namn (behaller namnet, fyller pa naring/innehall/allergener) - och sparar ny produkt aven fran baksidesfoto (fallback-namn) sa data inte tappas. - Produktpanelen visar 'Fota baksidan (naring & innehall)' nar info saknas, sa man kan komplettera en halvfardig produkt. Fel visas som dialog utan att tappa vyn. - 5 nya i18n-nycklar x 12 sprak. Verifierat: typecheck (ai-contracts/worker/api/mobil), worker 23/23, ai-contracts 24/24, api 104/104, samt repro mot testdatabas (byt ratt satter titel; planera om ger 1 plan; 3 veckor + omgenerering varierar; delvis produkt kompletteras och behaller namnet).
This commit is contained in:
@@ -149,32 +149,41 @@ export async function processScanJob(ctx: WorkerContext, scanJobId: string): Pro
|
||||
}
|
||||
: null;
|
||||
let product: Record<string, unknown> | null = null;
|
||||
if (gtin && o.productName) {
|
||||
if (gtin) {
|
||||
const [existing] = await ctx.db
|
||||
.select()
|
||||
.from(schema.products)
|
||||
.where(and(eq(schema.products.gtin, gtin), isNull(schema.products.validTo)))
|
||||
.limit(1);
|
||||
if (existing) {
|
||||
// Uppdatera den DELADE produkten: fyll på/uppdatera de fält fotot gav.
|
||||
// Behåll namnet om inget nytt lästes (t.ex. ett baksidesfoto som bara
|
||||
// ger näring/innehåll) – så kan man komplettera en halvfärdig produkt.
|
||||
const mergedAllergens =
|
||||
o.allergensDeclared && o.allergensDeclared.length > 0
|
||||
? o.allergensDeclared
|
||||
: existing.allergens;
|
||||
const [updated] = await ctx.db
|
||||
.update(schema.products)
|
||||
.set({
|
||||
name: o.productName,
|
||||
name: o.productName ?? existing.name,
|
||||
brand: o.brand ?? existing.brand,
|
||||
ingredientsText: o.ingredientsText ?? existing.ingredientsText,
|
||||
allergens: (o.allergensDeclared ?? existing.allergens) as never,
|
||||
allergens: mergedAllergens as never,
|
||||
nutrition: (nutrition ?? existing.nutrition) as never,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.products.id, existing.id))
|
||||
.returning();
|
||||
product = updated as Record<string, unknown>;
|
||||
} else {
|
||||
} else if (o.productName || nutrition || o.ingredientsText) {
|
||||
// Ny produkt. Helst med namn (framsida); annars fallback så datan inte
|
||||
// tappas – nästa skanning med framsida fyller i det riktiga namnet.
|
||||
const [inserted] = await ctx.db
|
||||
.insert(schema.products)
|
||||
.values({
|
||||
gtin,
|
||||
name: o.productName,
|
||||
name: o.productName ?? `Produkt ${gtin}`,
|
||||
brand: o.brand ?? null,
|
||||
ingredientsText: o.ingredientsText ?? null,
|
||||
allergens: (o.allergensDeclared ?? []) as never,
|
||||
@@ -187,9 +196,10 @@ export async function processScanJob(ctx: WorkerContext, scanJobId: string): Pro
|
||||
.returning();
|
||||
product = inserted as Record<string, unknown>;
|
||||
}
|
||||
} else if (o.productName) {
|
||||
} else if (o.productName || nutrition || o.ingredientsText) {
|
||||
// Ingen streckkod (fristående etikett) – returnera transient info, spara ej.
|
||||
product = {
|
||||
name: o.productName,
|
||||
name: o.productName ?? null,
|
||||
brand: o.brand ?? null,
|
||||
ingredientsText: o.ingredientsText ?? null,
|
||||
allergens: o.allergensDeclared ?? [],
|
||||
|
||||
@@ -158,6 +158,15 @@ export async function processGenerateWeekPlan(
|
||||
unitInfo.set(r.id, { densityGPerMl: r.densityGPerMl, gramsPerPiece: r.gramsPerPiece });
|
||||
}
|
||||
|
||||
// Variation mellan veckor OCH mellan omgenereringar: en liten deterministisk
|
||||
// "jitter" per recept, seedad på planens id + veckostart. Täckning och
|
||||
// utgångsdatum dominerar fortfarande (jitter är liten), men bland likvärdiga
|
||||
// rätter växlar urvalet så två veckor – och en "planera om" – inte blir
|
||||
// identiska. Nivån styrs av varietyLevel (kriteriet användaren valt).
|
||||
const varietyAmp =
|
||||
data.input.varietyLevel === "high" ? 0.35 : data.input.varietyLevel === "low" ? 0.1 : 0.2;
|
||||
const varietySeed = `${weekPlanId}|${data.input.weekStartDate}`;
|
||||
|
||||
const scored = candidates
|
||||
.filter((recipe) => {
|
||||
const ings = allIngredients.filter((i) => i.recipeId === recipe.id);
|
||||
@@ -198,7 +207,11 @@ export async function processGenerateWeekPlan(
|
||||
return {
|
||||
recipe,
|
||||
coverage,
|
||||
score: coverage.coverage + expiryBoost + (budgetOk ? 0 : -0.5),
|
||||
score:
|
||||
coverage.coverage +
|
||||
expiryBoost +
|
||||
(budgetOk ? 0 : -0.5) +
|
||||
varietyAmp * hashUnit(`${recipe.id}|${varietySeed}`),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.score - a.score);
|
||||
@@ -337,6 +350,16 @@ export async function processGenerateWeekPlan(
|
||||
});
|
||||
}
|
||||
|
||||
/** Deterministisk sträng-hash → [0,1). FNV-1a. Ger reproducerbar variation. */
|
||||
function hashUnit(s: string): number {
|
||||
let h = 2166136261 >>> 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h ^= s.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619) >>> 0;
|
||||
}
|
||||
return (h >>> 0) / 4294967296;
|
||||
}
|
||||
|
||||
async function defaultPortions(ctx: WorkerContext, householdId: string): Promise<number> {
|
||||
const [row] = await ctx.db
|
||||
.select({ total: sql<number>`coalesce(sum(${schema.householdMembers.portionFactor}), 2)` })
|
||||
|
||||
Reference in New Issue
Block a user