feat: Guest Management & RSVP system (7ead7758)

Backend:
- POST /api/events/:id/guests — add guest, auto-generate RSVP token (128-bit
  crypto.randomBytes), build wa.me WhatsApp deep-link, store in invitations
- GET /api/events/:id/guests — list with pg_trgm fuzzy Hebrew name search,
  status filter, pagination; returns RSVP summary + capacity warning at 90%
- GET /api/events/:id/guests/export — CSV export with UTF-8 BOM for Excel
  Hebrew support (json2csv)
- PUT /api/guests/:id — PATCH-style update (COALESCE), organizer ownership check
- DELETE /api/guests/:id — hard delete per Israeli Privacy Law 2023
- GET /api/rsvp/:token — public (no auth), marks opened_at on first visit
- POST /api/rsvp/:token — idempotent RSVP submit, supports dietary update,
  handles cancelled events (410)
- Israeli phone normalization: 05X-XXXXXXX → +972XXXXXXXXX E.164
- Capacity warning returned in add/list/update responses when confirmed ≥ 90%
  of venue_capacity (fire safety compliance)

Frontend:
- GuestListPage — sortable/filterable table, inline RSVP status override,
  WhatsApp send links, delete with confirmation, 30s polling for real-time updates
- AddGuestForm — RTL Hebrew form, all guest fields, shows WhatsApp link on success
- RsvpSummaryCard — 4-metric summary (total/confirmed/declined/pending) + capacity warning
- RsvpPage — public page at /rsvp/:token, shows event details, confirm/decline,
  dietary preference update; no login required
- New UI components: Badge, Select (shadcn/ui compatible)
- App.tsx: added /events/:eventId/guests (protected) and /rsvp/:token (public) routes

Build: 0 TS errors, all routes wired in server.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 18:31:08 +00:00
parent c8909befb1
commit b65f018a8b
12 changed files with 3326 additions and 1 deletions

View File

@@ -0,0 +1,67 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
interface Summary {
total: string | number;
confirmed: string | number;
declined: string | number;
pending: string | number;
}
interface CapacityWarning {
message: string;
percent: number;
confirmed: number;
capacity: number;
}
interface RsvpSummaryCardProps {
summary: Summary;
warning?: CapacityWarning | null;
}
export function RsvpSummaryCard({ summary, warning }: RsvpSummaryCardProps) {
return (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Card>
<CardHeader className="pb-1 pt-4 px-4">
<CardTitle className="text-sm font-medium text-muted-foreground">סה״כ מוזמנים</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4">
<p className="text-2xl font-bold">{summary.total}</p>
</CardContent>
</Card>
<Card className="border-green-200">
<CardHeader className="pb-1 pt-4 px-4">
<CardTitle className="text-sm font-medium text-green-700">מאושרים</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4">
<p className="text-2xl font-bold text-green-700">{summary.confirmed}</p>
</CardContent>
</Card>
<Card className="border-red-200">
<CardHeader className="pb-1 pt-4 px-4">
<CardTitle className="text-sm font-medium text-red-700">לא מגיעים</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4">
<p className="text-2xl font-bold text-red-700">{summary.declined}</p>
</CardContent>
</Card>
<Card className="border-yellow-200">
<CardHeader className="pb-1 pt-4 px-4">
<CardTitle className="text-sm font-medium text-yellow-700">ממתינים</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4">
<p className="text-2xl font-bold text-yellow-700">{summary.pending}</p>
</CardContent>
</Card>
</div>
{warning && (
<div className="rounded-md border border-orange-300 bg-orange-50 p-3 text-sm text-orange-800" dir="rtl">
{warning.message}
</div>
)}
</div>
);
}