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:
@@ -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
|
await app.db
|
||||||
.update(schema.shoppingLists)
|
.delete(schema.shoppingListItems)
|
||||||
.set({ status: "completed", updatedAt: new Date() })
|
.where(
|
||||||
.where(eq(schema.shoppingLists.id, id));
|
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, {
|
await emitEvent(app.db, {
|
||||||
type: "SHOPPING_COMPLETED",
|
type: "SHOPPING_COMPLETED",
|
||||||
@@ -270,7 +289,7 @@ export async function shoppingRoutes(app: FastifyInstance) {
|
|||||||
householdId: list.householdId,
|
householdId: list.householdId,
|
||||||
correlationId: req.correlationId,
|
correlationId: req.correlationId,
|
||||||
});
|
});
|
||||||
return { ok: true, itemsAddedToInventory: added };
|
return { ok: true, itemsAddedToInventory: added, remaining };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,6 @@ export default function RecipeScreen() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{recipe.verificationStatus === "editorial" && <Tag label="✓ plattformen" tone="success" />}
|
|
||||||
</Row>
|
</Row>
|
||||||
<Body muted>{recipe.description ?? recipe.descriptionSv}</Body>
|
<Body muted>{recipe.description ?? recipe.descriptionSv}</Body>
|
||||||
{recipe.creatorDisplayName && <Small>Skapat av {recipe.creatorDisplayName}</Small>}
|
{recipe.creatorDisplayName && <Small>Skapat av {recipe.creatorDisplayName}</Small>}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
Screen,
|
Screen,
|
||||||
Small,
|
Small,
|
||||||
Spacer,
|
Spacer,
|
||||||
Tag,
|
|
||||||
} from "@/components/ui";
|
} from "@/components/ui";
|
||||||
import { spacing } from "@/lib/theme";
|
import { spacing } from "@/lib/theme";
|
||||||
|
|
||||||
@@ -164,10 +163,7 @@ export default function BrowseRecipesScreen() {
|
|||||||
onPress={() => router.push(`/recipe/${r.id}`)}
|
onPress={() => router.push(`/recipe/${r.id}`)}
|
||||||
style={{ gap: spacing.xs }}
|
style={{ gap: spacing.xs }}
|
||||||
>
|
>
|
||||||
<Row style={{ justifyContent: "space-between", alignItems: "center" }}>
|
<Heading>{r.title ?? r.titleSv}</Heading>
|
||||||
<Heading>{r.title ?? r.titleSv}</Heading>
|
|
||||||
{r.verificationStatus === "editorial" && <Tag label="✓ plattformen" tone="success" />}
|
|
||||||
</Row>
|
|
||||||
<Small>
|
<Small>
|
||||||
{[
|
{[
|
||||||
r.totalTimeMinutes != null ? `${r.totalTimeMinutes} min` : null,
|
r.totalTimeMinutes != null ? `${r.totalTimeMinutes} min` : null,
|
||||||
|
|||||||
@@ -111,6 +111,20 @@ export default function ShoppingScreen() {
|
|||||||
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")),
|
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({
|
const complete = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
api(`/v1/shopping-lists/${listId}/complete`, {
|
api(`/v1/shopping-lists/${listId}/complete`, {
|
||||||
@@ -160,8 +174,11 @@ export default function ShoppingScreen() {
|
|||||||
<Card key={section}>
|
<Card key={section}>
|
||||||
<Heading>{sectionLabel(section)}</Heading>
|
<Heading>{sectionLabel(section)}</Heading>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Pressable key={item.id} onPress={() => toggle.mutate(item)}>
|
<Row
|
||||||
<Row style={{ justifyContent: "space-between", paddingVertical: 6 }}>
|
key={item.id}
|
||||||
|
style={{ justifyContent: "space-between", alignItems: "center", paddingVertical: 6 }}
|
||||||
|
>
|
||||||
|
<Pressable style={{ flex: 1 }} onPress={() => toggle.mutate(item)}>
|
||||||
<Body>
|
<Body>
|
||||||
<Text style={{ color: item.checked ? colors.primary : colors.textMuted }}>
|
<Text style={{ color: item.checked ? colors.primary : colors.textMuted }}>
|
||||||
{item.checked ? "☑" : "☐"}
|
{item.checked ? "☑" : "☐"}
|
||||||
@@ -176,15 +193,23 @@ export default function ShoppingScreen() {
|
|||||||
{item.displayName} · {formatQuantity(item.quantity, item.unit)}
|
{item.displayName} · {formatQuantity(item.quantity, item.unit)}
|
||||||
</Text>
|
</Text>
|
||||||
</Body>
|
</Body>
|
||||||
{item.estimatedPriceMinor != null && (
|
</Pressable>
|
||||||
<Small>
|
{item.estimatedPriceMinor != null && (
|
||||||
{t("shopping.estimated", {
|
<Small>
|
||||||
amount: formatMinor(item.estimatedPriceMinor, list.data.currency),
|
{t("shopping.estimated", {
|
||||||
})}
|
amount: formatMinor(item.estimatedPriceMinor, list.data.currency),
|
||||||
</Small>
|
})}
|
||||||
)}
|
</Small>
|
||||||
</Row>
|
)}
|
||||||
</Pressable>
|
{/* 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>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user