feat: clear a meal-plan day or the whole week in one action
New bulk-delete route for the owner's own weekStart-scoped plan (mirrors recipes/bulk's DELETE), plus ?ids= support added to the existing shared-plan DELETE route (kept ?entryId= working unchanged). UI: hover-reveal trash icon per day header, "Clear week" button in the toolbar, one shared confirm dialog for both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,16 @@ import Link from "next/link";
|
||||
import { Plus, Trash2, Sparkles, ChefHat, Check } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { FakeProgressBar } from "@/components/ui/fake-progress-bar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -190,8 +200,11 @@ export function MealPlanner({
|
||||
const [pantryMode, setPantryMode] = useState(false);
|
||||
const [aiDifficulty, setAiDifficulty] = useState<"" | "easy" | "medium" | "hard">("");
|
||||
const [targetNutritionGoals, setTargetNutritionGoals] = useState(false);
|
||||
const [clearTarget, setClearTarget] = useState<{ type: "day"; day: Day; label: string } | { type: "week" } | null>(null);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const t = useTranslations("mealPlan");
|
||||
const tRecipe = useTranslations("recipe");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
async function generateWithAi() {
|
||||
setAiGenerating(true);
|
||||
@@ -292,6 +305,29 @@ export function MealPlanner({
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmClear() {
|
||||
if (!clearTarget) return;
|
||||
const ids = (
|
||||
clearTarget.type === "day" ? entries.filter((e) => e.day === clearTarget.day) : entries
|
||||
).map((e) => e.id);
|
||||
if (ids.length === 0) { setClearTarget(null); return; }
|
||||
|
||||
setClearing(true);
|
||||
try {
|
||||
const res = await fetch(`/api/v1/meal-plans/${weekStart}/entries/bulk`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ids }),
|
||||
});
|
||||
if (!res.ok) { toast.error(t("removeFailed")); return; }
|
||||
setEntries((prev) => prev.filter((e) => !ids.includes(e.id)));
|
||||
toast.success(t("clearedSuccess", { count: ids.length }));
|
||||
} finally {
|
||||
setClearing(false);
|
||||
setClearTarget(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function markCooked(entry: Entry) {
|
||||
if (!entry.recipe || markingCookedIds.has(entry.id)) return;
|
||||
setMarkingCookedIds((prev) => new Set(prev).add(entry.id));
|
||||
@@ -325,6 +361,12 @@ export function MealPlanner({
|
||||
<>
|
||||
{/* AI Generate buttons */}
|
||||
<div className="flex justify-end gap-2 mb-4">
|
||||
{entries.length > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setClearTarget({ type: "week" })} className="gap-2 text-muted-foreground">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
{t("clearWeek")}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={() => setShowBatchModal(true)} className="gap-2">
|
||||
<ChefHat className="h-4 w-4" />
|
||||
{t("batchCooking")}
|
||||
@@ -346,9 +388,25 @@ export function MealPlanner({
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left text-xs font-medium text-muted-foreground pb-3" />
|
||||
{DAYS.map(({ key, label }) => (
|
||||
<th key={key} className="text-center text-sm font-semibold pb-3 px-2">{label}</th>
|
||||
))}
|
||||
{DAYS.map(({ key, label }) => {
|
||||
const dayHasEntries = entries.some((e) => e.day === key);
|
||||
return (
|
||||
<th key={key} className="text-center text-sm font-semibold pb-3 px-2">
|
||||
<span className="group inline-flex items-center gap-1">
|
||||
{label}
|
||||
{dayHasEntries && (
|
||||
<button
|
||||
onClick={() => setClearTarget({ type: "day", day: key, label })}
|
||||
title={t("clearDay")}
|
||||
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-destructive transition-opacity"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -541,6 +599,27 @@ export function MealPlanner({
|
||||
)}
|
||||
|
||||
<BatchCookGenerateDialog open={showBatchModal} onOpenChange={setShowBatchModal} />
|
||||
|
||||
<AlertDialog open={clearTarget !== null} onOpenChange={(open) => !open && setClearTarget(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{clearTarget?.type === "day" ? t("clearDayConfirmTitle", { day: clearTarget.label }) : t("clearWeekConfirmTitle")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>{t("clearConfirmDescription")}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={clearing}>{tCommon("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={clearing}
|
||||
onClick={() => { void confirmClear(); }}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{tCommon("delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user