Security recovery: hold sweep, dashboard updates, and admin fixes.
This commit is contained in:
@@ -6,6 +6,7 @@ export function StatusBadge({ status, compact = false }: { status: string; compa
|
||||
confirmed: 'bg-green-100 text-green-800',
|
||||
cancelled: 'bg-red-100 text-red-800',
|
||||
checked_in: 'bg-blue-100 text-blue-800',
|
||||
on_hold: 'bg-slate-100 text-slate-600',
|
||||
};
|
||||
return (
|
||||
<span className={clsx(
|
||||
|
||||
@@ -20,6 +20,7 @@ interface EventModalsProps {
|
||||
confirmedCount: number;
|
||||
checkedInCount: number;
|
||||
cancelledCount: number;
|
||||
onHoldCount: number;
|
||||
statusFilter: AttendeeStatusFilter;
|
||||
setStatusFilter: (value: AttendeeStatusFilter) => void;
|
||||
// mobile filter sheet
|
||||
@@ -75,6 +76,7 @@ export function EventModals(props: EventModalsProps) {
|
||||
confirmedCount,
|
||||
checkedInCount,
|
||||
cancelledCount,
|
||||
onHoldCount,
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
mobileFilterOpen,
|
||||
@@ -129,6 +131,7 @@ export function EventModals(props: EventModalsProps) {
|
||||
{ value: 'confirmed', label: `Confirmed (${confirmedCount})` },
|
||||
{ value: 'checked_in', label: `Checked In (${checkedInCount})` },
|
||||
{ value: 'cancelled', label: `Cancelled (${cancelledCount})` },
|
||||
{ value: 'on_hold', label: `On Hold (${onHoldCount})` },
|
||||
].map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
StarIcon,
|
||||
FunnelIcon,
|
||||
ChatBubbleLeftIcon,
|
||||
ArrowPathIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { StatusBadge } from '../_components/StatusBadge';
|
||||
import type { AttendeeStatusFilter, PrimaryAction } from '../_types';
|
||||
@@ -29,6 +30,7 @@ interface AttendeesTabProps {
|
||||
confirmedCount: number;
|
||||
checkedInCount: number;
|
||||
cancelledCount: number;
|
||||
onHoldCount: number;
|
||||
exporting: boolean;
|
||||
showExportDropdown: boolean;
|
||||
setShowExportDropdown: (value: boolean) => void;
|
||||
@@ -43,6 +45,7 @@ interface AttendeesTabProps {
|
||||
setShowAddTicketSheet: (value: boolean) => void;
|
||||
getPrimaryAction: (ticket: Ticket) => PrimaryAction | null;
|
||||
handleOpenNoteModal: (ticket: Ticket) => void;
|
||||
handleReactivate: (ticket: Ticket) => void;
|
||||
}
|
||||
|
||||
export function AttendeesTab({
|
||||
@@ -57,6 +60,7 @@ export function AttendeesTab({
|
||||
confirmedCount,
|
||||
checkedInCount,
|
||||
cancelledCount,
|
||||
onHoldCount,
|
||||
exporting,
|
||||
showExportDropdown,
|
||||
setShowExportDropdown,
|
||||
@@ -71,6 +75,7 @@ export function AttendeesTab({
|
||||
setShowAddTicketSheet,
|
||||
getPrimaryAction,
|
||||
handleOpenNoteModal,
|
||||
handleReactivate,
|
||||
}: AttendeesTabProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -98,6 +103,7 @@ export function AttendeesTab({
|
||||
<option value="confirmed">Confirmed ({confirmedCount})</option>
|
||||
<option value="checked_in">Checked In ({checkedInCount})</option>
|
||||
<option value="cancelled">Cancelled ({cancelledCount})</option>
|
||||
<option value="on_hold">On Hold ({onHoldCount})</option>
|
||||
</select>
|
||||
|
||||
<div className="flex-1" />
|
||||
@@ -241,6 +247,7 @@ export function AttendeesTab({
|
||||
<td className="px-4 py-2.5">
|
||||
<p className="text-sm text-gray-600 truncate max-w-[200px]">{ticket.attendeeEmail}</p>
|
||||
{ticket.attendeePhone && <p className="text-xs text-gray-400">{ticket.attendeePhone}</p>}
|
||||
{ticket.attendeeRuc && <p className="text-xs text-gray-400">RUC: {ticket.attendeeRuc}</p>}
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
@@ -267,6 +274,11 @@ export function AttendeesTab({
|
||||
</Button>
|
||||
)}
|
||||
<MoreMenu>
|
||||
{ticket.status === 'on_hold' && (
|
||||
<DropdownItem onClick={() => handleReactivate(ticket)}>
|
||||
<ArrowPathIcon className="w-4 h-4 mr-2" /> Reactivate
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem onClick={() => handleOpenNoteModal(ticket)}>
|
||||
<ChatBubbleLeftIcon className="w-4 h-4 mr-2" />
|
||||
{ticket.adminNote ? 'Edit Note' : 'Add Note'}
|
||||
@@ -307,6 +319,7 @@ export function AttendeesTab({
|
||||
<p className="font-medium text-sm truncate">{ticket.attendeeFirstName} {ticket.attendeeLastName || ''}</p>
|
||||
<p className="text-xs text-gray-500 truncate">{ticket.attendeeEmail}</p>
|
||||
{ticket.attendeePhone && <p className="text-[10px] text-gray-400">{ticket.attendeePhone}</p>}
|
||||
{ticket.attendeeRuc && <p className="text-[10px] text-gray-400">RUC: {ticket.attendeeRuc}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0 flex-wrap justify-end">
|
||||
<StatusBadge status={ticket.status} compact />
|
||||
@@ -328,6 +341,11 @@ export function AttendeesTab({
|
||||
</Button>
|
||||
)}
|
||||
<MoreMenu>
|
||||
{ticket.status === 'on_hold' && (
|
||||
<DropdownItem onClick={() => handleReactivate(ticket)}>
|
||||
<ArrowPathIcon className="w-4 h-4 mr-2" /> Reactivate
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem onClick={() => handleOpenNoteModal(ticket)}>
|
||||
<ChatBubbleLeftIcon className="w-4 h-4 mr-2" />
|
||||
{ticket.adminNote ? 'Edit Note' : 'Add Note'}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ComponentType } from 'react';
|
||||
|
||||
export type TabType = 'overview' | 'attendees' | 'tickets' | 'email' | 'payments';
|
||||
|
||||
export type AttendeeStatusFilter = 'all' | 'pending' | 'confirmed' | 'checked_in' | 'cancelled';
|
||||
export type AttendeeStatusFilter = 'all' | 'pending' | 'confirmed' | 'checked_in' | 'cancelled' | 'on_hold';
|
||||
export type TicketStatusFilter = 'all' | 'confirmed' | 'checked_in';
|
||||
export type RecipientFilter = 'all' | 'confirmed' | 'pending' | 'checked_in';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import { ticketsApi, emailsApi, adminApi, Ticket } from '@/lib/api';
|
||||
import { ticketsApi, emailsApi, adminApi, paymentsApi, Ticket } from '@/lib/api';
|
||||
import { formatDateLong, formatDateCompact, formatTime } from '@/lib/utils';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
@@ -163,6 +163,17 @@ export default function AdminEventDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReactivate = async (ticket: Ticket) => {
|
||||
if (!ticket.payment?.id) return;
|
||||
try {
|
||||
await paymentsApi.reactivate(ticket.payment.id);
|
||||
toast.success('Booking reactivated');
|
||||
loadEventData();
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to reactivate booking');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCheckin = async (ticketId: string) => {
|
||||
if (!confirm('Are you sure you want to remove the check-in for this attendee?')) return;
|
||||
try {
|
||||
@@ -421,6 +432,7 @@ export default function AdminEventDetailPage() {
|
||||
const pendingCount = getTicketsByStatus('pending').length;
|
||||
const checkedInCount = getTicketsByStatus('checked_in').length;
|
||||
const cancelledCount = getTicketsByStatus('cancelled').length;
|
||||
const onHoldCount = getTicketsByStatus('on_hold').length;
|
||||
const paidConfirmedCount = getTicketsByStatus('confirmed').filter(t => !t.isGuest).length;
|
||||
const paidCheckedInCount = getTicketsByStatus('checked_in').filter(t => !t.isGuest).length;
|
||||
const revenue = (paidConfirmedCount + paidCheckedInCount) * event.price;
|
||||
@@ -435,7 +447,7 @@ export default function AdminEventDetailPage() {
|
||||
|
||||
// ========== Primary action for a ticket ==========
|
||||
const getPrimaryAction = (ticket: Ticket): PrimaryAction | null => {
|
||||
if (ticket.status === 'pending') {
|
||||
if (ticket.status === 'pending' || ticket.status === 'on_hold') {
|
||||
return { label: 'Mark Paid', onClick: () => handleMarkPaid(ticket.id), variant: 'outline' };
|
||||
}
|
||||
if (ticket.status === 'confirmed') {
|
||||
@@ -671,6 +683,7 @@ export default function AdminEventDetailPage() {
|
||||
confirmedCount={confirmedCount}
|
||||
checkedInCount={checkedInCount}
|
||||
cancelledCount={cancelledCount}
|
||||
onHoldCount={onHoldCount}
|
||||
exporting={exporting}
|
||||
showExportDropdown={showExportDropdown}
|
||||
setShowExportDropdown={setShowExportDropdown}
|
||||
@@ -685,6 +698,7 @@ export default function AdminEventDetailPage() {
|
||||
setShowAddTicketSheet={setShowAddTicketSheet}
|
||||
getPrimaryAction={getPrimaryAction}
|
||||
handleOpenNoteModal={handleOpenNoteModal}
|
||||
handleReactivate={handleReactivate}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -742,6 +756,7 @@ export default function AdminEventDetailPage() {
|
||||
confirmedCount={confirmedCount}
|
||||
checkedInCount={checkedInCount}
|
||||
cancelledCount={cancelledCount}
|
||||
onHoldCount={onHoldCount}
|
||||
statusFilter={statusFilter}
|
||||
setStatusFilter={setStatusFilter}
|
||||
mobileFilterOpen={mobileFilterOpen}
|
||||
|
||||
Reference in New Issue
Block a user