Security recovery: hold sweep, dashboard updates, and admin fixes.

This commit is contained in:
Michilis
2026-07-01 05:51:38 +00:00
parent 38526f17b5
commit cacc52ec24
45 changed files with 1452 additions and 474 deletions
+53 -3
View File
@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react';
import { useLanguage } from '@/context/LanguageContext';
import { ticketsApi, eventsApi, Ticket, Event } from '@/lib/api';
import { ticketsApi, eventsApi, paymentsApi, Ticket, Event } from '@/lib/api';
import { parseDate } from '@/lib/utils';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
@@ -17,6 +17,7 @@ import {
PhoneIcon,
FunnelIcon,
MagnifyingGlassIcon,
ArrowPathIcon,
} from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
import clsx from 'clsx';
@@ -101,6 +102,20 @@ export default function AdminBookingsPage() {
}
};
const handleReactivate = async (ticket: TicketWithDetails) => {
if (!ticket.payment?.id) return;
setProcessing(ticket.id);
try {
await paymentsApi.reactivate(ticket.payment.id);
toast.success('Booking reactivated');
loadData();
} catch (error: any) {
toast.error(error.message || 'Failed to reactivate booking');
} finally {
setProcessing(null);
}
};
const handleCancel = async (ticketId: string) => {
if (!confirm('Are you sure you want to cancel this booking?')) return;
@@ -133,6 +148,7 @@ export default function AdminBookingsPage() {
case 'pending': return 'bg-yellow-100 text-yellow-800';
case 'cancelled': return 'bg-red-100 text-red-800';
case 'checked_in': return 'bg-blue-100 text-blue-800';
case 'on_hold': return 'bg-slate-100 text-slate-600';
default: return 'bg-gray-100 text-gray-800';
}
};
@@ -144,6 +160,7 @@ export default function AdminBookingsPage() {
case 'failed':
case 'cancelled': return 'bg-red-100 text-red-800';
case 'refunded': return 'bg-purple-100 text-purple-800';
case 'on_hold': return 'bg-slate-100 text-slate-600';
default: return 'bg-gray-100 text-gray-800';
}
};
@@ -191,6 +208,7 @@ export default function AdminBookingsPage() {
confirmed: tickets.filter(t => t.status === 'confirmed').length,
checkedIn: tickets.filter(t => t.status === 'checked_in').length,
cancelled: tickets.filter(t => t.status === 'cancelled').length,
onHold: tickets.filter(t => t.status === 'on_hold').length,
pendingPayment: tickets.filter(t => t.payment?.status === 'pending').length,
};
@@ -218,6 +236,9 @@ export default function AdminBookingsPage() {
if (ticket.status === 'pending' && ticket.payment?.status === 'pending') {
return { label: 'Mark Paid', onClick: () => handleMarkPaid(ticket.id), color: 'text-green-600' };
}
if (ticket.status === 'on_hold') {
return { label: 'Mark Paid', onClick: () => handleMarkPaid(ticket.id), color: 'text-green-600' };
}
if (ticket.status === 'confirmed') {
return { label: 'Check In', onClick: () => handleCheckin(ticket.id), color: 'text-blue-600' };
}
@@ -239,7 +260,7 @@ export default function AdminBookingsPage() {
</div>
{/* Stats Cards */}
<div className="grid grid-cols-3 md:grid-cols-3 lg:grid-cols-6 gap-2 md:gap-4 mb-6">
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-2 md:gap-4 mb-6">
<Card className="p-3 md:p-4 text-center">
<p className="text-xl md:text-2xl font-bold text-primary-dark">{stats.total}</p>
<p className="text-xs md:text-sm text-gray-500">Total</p>
@@ -260,6 +281,10 @@ export default function AdminBookingsPage() {
<p className="text-xl md:text-2xl font-bold text-red-600">{stats.cancelled}</p>
<p className="text-xs md:text-sm text-gray-500">Cancelled</p>
</Card>
<Card className="p-3 md:p-4 text-center border-l-4 border-slate-400">
<p className="text-xl md:text-2xl font-bold text-slate-600">{stats.onHold}</p>
<p className="text-xs md:text-sm text-gray-500">On Hold</p>
</Card>
<Card className="p-3 md:p-4 text-center border-l-4 border-orange-400">
<p className="text-xl md:text-2xl font-bold text-orange-600">{stats.pendingPayment}</p>
<p className="text-xs md:text-sm text-gray-500">Pending Pay</p>
@@ -305,6 +330,7 @@ export default function AdminBookingsPage() {
<option value="confirmed">Confirmed</option>
<option value="checked_in">Checked In</option>
<option value="cancelled">Cancelled</option>
<option value="on_hold">On Hold</option>
</select>
</div>
<div>
@@ -316,6 +342,7 @@ export default function AdminBookingsPage() {
<option value="paid">Paid</option>
<option value="refunded">Refunded</option>
<option value="failed">Failed</option>
<option value="on_hold">On Hold</option>
</select>
</div>
</div>
@@ -368,6 +395,7 @@ export default function AdminBookingsPage() {
<thead className="bg-secondary-gray">
<tr>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Attendee</th>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">RUC</th>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Event</th>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Payment</th>
<th className="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
@@ -378,7 +406,7 @@ export default function AdminBookingsPage() {
<tbody className="divide-y divide-secondary-light-gray">
{sortedTickets.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-12 text-center text-gray-500 text-sm">
<td colSpan={7} className="px-4 py-12 text-center text-gray-500 text-sm">
No bookings found.
</td>
</tr>
@@ -392,6 +420,7 @@ export default function AdminBookingsPage() {
<p className="text-xs text-gray-500 truncate max-w-[200px]">{ticket.attendeeEmail || 'N/A'}</p>
{ticket.attendeePhone && <p className="text-xs text-gray-400">{ticket.attendeePhone}</p>}
</td>
<td className="px-4 py-3 text-sm text-gray-600">{ticket.attendeeRuc || '-'}</td>
<td className="px-4 py-3">
<span className="text-sm truncate max-w-[150px] block">
{ticket.event?.title || events.find(e => e.id === ticket.eventId)?.title || 'Unknown'}
@@ -431,6 +460,18 @@ export default function AdminBookingsPage() {
Check In
</Button>
)}
{ticket.status === 'on_hold' && (
<>
<Button size="sm" variant="outline" onClick={() => handleMarkPaid(ticket.id)}
isLoading={processing === ticket.id} className="text-xs px-2 py-1">
Mark Paid
</Button>
<Button size="sm" variant="outline" onClick={() => handleReactivate(ticket)}
isLoading={processing === ticket.id} className="text-xs px-2 py-1">
Reactivate
</Button>
</>
)}
{(ticket.status === 'pending' || ticket.status === 'confirmed') && (
<MoreMenu>
<DropdownItem onClick={() => handleCancel(ticket.id)} className="text-red-600">
@@ -519,6 +560,13 @@ export default function AdminBookingsPage() {
</DropdownItem>
</MoreMenu>
)}
{ticket.status === 'on_hold' && (
<MoreMenu>
<DropdownItem onClick={() => handleReactivate(ticket)}>
<ArrowPathIcon className="w-4 h-4 mr-2" /> Reactivate
</DropdownItem>
</MoreMenu>
)}
{ticket.status === 'checked_in' && (
<span className="text-[10px] text-green-600 flex items-center gap-1">
<CheckCircleIcon className="w-3.5 h-3.5" /> Attended
@@ -557,6 +605,7 @@ export default function AdminBookingsPage() {
{ value: 'confirmed', label: `Confirmed (${stats.confirmed})` },
{ value: 'checked_in', label: `Checked In (${stats.checkedIn})` },
{ value: 'cancelled', label: `Cancelled (${stats.cancelled})` },
{ value: 'on_hold', label: `On Hold (${stats.onHold})` },
].map((opt) => (
<button
key={opt.value}
@@ -581,6 +630,7 @@ export default function AdminBookingsPage() {
{ value: 'paid', label: 'Paid' },
{ value: 'refunded', label: 'Refunded' },
{ value: 'failed', label: 'Failed' },
{ value: 'on_hold', label: 'On Hold' },
].map((opt) => (
<button
key={opt.value}