Add "paid at the door" ticket type to the Add Ticket modal.
Door walk-ins were only expressible as an unpaid ticket, which left the cash out of revenue. The new type records the cash payment as paid and makes every field optional, since a walk-in often gives no details: a blank name is logged as "Walk-in", and a confirmation email only goes out when an email is entered. Door tickets reuse paymentStatus 'paid' (the column enum is capped at paid/unpaid/comp) so badges and revenue totals pick them up with no migration; the cash payment row is referenced "Paid at door" to keep them distinguishable from emailed manual tickets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
be4dd5b47f
commit
a0161a67d2
@@ -1529,14 +1529,18 @@ ticketsRouter.post('/admin/create', requireAuth(['admin', 'organizer', 'staff'])
|
|||||||
// Unified admin add-attendee endpoint backing the single Add Ticket modal.
|
// Unified admin add-attendee endpoint backing the single Add Ticket modal.
|
||||||
// type drives payment handling:
|
// type drives payment handling:
|
||||||
// paid — email required; paid cash payment; confirmation email + QR sent
|
// paid — email required; paid cash payment; confirmation email + QR sent
|
||||||
|
// door — paid in cash at the door; all fields optional; counts toward revenue;
|
||||||
|
// confirmation email only when an email is provided
|
||||||
// unpaid — QR issued with balance due (collect at door); pending tpago payment;
|
// unpaid — QR issued with balance due (collect at door); pending tpago payment;
|
||||||
// pay-link (Bancard/TPago) email sent when an email is provided
|
// pay-link (Bancard/TPago) email sent when an email is provided
|
||||||
// guest — free comp ticket, not counted in revenue; confirmation email only
|
// guest — free comp ticket, not counted in revenue; confirmation email only
|
||||||
// when an email is provided
|
// when an email is provided
|
||||||
ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), zValidator('json', z.object({
|
ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), zValidator('json', z.object({
|
||||||
eventId: z.string(),
|
eventId: z.string(),
|
||||||
type: z.enum(['paid', 'unpaid', 'guest']),
|
type: z.enum(['paid', 'door', 'unpaid', 'guest']),
|
||||||
firstName: z.string().min(1),
|
// Door walk-ins can be logged with nothing filled in, so firstName is only
|
||||||
|
// required for the other types
|
||||||
|
firstName: z.string().optional().or(z.literal('')),
|
||||||
lastName: z.string().optional().or(z.literal('')),
|
lastName: z.string().optional().or(z.literal('')),
|
||||||
email: z.string().email().optional().or(z.literal('')),
|
email: z.string().email().optional().or(z.literal('')),
|
||||||
phone: z.string().optional().or(z.literal('')),
|
phone: z.string().optional().or(z.literal('')),
|
||||||
@@ -1546,6 +1550,9 @@ ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), z
|
|||||||
}).refine((d) => d.type !== 'paid' || !!(d.email && d.email.trim()), {
|
}).refine((d) => d.type !== 'paid' || !!(d.email && d.email.trim()), {
|
||||||
message: 'Email is required for paid tickets',
|
message: 'Email is required for paid tickets',
|
||||||
path: ['email'],
|
path: ['email'],
|
||||||
|
}).refine((d) => d.type === 'door' || !!(d.firstName && d.firstName.trim()), {
|
||||||
|
message: 'First name is required',
|
||||||
|
path: ['firstName'],
|
||||||
})), async (c) => {
|
})), async (c) => {
|
||||||
const data = c.req.valid('json');
|
const data = c.req.valid('json');
|
||||||
|
|
||||||
@@ -1565,9 +1572,11 @@ ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), z
|
|||||||
? data.email!.trim()
|
? data.email!.trim()
|
||||||
: `${data.type === 'guest' ? 'guest' : 'door'}-${generateId()}@${data.type === 'guest' ? 'guestinvite' : 'doorentry'}.local`;
|
: `${data.type === 'guest' ? 'guest' : 'door'}-${generateId()}@${data.type === 'guest' ? 'guestinvite' : 'doorentry'}.local`;
|
||||||
|
|
||||||
|
// Nameless door walk-ins still need a display name on the ticket
|
||||||
|
const firstName = (data.firstName && data.firstName.trim()) || 'Walk-in';
|
||||||
const fullName = data.lastName && data.lastName.trim()
|
const fullName = data.lastName && data.lastName.trim()
|
||||||
? `${data.firstName} ${data.lastName}`.trim()
|
? `${firstName} ${data.lastName.trim()}`
|
||||||
: data.firstName;
|
: firstName;
|
||||||
|
|
||||||
// Find or create user
|
// Find or create user
|
||||||
let user = await dbGet<any>(
|
let user = await dbGet<any>(
|
||||||
@@ -1613,13 +1622,13 @@ ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), z
|
|||||||
|
|
||||||
const ticketId = generateId();
|
const ticketId = generateId();
|
||||||
const qrCode = generateTicketCode();
|
const qrCode = generateTicketCode();
|
||||||
const paymentStatus = data.type === 'guest' ? 'comp' : data.type === 'paid' ? 'paid' : 'unpaid';
|
const paymentStatus = data.type === 'guest' ? 'comp' : data.type === 'unpaid' ? 'unpaid' : 'paid';
|
||||||
|
|
||||||
const newTicket = {
|
const newTicket = {
|
||||||
id: ticketId,
|
id: ticketId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
eventId: data.eventId,
|
eventId: data.eventId,
|
||||||
attendeeFirstName: data.firstName,
|
attendeeFirstName: firstName,
|
||||||
attendeeLastName: data.lastName && data.lastName.trim() ? data.lastName.trim() : null,
|
attendeeLastName: data.lastName && data.lastName.trim() ? data.lastName.trim() : null,
|
||||||
attendeeEmail: hasEmail ? data.email!.trim() : null,
|
attendeeEmail: hasEmail ? data.email!.trim() : null,
|
||||||
attendeePhone: data.phone && data.phone.trim() ? data.phone.trim() : null,
|
attendeePhone: data.phone && data.phone.trim() ? data.phone.trim() : null,
|
||||||
@@ -1636,7 +1645,7 @@ ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), z
|
|||||||
|
|
||||||
await (db as any).insert(tickets).values(newTicket);
|
await (db as any).insert(tickets).values(newTicket);
|
||||||
|
|
||||||
// Payment record: paid cash for paid/guest ($0 for guest), pending tpago for unpaid
|
// Payment record: paid cash for paid/door/guest ($0 for guest), pending tpago for unpaid
|
||||||
const paymentId = generateId();
|
const paymentId = generateId();
|
||||||
const newPayment = data.type === 'unpaid'
|
const newPayment = data.type === 'unpaid'
|
||||||
? {
|
? {
|
||||||
@@ -1659,7 +1668,11 @@ ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), z
|
|||||||
amount: data.type === 'guest' ? 0 : event.price,
|
amount: data.type === 'guest' ? 0 : event.price,
|
||||||
currency: event.currency,
|
currency: event.currency,
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
reference: data.type === 'guest' ? 'Guest invite' : 'Manual ticket',
|
reference: data.type === 'guest'
|
||||||
|
? 'Guest invite'
|
||||||
|
: data.type === 'door'
|
||||||
|
? 'Paid at door'
|
||||||
|
: 'Manual ticket',
|
||||||
paidAt: now,
|
paidAt: now,
|
||||||
paidByAdminId: adminUser?.id || null,
|
paidByAdminId: adminUser?.id || null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
@@ -1668,8 +1681,8 @@ ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), z
|
|||||||
|
|
||||||
await (db as any).insert(payments).values(newPayment);
|
await (db as any).insert(payments).values(newPayment);
|
||||||
|
|
||||||
// Emails (asynchronous): paid always confirms; guest confirms when an email
|
// Emails (asynchronous): paid always confirms; door/guest confirm only when an
|
||||||
// exists; unpaid sends the TPago (Bancard) pay-link instructions instead
|
// email exists; unpaid sends the TPago (Bancard) pay-link instructions instead
|
||||||
if (data.type === 'unpaid') {
|
if (data.type === 'unpaid') {
|
||||||
if (hasEmail) {
|
if (hasEmail) {
|
||||||
emailService.sendPaymentInstructions(ticketId).then(result => {
|
emailService.sendPaymentInstructions(ticketId).then(result => {
|
||||||
@@ -1692,6 +1705,9 @@ ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), z
|
|||||||
|
|
||||||
const messages: Record<string, string> = {
|
const messages: Record<string, string> = {
|
||||||
paid: 'Ticket created — confirmation email sent',
|
paid: 'Ticket created — confirmation email sent',
|
||||||
|
door: hasEmail
|
||||||
|
? 'Ticket created — paid at the door, confirmation email sent'
|
||||||
|
: 'Ticket created — paid at the door',
|
||||||
unpaid: hasEmail
|
unpaid: hasEmail
|
||||||
? 'Unpaid ticket created — payment link sent'
|
? 'Unpaid ticket created — payment link sent'
|
||||||
: 'Unpaid ticket created — collect payment at the door',
|
: 'Unpaid ticket created — collect payment at the door',
|
||||||
|
|||||||
@@ -24,18 +24,21 @@ interface AddTicketModalProps {
|
|||||||
|
|
||||||
const TYPE_OPTIONS: { value: AddTicketType; label: string }[] = [
|
const TYPE_OPTIONS: { value: AddTicketType; label: string }[] = [
|
||||||
{ value: 'paid', label: 'Paid' },
|
{ value: 'paid', label: 'Paid' },
|
||||||
|
{ value: 'door', label: 'At Door' },
|
||||||
{ value: 'unpaid', label: 'Unpaid' },
|
{ value: 'unpaid', label: 'Unpaid' },
|
||||||
{ value: 'guest', label: 'Guest' },
|
{ value: 'guest', label: 'Guest' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SUBMIT_LABELS: Record<AddTicketType, string> = {
|
const SUBMIT_LABELS: Record<AddTicketType, string> = {
|
||||||
paid: 'Create & send ticket',
|
paid: 'Create & send ticket',
|
||||||
|
door: 'Record door payment',
|
||||||
unpaid: 'Create & send pay link',
|
unpaid: 'Create & send pay link',
|
||||||
guest: 'Invite guest',
|
guest: 'Invite guest',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUBMIT_ICONS: Record<AddTicketType, typeof EnvelopeIcon> = {
|
const SUBMIT_ICONS: Record<AddTicketType, typeof EnvelopeIcon> = {
|
||||||
paid: EnvelopeIcon,
|
paid: EnvelopeIcon,
|
||||||
|
door: BanknotesIcon,
|
||||||
unpaid: LinkIcon,
|
unpaid: LinkIcon,
|
||||||
guest: StarIcon,
|
guest: StarIcon,
|
||||||
};
|
};
|
||||||
@@ -47,6 +50,15 @@ function previewLines(form: AddTicketFormState, eventPriceLabel: string): string
|
|||||||
if (form.type === 'paid') {
|
if (form.type === 'paid') {
|
||||||
lines.push(`Payment of ${eventPriceLabel} recorded as paid — counts toward revenue`);
|
lines.push(`Payment of ${eventPriceLabel} recorded as paid — counts toward revenue`);
|
||||||
lines.push('Confirmation email with QR ticket sent');
|
lines.push('Confirmation email with QR ticket sent');
|
||||||
|
} else if (form.type === 'door') {
|
||||||
|
lines.push(`Cash payment of ${eventPriceLabel} recorded as paid at the door — counts toward revenue`);
|
||||||
|
lines.push('QR code issued');
|
||||||
|
if (!form.firstName.trim()) {
|
||||||
|
lines.push('No name — the ticket is logged as a "Walk-in"');
|
||||||
|
}
|
||||||
|
lines.push(hasEmail
|
||||||
|
? 'Confirmation email with QR ticket sent'
|
||||||
|
: 'No email — nothing is sent, walk-in kept on the list only');
|
||||||
} else if (form.type === 'unpaid') {
|
} else if (form.type === 'unpaid') {
|
||||||
lines.push(`Ticket marked unpaid — balance of ${eventPriceLabel} to collect at the door`);
|
lines.push(`Ticket marked unpaid — balance of ${eventPriceLabel} to collect at the door`);
|
||||||
lines.push('QR code issued, flagged "unpaid" for door staff');
|
lines.push('QR code issued, flagged "unpaid" for door staff');
|
||||||
@@ -66,12 +78,14 @@ function previewLines(form: AddTicketFormState, eventPriceLabel: string): string
|
|||||||
|
|
||||||
const PREVIEW_STYLES: Record<AddTicketType, { box: string; icon: string; text: string }> = {
|
const PREVIEW_STYLES: Record<AddTicketType, { box: string; icon: string; text: string }> = {
|
||||||
paid: { box: 'bg-blue-50 border-blue-200', icon: 'text-blue-500', text: 'text-blue-800' },
|
paid: { box: 'bg-blue-50 border-blue-200', icon: 'text-blue-500', text: 'text-blue-800' },
|
||||||
|
door: { box: 'bg-emerald-50 border-emerald-200', icon: 'text-emerald-500', text: 'text-emerald-800' },
|
||||||
unpaid: { box: 'bg-orange-50 border-orange-200', icon: 'text-orange-500', text: 'text-orange-800' },
|
unpaid: { box: 'bg-orange-50 border-orange-200', icon: 'text-orange-500', text: 'text-orange-800' },
|
||||||
guest: { box: 'bg-amber-50 border-amber-200', icon: 'text-amber-500', text: 'text-amber-800' },
|
guest: { box: 'bg-amber-50 border-amber-200', icon: 'text-amber-500', text: 'text-amber-800' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const PREVIEW_ICONS: Record<AddTicketType, typeof EnvelopeIcon> = {
|
const PREVIEW_ICONS: Record<AddTicketType, typeof EnvelopeIcon> = {
|
||||||
paid: CheckCircleIcon,
|
paid: CheckCircleIcon,
|
||||||
|
door: BanknotesIcon,
|
||||||
unpaid: BanknotesIcon,
|
unpaid: BanknotesIcon,
|
||||||
guest: StarIcon,
|
guest: StarIcon,
|
||||||
};
|
};
|
||||||
@@ -88,6 +102,8 @@ export function AddTicketModal({
|
|||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
const emailRequired = form.type === 'paid';
|
const emailRequired = form.type === 'paid';
|
||||||
|
// Door walk-ins can be logged with nothing filled in
|
||||||
|
const nameRequired = form.type !== 'door';
|
||||||
const style = PREVIEW_STYLES[form.type];
|
const style = PREVIEW_STYLES[form.type];
|
||||||
const PreviewIcon = PREVIEW_ICONS[form.type];
|
const PreviewIcon = PREVIEW_ICONS[form.type];
|
||||||
const SubmitIcon = SUBMIT_ICONS[form.type];
|
const SubmitIcon = SUBMIT_ICONS[form.type];
|
||||||
@@ -120,7 +136,7 @@ export function AddTicketModal({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setForm((f) => ({ ...f, type: option.value }))}
|
onClick={() => setForm((f) => ({ ...f, type: option.value }))}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1 px-3 py-2 text-sm font-medium rounded-btn min-h-[36px] transition-colors',
|
'flex-1 px-2 py-2 text-xs sm:text-sm font-medium rounded-btn min-h-[36px] whitespace-nowrap transition-colors',
|
||||||
form.type === option.value
|
form.type === option.value
|
||||||
? 'bg-white shadow-sm text-primary-dark'
|
? 'bg-white shadow-sm text-primary-dark'
|
||||||
: 'text-gray-500 hover:text-gray-700'
|
: 'text-gray-500 hover:text-gray-700'
|
||||||
@@ -133,11 +149,11 @@ export function AddTicketModal({
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium mb-1">First Name *</label>
|
<label className="block text-xs font-medium mb-1">First Name {nameRequired && '*'}</label>
|
||||||
<input type="text" required value={form.firstName}
|
<input type="text" required={nameRequired} value={form.firstName}
|
||||||
onChange={(e) => setForm((f) => ({ ...f, firstName: e.target.value }))}
|
onChange={(e) => setForm((f) => ({ ...f, firstName: e.target.value }))}
|
||||||
className="w-full px-3 py-2.5 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
className="w-full px-3 py-2.5 text-sm rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
||||||
placeholder="First name" />
|
placeholder={nameRequired ? 'First name' : 'First name (optional)'} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium mb-1">Last Name</label>
|
<label className="block text-xs font-medium mb-1">Last Name</label>
|
||||||
@@ -155,6 +171,7 @@ export function AddTicketModal({
|
|||||||
placeholder={emailRequired ? 'email@example.com' : 'email@example.com (optional)'} />
|
placeholder={emailRequired ? 'email@example.com' : 'email@example.com (optional)'} />
|
||||||
<p className="text-[10px] text-gray-500 mt-1">
|
<p className="text-[10px] text-gray-500 mt-1">
|
||||||
{form.type === 'paid' && 'Ticket will be sent to this email'}
|
{form.type === 'paid' && 'Ticket will be sent to this email'}
|
||||||
|
{form.type === 'door' && 'Optional — if provided, the ticket confirmation is sent here'}
|
||||||
{form.type === 'unpaid' && 'If provided, the payment link is sent here'}
|
{form.type === 'unpaid' && 'If provided, the payment link is sent here'}
|
||||||
{form.type === 'guest' && 'If provided, a confirmation email will be sent'}
|
{form.type === 'guest' && 'If provided, a confirmation email will be sent'}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -133,6 +133,16 @@ export function EventModals(props: EventModalsProps) {
|
|||||||
<p className="text-xs text-gray-500">Send confirmation email with QR ticket</p>
|
<p className="text-xs text-gray-500">Send confirmation email with QR ticket</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { openAddTicket('door'); setShowAddTicketSheet(false); }}
|
||||||
|
className="w-full text-left px-4 py-3 rounded-btn text-sm hover:bg-gray-50 min-h-[44px] flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<BanknotesIcon className="w-5 h-5 text-gray-500" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Paid at Door</p>
|
||||||
|
<p className="text-xs text-gray-500">Cash taken at the door, all fields optional</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { openAddTicket('unpaid'); setShowAddTicketSheet(false); }}
|
onClick={() => { openAddTicket('unpaid'); setShowAddTicketSheet(false); }}
|
||||||
className="w-full text-left px-4 py-3 rounded-btn text-sm hover:bg-gray-50 min-h-[44px] flex items-center gap-3"
|
className="w-full text-left px-4 py-3 rounded-btn text-sm hover:bg-gray-50 min-h-[44px] flex items-center gap-3"
|
||||||
|
|||||||
@@ -148,6 +148,9 @@ export function AttendeesTab({
|
|||||||
<DropdownItem onClick={() => { openAddTicket('paid'); setShowAddTicketDropdown(false); }}>
|
<DropdownItem onClick={() => { openAddTicket('paid'); setShowAddTicketDropdown(false); }}>
|
||||||
<EnvelopeIcon className="w-4 h-4 mr-2" /> Paid Ticket
|
<EnvelopeIcon className="w-4 h-4 mr-2" /> Paid Ticket
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<DropdownItem onClick={() => { openAddTicket('door'); setShowAddTicketDropdown(false); }}>
|
||||||
|
<BanknotesIcon className="w-4 h-4 mr-2" /> Paid at Door
|
||||||
|
</DropdownItem>
|
||||||
<DropdownItem onClick={() => { openAddTicket('unpaid'); setShowAddTicketDropdown(false); }}>
|
<DropdownItem onClick={() => { openAddTicket('unpaid'); setShowAddTicketDropdown(false); }}>
|
||||||
<BanknotesIcon className="w-4 h-4 mr-2" /> Unpaid Ticket
|
<BanknotesIcon className="w-4 h-4 mr-2" /> Unpaid Ticket
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ export interface PrimaryAction {
|
|||||||
|
|
||||||
// Ticket type in the unified Add Ticket modal:
|
// Ticket type in the unified Add Ticket modal:
|
||||||
// paid = confirmation + QR emailed, counts toward revenue
|
// paid = confirmation + QR emailed, counts toward revenue
|
||||||
|
// door = already paid in cash at the door, counts toward revenue, every field optional
|
||||||
// unpaid = QR flagged unpaid, balance collected at door, pay link emailed if possible
|
// unpaid = QR flagged unpaid, balance collected at door, pay link emailed if possible
|
||||||
// guest = free comp ticket, auto-confirmed, no revenue
|
// guest = free comp ticket, auto-confirmed, no revenue
|
||||||
export type AddTicketType = 'paid' | 'unpaid' | 'guest';
|
export type AddTicketType = 'paid' | 'door' | 'unpaid' | 'guest';
|
||||||
|
|
||||||
export interface AddTicketFormState {
|
export interface AddTicketFormState {
|
||||||
type: AddTicketType;
|
type: AddTicketType;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default function AdminEventDetailPage() {
|
|||||||
const [showNoteModal, setShowNoteModal] = useState(false);
|
const [showNoteModal, setShowNoteModal] = useState(false);
|
||||||
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
|
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
|
||||||
const [noteText, setNoteText] = useState('');
|
const [noteText, setNoteText] = useState('');
|
||||||
// Unified Add Ticket modal (paid / unpaid / guest via segmented control)
|
// Unified Add Ticket modal (paid / door / unpaid / guest via segmented control)
|
||||||
const [showAddTicketModal, setShowAddTicketModal] = useState(false);
|
const [showAddTicketModal, setShowAddTicketModal] = useState(false);
|
||||||
const [addTicketForm, setAddTicketForm] = useState<AddTicketFormState>(EMPTY_ADD_TICKET_FORM);
|
const [addTicketForm, setAddTicketForm] = useState<AddTicketFormState>(EMPTY_ADD_TICKET_FORM);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -222,7 +222,7 @@ export default function AdminEventDetailPage() {
|
|||||||
const res = await ticketsApi.adminAdd({
|
const res = await ticketsApi.adminAdd({
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
type: addTicketForm.type,
|
type: addTicketForm.type,
|
||||||
firstName: addTicketForm.firstName,
|
firstName: addTicketForm.firstName || undefined,
|
||||||
lastName: addTicketForm.lastName || undefined,
|
lastName: addTicketForm.lastName || undefined,
|
||||||
email: addTicketForm.email || undefined,
|
email: addTicketForm.email || undefined,
|
||||||
phone: addTicketForm.phone || undefined,
|
phone: addTicketForm.phone || undefined,
|
||||||
|
|||||||
@@ -106,11 +106,12 @@ export const ticketsApi = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
// Unified add-attendee endpoint behind the single Add Ticket modal
|
// Unified add-attendee endpoint behind the single Add Ticket modal
|
||||||
// (paid = confirmation + QR, unpaid = pay link + door collection, guest = free comp)
|
// (paid = confirmation + QR, door = cash taken at the door, unpaid = pay link +
|
||||||
|
// door collection, guest = free comp)
|
||||||
adminAdd: (data: {
|
adminAdd: (data: {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
type: 'paid' | 'unpaid' | 'guest';
|
type: 'paid' | 'door' | 'unpaid' | 'guest';
|
||||||
firstName: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user