2026-04-29 02:45:12 +02:00
|
|
|
|
'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'
|
2026-06-04 16:24:01 +02:00
|
|
|
|
import {
|
|
|
|
|
|
AlertDialog,
|
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
|
AlertDialogTrigger,
|
|
|
|
|
|
} from '@/components/ui/alert-dialog'
|
|
|
|
|
|
import { Send, Eye, Bell } from 'lucide-react'
|
2026-04-29 02:45:12 +02:00
|
|
|
|
import { toast } from 'sonner'
|
|
|
|
|
|
|
|
|
|
|
|
export function LunchRecapActions({
|
|
|
|
|
|
programId,
|
2026-06-04 16:24:01 +02:00
|
|
|
|
lunchEventId,
|
2026-04-29 02:45:12 +02:00
|
|
|
|
recapSentAt,
|
|
|
|
|
|
extraRecipientCount,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
programId: string
|
2026-06-04 16:24:01 +02:00
|
|
|
|
lunchEventId: string
|
2026-04-29 02:45:12 +02:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-04 16:24:01 +02:00
|
|
|
|
const sendReminders = trpc.lunch.sendReminders.useMutation({
|
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
|
toast.success(`Reminders sent to ${data.sent} attendee${data.sent === 1 ? '' : 's'}`)
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: (e) => {
|
|
|
|
|
|
toast.error(`Failed to send reminders: ${e.message}`)
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-29 02:45:12 +02:00
|
|
|
|
const { data: preview, isLoading: loadingPreview } =
|
|
|
|
|
|
trpc.lunch.getRecapPreview.useQuery(
|
|
|
|
|
|
{ programId },
|
|
|
|
|
|
{ enabled: previewOpen },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle>Recap</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
<Button variant="outline" onClick={() => setPreviewOpen(true)}>
|
|
|
|
|
|
<Eye className="mr-2 h-4 w-4" /> Preview recap
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={() => send.mutate({ programId })}
|
|
|
|
|
|
disabled={send.isPending}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Send className="mr-2 h-4 w-4" /> Send recap now
|
|
|
|
|
|
</Button>
|
2026-06-04 16:24:01 +02:00
|
|
|
|
<AlertDialog>
|
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
|
<Button variant="outline" disabled={sendReminders.isPending}>
|
|
|
|
|
|
<Bell className="mr-2 h-4 w-4" /> Send reminders now
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogTitle>Send lunch pick reminders?</AlertDialogTitle>
|
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
|
This will send a reminder email to all confirmed attendees who
|
|
|
|
|
|
haven't picked a lunch dish yet. You can do this multiple
|
|
|
|
|
|
times — it won't affect the automatic reminder window.
|
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
|
onClick={() => sendReminders.mutate({ lunchEventId })}
|
|
|
|
|
|
>
|
|
|
|
|
|
Send reminders
|
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
|
</AlertDialog>
|
2026-04-29 02:45:12 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-muted-foreground text-xs">
|
|
|
|
|
|
{recapSentAt
|
|
|
|
|
|
? `Last sent: ${new Date(recapSentAt).toLocaleString()}. Recipients: edition admins${extraRecipientCount > 0 ? ` + ${extraRecipientCount} extra` : ''}.`
|
|
|
|
|
|
: 'Recap has not been sent yet.'}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
|
|
|
|
|
|
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
|
|
|
|
|
|
<DialogContent className="max-w-3xl">
|
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
|
<DialogTitle>Recap preview</DialogTitle>
|
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
{loadingPreview && (
|
|
|
|
|
|
<p className="text-muted-foreground text-sm">Loading…</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{preview && (
|
|
|
|
|
|
<div className="space-y-4 text-sm">
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<strong>
|
|
|
|
|
|
{preview.summary.picked}/{preview.summary.total}
|
|
|
|
|
|
</strong>{' '}
|
|
|
|
|
|
picked
|
|
|
|
|
|
{preview.summary.missing > 0
|
|
|
|
|
|
? ` · ${preview.summary.missing} missing`
|
|
|
|
|
|
: ''}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{Object.keys(preview.dishCounts).length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h4 className="font-medium">Dishes</h4>
|
|
|
|
|
|
<ul className="ml-4 list-disc">
|
|
|
|
|
|
{Object.entries(preview.dishCounts).map(([n, c]) => (
|
|
|
|
|
|
<li key={n}>
|
|
|
|
|
|
{c}× {n}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{Object.keys(preview.dietaryCounts).length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h4 className="font-medium">Dietary tags</h4>
|
|
|
|
|
|
<ul className="ml-4 list-disc">
|
|
|
|
|
|
{Object.entries(preview.dietaryCounts).map(([n, c]) => (
|
|
|
|
|
|
<li key={n}>
|
|
|
|
|
|
{c}× {n.replace('_', ' ').toLowerCase()}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h4 className="font-medium">Allergens</h4>
|
|
|
|
|
|
{Object.keys(preview.allergenCounts).length === 0 ? (
|
|
|
|
|
|
<p className="text-muted-foreground">None reported.</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ul className="ml-4 list-disc">
|
|
|
|
|
|
{Object.entries(preview.allergenCounts).map(([n, c]) => (
|
|
|
|
|
|
<li key={n}>
|
|
|
|
|
|
{c}× {n.replace('_', ' ').toLowerCase()}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
|
<Button variant="outline" onClick={() => setPreviewOpen(false)}>
|
|
|
|
|
|
Close
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|