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, + }} /> )