fix(inkop+recept): delvis-kop, radera-vara, bort med plattformen-taggen

- Inkopslista /complete: kopte (markerade) varor laggs i lagret OCH tas bort
  ur listan; OMARKERADE varor blir kvar (handla i flera butiker). Listan
  arkiveras forst nar den ar tom. Tidigare markerades HELA listan completed
  sa allt forsvann.
- Ny radera-knapp (x) per vara med bekraftelse - ta bort nagot man angrat
  utan att kopa det (DELETE-endpointen fanns redan).
- Bort med '✓ plattformen'-taggen pa recept + bladdra (satt pa nastan alla
  editorial-recept och sag efterblivet ut). Namn visas fortfarande bara for
  anvandarskapade recept (creatorDisplayName).

Typecheck: api + mobil gron.
This commit is contained in:
Claude
2026-08-19 00:52:10 +00:00
parent 242e09d59b
commit fab8fa5ba9
4 changed files with 60 additions and 21 deletions
+23 -4
View File
@@ -258,10 +258,29 @@ export async function shoppingRoutes(app: FastifyInstance) {
}
}
// Ta bort de köpta (markerade) varorna de ligger nu i lagret. OMARKERADE
// varor blir kvar i listan (man kan handla i flera butiker och radera själv).
await app.db
.update(schema.shoppingLists)
.set({ status: "completed", updatedAt: new Date() })
.where(eq(schema.shoppingLists.id, id));
.delete(schema.shoppingListItems)
.where(
and(
eq(schema.shoppingListItems.shoppingListId, id),
eq(schema.shoppingListItems.checked, true),
),
);
// Markera hela listan som klar först när den är tömd; annars aktiv för fortsatt handling.
const remainingItems = await app.db
.select({ id: schema.shoppingListItems.id })
.from(schema.shoppingListItems)
.where(eq(schema.shoppingListItems.shoppingListId, id));
const remaining = remainingItems.length;
if (remaining === 0) {
await app.db
.update(schema.shoppingLists)
.set({ status: "completed", updatedAt: new Date() })
.where(eq(schema.shoppingLists.id, id));
}
await emitEvent(app.db, {
type: "SHOPPING_COMPLETED",
@@ -270,7 +289,7 @@ export async function shoppingRoutes(app: FastifyInstance) {
householdId: list.householdId,
correlationId: req.correlationId,
});
return { ok: true, itemsAddedToInventory: added };
return { ok: true, itemsAddedToInventory: added, remaining };
});
}
-1
View File
@@ -203,7 +203,6 @@ export default function RecipeScreen() {
})}
/>
)}
{recipe.verificationStatus === "editorial" && <Tag label="✓ plattformen" tone="success" />}
</Row>
<Body muted>{recipe.description ?? recipe.descriptionSv}</Body>
{recipe.creatorDisplayName && <Small>Skapat av {recipe.creatorDisplayName}</Small>}
+1 -5
View File
@@ -15,7 +15,6 @@ import {
Screen,
Small,
Spacer,
Tag,
} from "@/components/ui";
import { spacing } from "@/lib/theme";
@@ -164,10 +163,7 @@ export default function BrowseRecipesScreen() {
onPress={() => router.push(`/recipe/${r.id}`)}
style={{ gap: spacing.xs }}
>
<Row style={{ justifyContent: "space-between", alignItems: "center" }}>
<Heading>{r.title ?? r.titleSv}</Heading>
{r.verificationStatus === "editorial" && <Tag label="✓ plattformen" tone="success" />}
</Row>
<Heading>{r.title ?? r.titleSv}</Heading>
<Small>
{[
r.totalTimeMinutes != null ? `${r.totalTimeMinutes} min` : null,
+36 -11
View File
@@ -111,6 +111,20 @@ export default function ShoppingScreen() {
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")),
});
const remove = useMutation({
mutationFn: (itemId: string) =>
api(`/v1/shopping-lists/${listId}/items/${itemId}`, { method: "DELETE" }),
onSuccess: invalidate,
onError: (err) =>
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")),
});
const confirmRemove = (item: ShoppingItem) =>
Alert.alert(item.displayName, "Ta bort varan från listan?", [
{ text: t("common.cancel"), style: "cancel" },
{ text: "Ta bort", style: "destructive", onPress: () => remove.mutate(item.id) },
]);
const complete = useMutation({
mutationFn: () =>
api(`/v1/shopping-lists/${listId}/complete`, {
@@ -160,8 +174,11 @@ export default function ShoppingScreen() {
<Card key={section}>
<Heading>{sectionLabel(section)}</Heading>
{items.map((item) => (
<Pressable key={item.id} onPress={() => toggle.mutate(item)}>
<Row style={{ justifyContent: "space-between", paddingVertical: 6 }}>
<Row
key={item.id}
style={{ justifyContent: "space-between", alignItems: "center", paddingVertical: 6 }}
>
<Pressable style={{ flex: 1 }} onPress={() => toggle.mutate(item)}>
<Body>
<Text style={{ color: item.checked ? colors.primary : colors.textMuted }}>
{item.checked ? "☑" : "☐"}
@@ -176,15 +193,23 @@ export default function ShoppingScreen() {
{item.displayName} · {formatQuantity(item.quantity, item.unit)}
</Text>
</Body>
{item.estimatedPriceMinor != null && (
<Small>
{t("shopping.estimated", {
amount: formatMinor(item.estimatedPriceMinor, list.data.currency),
})}
</Small>
)}
</Row>
</Pressable>
</Pressable>
{item.estimatedPriceMinor != null && (
<Small>
{t("shopping.estimated", {
amount: formatMinor(item.estimatedPriceMinor, list.data.currency),
})}
</Small>
)}
{/* Radera en vara man ångrat (utan att köpa den). */}
<Pressable
onPress={() => confirmRemove(item)}
hitSlop={8}
style={{ paddingHorizontal: spacing.sm }}
>
<Text style={{ color: colors.textMuted, fontSize: 18 }}></Text>
</Pressable>
</Row>
))}
</Card>
))}