Unify admin ticket creation into one modal with first-class payment status.
- Replace the three Attendees-tab modals (Manual Ticket / Add at Door / Invite Guest) with a single Add Ticket modal: Paid/Unpaid/Guest segmented control, shared fields, "Check in now" for all types, and a live "what happens" preview, backed by one POST /api/tickets/admin/add. - Add tickets.payment_status (paid | unpaid | comp) with a backfill migration; keep it in sync on every payment-settlement path (mark-paid, admin approval, Lightning, free bookings, hold recovery). - Show Paid/Unpaid/Comp badges in the attendee list, count only paid tickets toward revenue, let unpaid tickets be resolved via Mark Paid, and flag unpaid tickets with their balance due in the door scanner. - Replace the per-page useStatsPrivacy hook with an admin-wide PrivacyContext + SensitiveValue mask, toggled from the admin layout. - Add server-side pagination with page-size options to the users page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e38d14970d
commit
9b2668f498
@@ -14,9 +14,11 @@ import {
|
||||
FunnelIcon,
|
||||
ChatBubbleLeftIcon,
|
||||
ArrowPathIcon,
|
||||
BanknotesIcon,
|
||||
CheckCircleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { StatusBadge } from '../_components/StatusBadge';
|
||||
import type { AttendeeStatusFilter, PrimaryAction } from '../_types';
|
||||
import { StatusBadge, PaymentBadge } from '../_components/StatusBadge';
|
||||
import type { AttendeeStatusFilter, AddTicketType, PrimaryAction } from '../_types';
|
||||
|
||||
interface AttendeesTabProps {
|
||||
locale: string;
|
||||
@@ -37,15 +39,15 @@ interface AttendeesTabProps {
|
||||
showAddTicketDropdown: boolean;
|
||||
setShowAddTicketDropdown: (value: boolean) => void;
|
||||
handleExportAttendees: (status: 'confirmed' | 'checked_in' | 'confirmed_pending' | 'all') => void;
|
||||
setShowManualTicketModal: (value: boolean) => void;
|
||||
setShowAddAtDoorModal: (value: boolean) => void;
|
||||
setShowInviteGuestModal: (value: boolean) => void;
|
||||
openAddTicket: (type: AddTicketType) => void;
|
||||
setMobileFilterOpen: (value: boolean) => void;
|
||||
setShowExportSheet: (value: boolean) => void;
|
||||
setShowAddTicketSheet: (value: boolean) => void;
|
||||
getPrimaryAction: (ticket: Ticket) => PrimaryAction | null;
|
||||
handleOpenNoteModal: (ticket: Ticket) => void;
|
||||
handleReactivate: (ticket: Ticket) => void;
|
||||
handleMarkPaid: (ticketId: string) => void;
|
||||
handleCheckin: (ticketId: string) => void;
|
||||
}
|
||||
|
||||
export function AttendeesTab({
|
||||
@@ -67,15 +69,15 @@ export function AttendeesTab({
|
||||
showAddTicketDropdown,
|
||||
setShowAddTicketDropdown,
|
||||
handleExportAttendees,
|
||||
setShowManualTicketModal,
|
||||
setShowAddAtDoorModal,
|
||||
setShowInviteGuestModal,
|
||||
openAddTicket,
|
||||
setMobileFilterOpen,
|
||||
setShowExportSheet,
|
||||
setShowAddTicketSheet,
|
||||
getPrimaryAction,
|
||||
handleOpenNoteModal,
|
||||
handleReactivate,
|
||||
handleMarkPaid,
|
||||
handleCheckin,
|
||||
}: AttendeesTabProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -143,13 +145,13 @@ export function AttendeesTab({
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<DropdownItem onClick={() => { setShowManualTicketModal(true); setShowAddTicketDropdown(false); }}>
|
||||
<EnvelopeIcon className="w-4 h-4 mr-2" /> Manual Ticket
|
||||
<DropdownItem onClick={() => { openAddTicket('paid'); setShowAddTicketDropdown(false); }}>
|
||||
<EnvelopeIcon className="w-4 h-4 mr-2" /> Paid Ticket
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={() => { setShowAddAtDoorModal(true); setShowAddTicketDropdown(false); }}>
|
||||
<PlusIcon className="w-4 h-4 mr-2" /> Add at Door
|
||||
<DropdownItem onClick={() => { openAddTicket('unpaid'); setShowAddTicketDropdown(false); }}>
|
||||
<BanknotesIcon className="w-4 h-4 mr-2" /> Unpaid Ticket
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={() => { setShowInviteGuestModal(true); setShowAddTicketDropdown(false); }}>
|
||||
<DropdownItem onClick={() => { openAddTicket('guest'); setShowAddTicketDropdown(false); }}>
|
||||
<StarIcon className="w-4 h-4 mr-2" /> Invite Guest
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
@@ -252,9 +254,7 @@ export function AttendeesTab({
|
||||
<td className="px-4 py-2.5">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<StatusBadge status={ticket.status} compact />
|
||||
{!!ticket.isGuest && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-amber-100 text-amber-700 font-medium">Guest</span>
|
||||
)}
|
||||
<PaymentBadge paymentStatus={ticket.paymentStatus} compact />
|
||||
</div>
|
||||
{ticket.checkinAt && (
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||
@@ -279,6 +279,16 @@ export function AttendeesTab({
|
||||
<ArrowPathIcon className="w-4 h-4 mr-2" /> Reactivate
|
||||
</DropdownItem>
|
||||
)}
|
||||
{ticket.paymentStatus === 'unpaid' && ticket.status === 'checked_in' && (
|
||||
<DropdownItem onClick={() => handleMarkPaid(ticket.id)}>
|
||||
<BanknotesIcon className="w-4 h-4 mr-2" /> Mark as Paid
|
||||
</DropdownItem>
|
||||
)}
|
||||
{ticket.paymentStatus === 'unpaid' && ticket.status === 'confirmed' && (
|
||||
<DropdownItem onClick={() => handleCheckin(ticket.id)}>
|
||||
<CheckCircleIcon className="w-4 h-4 mr-2" /> Check In (unpaid)
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem onClick={() => handleOpenNoteModal(ticket)}>
|
||||
<ChatBubbleLeftIcon className="w-4 h-4 mr-2" />
|
||||
{ticket.adminNote ? 'Edit Note' : 'Add Note'}
|
||||
@@ -323,9 +333,7 @@ export function AttendeesTab({
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0 flex-wrap justify-end">
|
||||
<StatusBadge status={ticket.status} compact />
|
||||
{!!ticket.isGuest && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-amber-100 text-amber-700 font-medium">Guest</span>
|
||||
)}
|
||||
<PaymentBadge paymentStatus={ticket.paymentStatus} compact />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2 pt-2 border-t border-gray-100">
|
||||
@@ -346,6 +354,16 @@ export function AttendeesTab({
|
||||
<ArrowPathIcon className="w-4 h-4 mr-2" /> Reactivate
|
||||
</DropdownItem>
|
||||
)}
|
||||
{ticket.paymentStatus === 'unpaid' && ticket.status === 'checked_in' && (
|
||||
<DropdownItem onClick={() => handleMarkPaid(ticket.id)}>
|
||||
<BanknotesIcon className="w-4 h-4 mr-2" /> Mark as Paid
|
||||
</DropdownItem>
|
||||
)}
|
||||
{ticket.paymentStatus === 'unpaid' && ticket.status === 'confirmed' && (
|
||||
<DropdownItem onClick={() => handleCheckin(ticket.id)}>
|
||||
<CheckCircleIcon className="w-4 h-4 mr-2" /> Check In (unpaid)
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem onClick={() => handleOpenNoteModal(ticket)}>
|
||||
<ChatBubbleLeftIcon className="w-4 h-4 mr-2" />
|
||||
{ticket.adminNote ? 'Edit Note' : 'Add Note'}
|
||||
|
||||
Reference in New Issue
Block a user