diff --git a/frontend/src/components/ui/EmptyState.tsx b/frontend/src/components/ui/EmptyState.tsx
index 333f726..bd109c5 100644
--- a/frontend/src/components/ui/EmptyState.tsx
+++ b/frontend/src/components/ui/EmptyState.tsx
@@ -10,6 +10,7 @@ interface EmptyStateProps {
label: string;
onClick?: () => void;
to?: string;
+ disabled?: boolean;
};
}
@@ -27,7 +28,7 @@ export function EmptyState({ icon: Icon, title, description, action }: EmptyStat
) : (
-
+
)
)}
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index cf32012..bf08320 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -362,6 +362,7 @@ export default function Dashboard() {
}
const [planningWeek, setPlanningWeek] = useState(false)
const [planMenuOpen, setPlanMenuOpen] = useState(false)
+ const [generatingFirstPlan, setGeneratingFirstPlan] = useState(false)
async function handlePlanWeek(mealTypes: string[]) {
if (!mealPlan || planningWeek) return
@@ -390,6 +391,62 @@ export default function Dashboard() {
setPlanningWeek(false)
}
}
+
+ // Sprint 11: wire the dead "Generate Meal Plan" empty-state CTA.
+ // Creates a fresh meal plan for the current week, then fills its
+ // empty slots from the recipe library via the same endpoint the
+ // existing `Plan Week` menu uses (handlePlanWeek above). Two
+ // requests, but they reuse existing endpoints; no backend changes.
+ async function handleGenerateFirstPlan() {
+ if (generatingFirstPlan) return
+ setGeneratingFirstPlan(true)
+ try {
+ // 1) Create the empty plan. The backend returns 400 with detail
+ // "Meal plan for this week already exists" if another tab
+ // created one first — we fall through to fillEmptySlots in
+ // that case.
+ let planId: string | undefined
+ try {
+ const res = await mealPlannerApi.meals.create({
+ week_start_date: weekStart,
+ status: 'draft',
+ items: [],
+ })
+ planId = (res.data as { id?: string } | undefined)?.id
+ } catch (createErr: unknown) {
+ // Race with another tab: re-fetch the plan to get its id.
+ const existing = await mealPlannerApi.meals.getPlanned(weekStart)
+ planId = (existing.data as { id?: string } | undefined)?.id
+ if (!planId) throw createErr
+ }
+ if (!planId) {
+ showToast.error('Failed to create meal plan')
+ return
+ }
+
+ // 2) Fill the empty slots from the library. Same partial-success
+ // toast pattern as handlePlanWeek.
+ const fillRes = await mealPlannerApi.meals.fillEmptySlots(planId, ['breakfast', 'lunch', 'dinner'])
+ const data = fillRes.data as { filled: unknown[]; failed: { reason: string }[] }
+ const filledCount = data.filled.length
+ const failedCount = data.failed.length
+ if (filledCount === 0 && failedCount === 0) {
+ showToast.success('Plan created — no recipes to add yet')
+ } else if (failedCount === 0) {
+ showToast.success(`Planned ${filledCount} meals`)
+ } else {
+ const reason = data.failed[0]?.reason ?? 'Unknown'
+ showToast.error(
+ `Planned ${filledCount} of ${filledCount + failedCount} meals — ${failedCount} failed (e.g. ${reason})`,
+ )
+ }
+ queryClient.invalidateQueries({ queryKey: ['mealPlan', weekStart] })
+ } catch (err) {
+ showApiError(err, 'Failed to generate meal plan')
+ } finally {
+ setGeneratingFirstPlan(false)
+ }
+ }
const { data: mealPlan, isLoading } = useQuery({
queryKey: ['mealPlan', weekStart],
queryFn: () => mealPlannerApi.meals.getPlanned(weekStart).then(r => r.data),
@@ -500,7 +557,11 @@ export default function Dashboard() {
icon={Sparkles}
title="No meal plan yet"
description="Generate your first weekly meal plan to get started with smart shopping lists and vote emails."
- action={{ label: 'Generate Meal Plan', onClick: () => {} }}
+ action={{
+ label: generatingFirstPlan ? 'Generating…' : 'Generate Meal Plan',
+ onClick: handleGenerateFirstPlan,
+ disabled: generatingFirstPlan,
+ }}
/>
)