feat(mobile): veckoplan – markera lagad/hoppa över (smart omplanering) + aktivera plan
CI / Typecheck, test & build (push) Successful in 1m48s
CI / Typecheck, test & build (push) Successful in 1m48s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Alert, Pressable, Text, View } from "react-native";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api } from "@/lib/api";
|
||||
import { getLanguageTag, t } from "@/lib/i18n";
|
||||
import { colors, radius, spacing } from "@/lib/theme";
|
||||
@@ -71,6 +71,7 @@ function cap(s: string): string {
|
||||
|
||||
export default function PlanScreen() {
|
||||
const locale = getLanguageTag();
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedKey, setSelectedKey] = useState<string>(() => toKey(new Date()));
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const todayKey = toKey(new Date());
|
||||
@@ -86,6 +87,8 @@ export default function PlanScreen() {
|
||||
refetchInterval: generating ? 3000 : false,
|
||||
});
|
||||
const plan = query.data?.plans?.[0] ?? null;
|
||||
const planId = plan?.id ?? null;
|
||||
const draftPlanId = plan && plan.status === "draft" ? plan.id : null;
|
||||
|
||||
const entriesByDate = useMemo(() => {
|
||||
const map: Record<string, WeekPlanEntry[]> = {};
|
||||
@@ -114,6 +117,10 @@ export default function PlanScreen() {
|
||||
return () => clearTimeout(timer);
|
||||
}, [generating]);
|
||||
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["week-plan", mondayKey] });
|
||||
const onMutationError = (err: unknown) =>
|
||||
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error"));
|
||||
|
||||
const generate = useMutation({
|
||||
mutationFn: () =>
|
||||
api("/v1/week-plans/generate", {
|
||||
@@ -121,10 +128,40 @@ export default function PlanScreen() {
|
||||
body: { weekStartDate: mondayKey, mealTypes: ["lunch", "dinner"] },
|
||||
}),
|
||||
onSuccess: () => setGenerating(true),
|
||||
onError: (err) =>
|
||||
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")),
|
||||
onError: onMutationError,
|
||||
});
|
||||
|
||||
const patchEntry = useMutation({
|
||||
mutationFn: (vars: { planId: string; entryId: string; status: string }) =>
|
||||
api(`/v1/week-plans/${vars.planId}/entries/${vars.entryId}`, {
|
||||
method: "PATCH",
|
||||
body: { status: vars.status },
|
||||
}),
|
||||
onSuccess: () => invalidate(),
|
||||
onError: onMutationError,
|
||||
});
|
||||
|
||||
const activate = useMutation({
|
||||
mutationFn: (id: string) => api(`/v1/week-plans/${id}/activate`, { method: "POST" }),
|
||||
onSuccess: () => invalidate(),
|
||||
onError: onMutationError,
|
||||
});
|
||||
|
||||
const openActions = (entry: WeekPlanEntry) => {
|
||||
if (!planId) return;
|
||||
Alert.alert(entry.titleSv, undefined, [
|
||||
{
|
||||
text: t("recipe.iCookedThis"),
|
||||
onPress: () => patchEntry.mutate({ planId, entryId: entry.id, status: "cooked" }),
|
||||
},
|
||||
{
|
||||
text: t("common.skip"),
|
||||
onPress: () => patchEntry.mutate({ planId, entryId: entry.id, status: "skipped" }),
|
||||
},
|
||||
{ text: t("common.cancel"), style: "cancel" },
|
||||
]);
|
||||
};
|
||||
|
||||
const monthLabel = cap(
|
||||
new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(selected),
|
||||
);
|
||||
@@ -219,6 +256,14 @@ export default function PlanScreen() {
|
||||
</View>
|
||||
|
||||
<Small>{dayHeader}</Small>
|
||||
{draftPlanId ? (
|
||||
<Button
|
||||
label={t("plan.activate")}
|
||||
variant="secondary"
|
||||
onPress={() => draftPlanId && activate.mutate(draftPlanId)}
|
||||
loading={activate.isPending}
|
||||
/>
|
||||
) : null}
|
||||
<Spacer size={spacing.xs} />
|
||||
|
||||
{query.isLoading ? (
|
||||
@@ -231,15 +276,24 @@ export default function PlanScreen() {
|
||||
<LoadingView />
|
||||
</>
|
||||
) : selectedEntries.length > 0 ? (
|
||||
selectedEntries.map((entry) => (
|
||||
<Card key={entry.id}>
|
||||
<Row style={{ justifyContent: "space-between" }}>
|
||||
<Body>{entry.titleSv}</Body>
|
||||
<Tag label={mealTypeLabel(entry.mealType)} />
|
||||
</Row>
|
||||
<Small>{t("plan.portions", { count: entry.portions })}</Small>
|
||||
</Card>
|
||||
))
|
||||
selectedEntries.map((entry) => {
|
||||
const cooked = entry.status === "cooked";
|
||||
const skipped = entry.status === "skipped";
|
||||
return (
|
||||
<Card key={entry.id} onPress={() => openActions(entry)}>
|
||||
<Row style={{ justifyContent: "space-between" }}>
|
||||
<Body muted={cooked || skipped}>{entry.titleSv}</Body>
|
||||
<Row>
|
||||
{cooked ? <Tag label={t("plan.cooked")} tone="success" /> : null}
|
||||
{skipped ? <Tag label={t("plan.skipped")} tone="warning" /> : null}
|
||||
<Tag label={mealTypeLabel(entry.mealType)} />
|
||||
</Row>
|
||||
</Row>
|
||||
<Small>{t("plan.portions", { count: entry.portions })}</Small>
|
||||
{entry.rescheduleReasonSv ? <Small>{entry.rescheduleReasonSv}</Small> : null}
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
<EmptyState text={t("plan.emptyDay")} />
|
||||
|
||||
Reference in New Issue
Block a user