diff --git a/apps/api/src/routes/scans.ts b/apps/api/src/routes/scans.ts index 2d9de75..c5af145 100644 --- a/apps/api/src/routes/scans.ts +++ b/apps/api/src/routes/scans.ts @@ -7,6 +7,7 @@ import { confirmScanInputSchema, createScanInputSchema, idParamSchema } from "@a import { errors, parse } from "../lib/errors.js"; import { emitEvent, requireActiveHousehold } from "../lib/helpers.js"; import { consumeAiScan } from "../lib/entitlements.js"; +import { normalizeItemName } from "@app/inventory-engine"; /** * Skanningsflödet (spec §50): @@ -141,6 +142,23 @@ export async function scanRoutes(app: FastifyInstance) { const created: string[] = []; const proposals = extractProposals(job); + + // Aktiva varor i hushållet, för dubblett-hopslagning vid bekräftelse. + const activeRows = await app.db + .select({ + id: schema.inventoryItems.id, + canonicalIngredientId: schema.inventoryItems.canonicalIngredientId, + displayName: schema.inventoryItems.displayName, + }) + .from(schema.inventoryItems) + .where( + and( + eq(schema.inventoryItems.householdId, householdId), + gt(schema.inventoryItems.quantity, 0), + ), + ); + const activeItems = activeRows.map((r) => ({ ...r, norm: normalizeItemName(r.displayName) })); + for (const item of input.items) { const proposal = findProposal(proposals, item.tempId); await recordCorrection(app, job, item, proposal); @@ -150,25 +168,17 @@ export async function scanRoutes(app: FastifyInstance) { if (!locationId) throw errors.badRequest("storageLocationId saknas och ingen standardplats finns."); - // Dedup (#1): finns varan redan aktiv i hushållet? Uppdatera + hoppa över, - // så överlappande foton / omfotografering inte skapar dubletter. - const dedupCond = item.canonicalIngredientId - ? and( - eq(schema.inventoryItems.householdId, householdId), - eq(schema.inventoryItems.canonicalIngredientId, item.canonicalIngredientId), - gt(schema.inventoryItems.quantity, 0), - ) - : and( - eq(schema.inventoryItems.householdId, householdId), - isNull(schema.inventoryItems.canonicalIngredientId), - eq(schema.inventoryItems.displayName, item.displayName), - gt(schema.inventoryItems.quantity, 0), - ); - const [dupe] = await app.db - .select({ id: schema.inventoryItems.id }) - .from(schema.inventoryItems) - .where(dedupCond) - .limit(1); + // Dedup (#1): slå ihop om varan redan finns aktiv i hushållet, så att + // överlappande foton / omfotografering av samma hylla inte dubbellagras. + // Matcha på katalog-id ELLER normaliserat namn — Vision skriver sällan + // exakt samma namn två gånger ("Mjölk" vs "mjölk" vs "Mjölk 1L"). + const wantNorm = normalizeItemName(item.displayName); + const dupe = activeItems.find( + (r) => + (item.canonicalIngredientId != null && + r.canonicalIngredientId === item.canonicalIngredientId) || + r.norm === wantNorm, + ); if (dupe) { await app.db .update(schema.inventoryItems) diff --git a/apps/mobile/src/app/(tabs)/home.tsx b/apps/mobile/src/app/(tabs)/home.tsx index f123f12..2f2321d 100644 --- a/apps/mobile/src/app/(tabs)/home.tsx +++ b/apps/mobile/src/app/(tabs)/home.tsx @@ -1,7 +1,7 @@ -import { Pressable, View } from "react-native"; +import { Alert, Pressable, View } from "react-native"; import { useCallback } from "react"; import { router, useFocusEffect } from "expo-router"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { api } from "@/lib/api"; import { useAuth } from "@/lib/auth"; import { t } from "@/lib/i18n"; @@ -9,6 +9,7 @@ import * as Haptics from "expo-haptics"; import { formatMinor } from "@/lib/money"; import { Body, + Button, Card, EmptyState, ErrorView, @@ -41,6 +42,18 @@ interface BudgetSummary { } export default function HomeScreen() { + const queryClient = useQueryClient(); + const remove = useMutation({ + mutationFn: (id: string) => api(`/v1/inventory/items/${id}`, { method: "DELETE" }), + onError: (err) => + Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")), + onSettled: () => void queryClient.invalidateQueries({ queryKey: ["inventory"] }), + }); + const confirmRemove = (id: string, name: string) => + Alert.alert("Ta bort", `Ta bort ${name} ur ditt lager?`, [ + { text: t("common.cancel"), style: "cancel" }, + { text: "Ta bort", style: "destructive", onPress: () => remove.mutate(id) }, + ]); const inventory = useQuery({ queryKey: ["inventory"], queryFn: () => @@ -160,11 +173,18 @@ export default function HomeScreen() { {location} {locationItems.slice(0, 12).map((item) => ( - + {item.displayName} · {formatQuantity(item.quantity, item.unit)} - + + +