fix(lunch): reminder filter, recap failure surfacing, manual send-reminders

- Extract selectUnpickedAttendees helper with OR filter (is null OR pickedAt null)
  to fix cron missing attendees with no MemberLunchPick row at all
- Update cron route to use the helper
- sendRecap now throws TRPCError on email failure instead of silently stamping success
- Add lunch.sendReminders adminProcedure for manual on-demand reminder sends
- Add "Send reminders now" AlertDialog button to LunchRecapActions
- Tests: lunch-reminder-filter.test.ts (2 new), all 5 lunch test files pass (40 tests)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-04 16:24:01 +02:00
parent 884c96c710
commit 3f25ba112b
6 changed files with 269 additions and 13 deletions

View File

@@ -11,15 +11,28 @@ import {
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog'
import { Send, Eye } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { Send, Eye, Bell } from 'lucide-react'
import { toast } from 'sonner'
export function LunchRecapActions({
programId,
lunchEventId,
recapSentAt,
extraRecipientCount,
}: {
programId: string
lunchEventId: string
recapSentAt: Date | null
extraRecipientCount: number
}) {
@@ -46,6 +59,15 @@ export function LunchRecapActions({
},
})
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}`)
},
})
const { data: preview, isLoading: loadingPreview } =
trpc.lunch.getRecapPreview.useQuery(
{ programId },
@@ -68,6 +90,31 @@ export function LunchRecapActions({
>
<Send className="mr-2 h-4 w-4" /> Send recap now
</Button>
<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&apos;t picked a lunch dish yet. You can do this multiple
times it won&apos;t affect the automatic reminder window.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => sendReminders.mutate({ lunchEventId })}
>
Send reminders
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<p className="text-muted-foreground text-xs">
{recapSentAt

View File

@@ -32,6 +32,7 @@ export function LunchTab({ programId }: { programId: string }) {
/>
<LunchRecapActions
programId={programId}
lunchEventId={event.id}
recapSentAt={event.recapSentAt}
extraRecipientCount={event.extraRecipients.length}
/>