diff --git a/src/components/admin/logistics/lunch-recap-actions.tsx b/src/components/admin/logistics/lunch-recap-actions.tsx new file mode 100644 index 0000000..61626ba --- /dev/null +++ b/src/components/admin/logistics/lunch-recap-actions.tsx @@ -0,0 +1,147 @@ +'use client' + +import { useState } from 'react' +import { trpc } from '@/lib/trpc/client' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' +import { Send, Eye } from 'lucide-react' +import { toast } from 'sonner' + +export function LunchRecapActions({ + programId, + recapSentAt, + extraRecipientCount, +}: { + programId: string + recapSentAt: Date | null + extraRecipientCount: number +}) { + const utils = trpc.useUtils() + const [previewOpen, setPreviewOpen] = useState(false) + + const send = trpc.lunch.sendRecap.useMutation({ + onSuccess: () => { + utils.lunch.getEvent.invalidate({ programId }) + toast.success('Recap sent') + }, + onError: (e) => { + if (e.data?.code === 'PRECONDITION_FAILED') { + if ( + confirm( + "You've already sent a recap. Send updated version to all recipients?", + ) + ) { + send.mutate({ programId, forceUpdate: true }) + } + } else { + toast.error(e.message) + } + }, + }) + + const { data: preview, isLoading: loadingPreview } = + trpc.lunch.getRecapPreview.useQuery( + { programId }, + { enabled: previewOpen }, + ) + + return ( + + + Recap + + +
+ + +
+

+ {recapSentAt + ? `Last sent: ${new Date(recapSentAt).toLocaleString()}. Recipients: edition admins${extraRecipientCount > 0 ? ` + ${extraRecipientCount} extra` : ''}.` + : 'Recap has not been sent yet.'} +

+
+ + + + + Recap preview + + {loadingPreview && ( +

Loading…

+ )} + {preview && ( +
+

+ + {preview.summary.picked}/{preview.summary.total} + {' '} + picked + {preview.summary.missing > 0 + ? ` · ${preview.summary.missing} missing` + : ''} +

+ {Object.keys(preview.dishCounts).length > 0 && ( +
+

Dishes

+
    + {Object.entries(preview.dishCounts).map(([n, c]) => ( +
  • + {c}× {n} +
  • + ))} +
+
+ )} + {Object.keys(preview.dietaryCounts).length > 0 && ( +
+

Dietary tags

+
    + {Object.entries(preview.dietaryCounts).map(([n, c]) => ( +
  • + {c}× {n.replace('_', ' ').toLowerCase()} +
  • + ))} +
+
+ )} +
+

Allergens

+ {Object.keys(preview.allergenCounts).length === 0 ? ( +

None reported.

+ ) : ( +
    + {Object.entries(preview.allergenCounts).map(([n, c]) => ( +
  • + {c}× {n.replace('_', ' ').toLowerCase()} +
  • + ))} +
+ )} +
+
+ )} + + + +
+
+
+ ) +} diff --git a/src/components/admin/logistics/lunch-tab.tsx b/src/components/admin/logistics/lunch-tab.tsx index 182661c..43056a1 100644 --- a/src/components/admin/logistics/lunch-tab.tsx +++ b/src/components/admin/logistics/lunch-tab.tsx @@ -7,6 +7,7 @@ import { LunchEventConfig } from './lunch-event-config' import { LunchDishes } from './lunch-dishes' import { LunchManifest } from './lunch-manifest' import { LunchExternals, type LunchExternalsHandle } from './lunch-externals' +import { LunchRecapActions } from './lunch-recap-actions' export function LunchTab({ programId }: { programId: string }) { const { data: event, isLoading } = trpc.lunch.getEvent.useQuery({ programId }) @@ -29,7 +30,11 @@ export function LunchTab({ programId }: { programId: string }) { programId={programId} lunchEventId={event.id} /> - {/* Recap actions card mounts in Task 18. */} + )}