Public Access
feat: 21 meals per week (3/day) + approve/deny + generate single meal
Backend:
- Planner config: set_size=21 (was 3), top_k=30 (was 20)
- generate.py: distribute 21 recipes across 7 days × 3 meal types
- meals.py: add POST /items/{id}/approve, /items/{id}/deny
- meals.py: add POST /{plan_id}/generate-item for empty slots
Frontend:
- Dashboard: Approve/Deny buttons on pending meal cards
- Dashboard: Generate button in empty meal slots
- API client: approveItem, denyItem, generateItem methods
Build: TypeScript compiles clean, Python syntax verified.
This commit is contained in:
@@ -31,10 +31,12 @@ const MEAL_TYPES = ['breakfast', 'lunch', 'dinner'] as const
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* MealCard (draggable) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
function MealCard({ item, dragHandleProps, isDragging }: {
|
||||
function MealCard({ item, dragHandleProps, isDragging, onApprove, onDeny }: {
|
||||
item: MealPlanItem
|
||||
dragHandleProps?: DraggableProvidedDragHandleProps | null
|
||||
isDragging?: boolean
|
||||
onApprove?: (itemId: string) => void
|
||||
onDeny?: (itemId: string) => void
|
||||
}) {
|
||||
const totalTime = item.recipe?.total_time_minutes ??
|
||||
(item.recipe?.prep_time_minutes || 0) + (item.recipe?.cook_time_minutes || 0)
|
||||
@@ -94,6 +96,23 @@ function MealCard({ item, dragHandleProps, isDragging }: {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Approve / Deny buttons */}
|
||||
{item.approval_status === 'pending' && onApprove && onDeny && (
|
||||
<div className="flex items-center gap-1 mt-1.5">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onApprove(item.id) }}
|
||||
className="text-[10px] px-2 py-0.5 rounded bg-success-50 text-success-700 hover:bg-success-100 font-medium transition-colors"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onDeny(item.id) }}
|
||||
className="text-[10px] px-2 py-0.5 rounded bg-danger-50 text-danger-700 hover:bg-danger-100 font-medium transition-colors"
|
||||
>
|
||||
Deny
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,10 +126,16 @@ function MealSlot({
|
||||
dayIndex,
|
||||
mealType,
|
||||
item,
|
||||
onApprove,
|
||||
onDeny,
|
||||
onGenerate,
|
||||
}: {
|
||||
dayIndex: number
|
||||
mealType: string
|
||||
item?: MealPlanItem
|
||||
onApprove?: (itemId: string) => void
|
||||
onDeny?: (itemId: string) => void
|
||||
onGenerate?: (dayIndex: number, mealType: string) => void
|
||||
}) {
|
||||
const droppableId = `slot-${dayIndex}-${mealType}`
|
||||
|
||||
@@ -137,14 +162,23 @@ function MealSlot({
|
||||
item={item}
|
||||
dragHandleProps={dragProvided.dragHandleProps}
|
||||
isDragging={dragSnapshot.isDragging}
|
||||
onApprove={onApprove}
|
||||
onDeny={onDeny}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
) : (
|
||||
/* empty slot placeholder — invisible but droppable */
|
||||
<div className="h-16 rounded-xl border-2 border-dashed border-surface-200 flex items-center justify-center">
|
||||
<span className="text-xs text-surface-300">Drop here</span>
|
||||
<div className="h-16 rounded-xl border-2 border-dashed border-surface-200 flex flex-col items-center justify-center gap-1">
|
||||
<span className="text-xs text-surface-300">Empty</span>
|
||||
{onGenerate && (
|
||||
<button
|
||||
onClick={() => onGenerate(dayIndex, mealType)}
|
||||
className="text-[10px] px-2 py-0.5 rounded bg-primary-50 text-primary-700 hover:bg-primary-100 font-medium transition-colors"
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
@@ -160,9 +194,15 @@ function MealSlot({
|
||||
function DayColumn({
|
||||
dayIndex,
|
||||
items,
|
||||
onApprove,
|
||||
onDeny,
|
||||
onGenerate,
|
||||
}: {
|
||||
dayIndex: number
|
||||
items: MealPlanItem[]
|
||||
onApprove?: (itemId: string) => void
|
||||
onDeny?: (itemId: string) => void
|
||||
onGenerate?: (dayIndex: number, mealType: string) => void
|
||||
}) {
|
||||
const today = new Date().getDay()
|
||||
const isToday = today === (dayIndex + 1) % 7
|
||||
@@ -190,6 +230,9 @@ function DayColumn({
|
||||
dayIndex={dayIndex}
|
||||
mealType={mealType}
|
||||
item={item}
|
||||
onApprove={onApprove}
|
||||
onDeny={onDeny}
|
||||
onGenerate={onGenerate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -289,6 +332,37 @@ export default function Dashboard() {
|
||||
handleDrop(itemId, parseInt(dayStr, 10), typeStr)
|
||||
}
|
||||
|
||||
async function handleApprove(itemId: string) {
|
||||
try {
|
||||
await mealPlannerApi.meals.approveItem(itemId)
|
||||
toast.success('Meal approved')
|
||||
queryClient.invalidateQueries({ queryKey: ['mealPlan'] })
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.detail || 'Failed to approve meal')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeny(itemId: string) {
|
||||
try {
|
||||
await mealPlannerApi.meals.denyItem(itemId)
|
||||
toast.success('Meal denied')
|
||||
queryClient.invalidateQueries({ queryKey: ['mealPlan'] })
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.detail || 'Failed to deny meal')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGenerate(dayIndex: number, mealType: string) {
|
||||
if (!mealPlan) return
|
||||
try {
|
||||
await mealPlannerApi.meals.generateItem(mealPlan.id, dayIndex + 1, mealType)
|
||||
toast.success('Meal generated')
|
||||
queryClient.invalidateQueries({ queryKey: ['mealPlan'] })
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.detail || 'Failed to generate meal')
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) return <DashboardSkeleton />
|
||||
|
||||
if (!mealPlan) {
|
||||
@@ -359,11 +433,14 @@ export default function Dashboard() {
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{itemsByDay.map((items, index) => (
|
||||
<DayColumn
|
||||
key={index}
|
||||
dayIndex={index}
|
||||
items={items}
|
||||
/>
|
||||
<DayColumn
|
||||
key={index}
|
||||
dayIndex={index}
|
||||
items={items}
|
||||
onApprove={handleApprove}
|
||||
onDeny={handleDeny}
|
||||
onGenerate={handleGenerate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
|
||||
Reference in New Issue
Block a user