feat(recept): visa vad du saknar + lagg till i inkopslistan
Receptdetaljen (GET /v1/recipes/:id) returnerar nu coverage {percent, missing}
via samma computeCoverage som rekommendationerna, sa "du har X% hemma" stammer.
Recept-skarmen visar procenten + listan pa saknade ingredienser och en
"Lagg till N i inkopslistan"-knapp (skapar lista om ingen finns). Uppfyller
loftet i explain.ts ("resten hamnar pa inkopslistan").
This commit is contained in:
@@ -56,6 +56,15 @@ interface RecipeDetail {
|
||||
}>;
|
||||
safety: { safe: boolean; violations: Array<{ severity: string; messageSv: string }> };
|
||||
variants: Array<{ id: string; titleSv: string; variantType: string }>;
|
||||
coverage: {
|
||||
percent: number;
|
||||
missing: Array<{
|
||||
canonicalIngredientId: string;
|
||||
displayNameSv: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
}>;
|
||||
} | null;
|
||||
myRating: { stars: number } | null;
|
||||
isFavorite: boolean;
|
||||
ratingAverage: number | null;
|
||||
@@ -119,6 +128,37 @@ export default function RecipeScreen() {
|
||||
onSettled: () => void queryClient.invalidateQueries({ queryKey: ["recipe", id] }),
|
||||
});
|
||||
|
||||
// Lägg de saknade ingredienserna på inköpslistan (skapar en lista om ingen finns).
|
||||
const addMissing = useMutation({
|
||||
mutationFn: async (missing: NonNullable<RecipeDetail["coverage"]>["missing"]) => {
|
||||
const result = await api<{ lists: Array<{ id: string }> }>("/v1/shopping-lists");
|
||||
let listId = result.lists[0]?.id;
|
||||
if (!listId) {
|
||||
const created = await api<{ list: { id: string } }>("/v1/shopping-lists", {
|
||||
method: "POST",
|
||||
body: { name: t("shopping.title") },
|
||||
});
|
||||
listId = created.list.id;
|
||||
}
|
||||
for (const m of missing) {
|
||||
await api(`/v1/shopping-lists/${listId}/items`, {
|
||||
method: "POST",
|
||||
body: { displayName: m.displayNameSv },
|
||||
});
|
||||
}
|
||||
return missing.length;
|
||||
},
|
||||
onSuccess: (count) => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["shopping-lists"] });
|
||||
Alert.alert(
|
||||
"Tillagt i inköpslistan",
|
||||
`${count} ${count === 1 ? "vara" : "varor"} lades till.`,
|
||||
);
|
||||
},
|
||||
onError: (err) =>
|
||||
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")),
|
||||
});
|
||||
|
||||
if (query.isLoading) return <LoadingView />;
|
||||
if (query.isError || !query.data) return <ErrorView onRetry={() => void query.refetch()} />;
|
||||
const recipe = query.data;
|
||||
@@ -177,6 +217,33 @@ export default function RecipeScreen() {
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{recipe.coverage && (
|
||||
<Card>
|
||||
<Row style={{ justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Heading>Du har {recipe.coverage.percent}% hemma</Heading>
|
||||
{recipe.coverage.missing.length > 0 && (
|
||||
<Tag label={`Saknar ${recipe.coverage.missing.length}`} tone="warning" />
|
||||
)}
|
||||
</Row>
|
||||
{recipe.coverage.missing.length === 0 ? (
|
||||
<Small>Du har allt som behövs.</Small>
|
||||
) : (
|
||||
<>
|
||||
<Small>
|
||||
Du saknar: {recipe.coverage.missing.map((m) => m.displayNameSv).join(", ")}
|
||||
</Small>
|
||||
<Spacer size={spacing.xs} />
|
||||
<Button
|
||||
label={`Lägg till ${recipe.coverage.missing.length} i inköpslistan`}
|
||||
variant="primary"
|
||||
loading={addMissing.isPending}
|
||||
onPress={() => addMissing.mutate(recipe.coverage!.missing)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<Row style={{ justifyContent: "space-between" }}>
|
||||
<Heading>{t("recipe.ingredients")}</Heading>
|
||||
|
||||
Reference in New Issue
Block a user