@@ -257,7 +257,28 @@ function DashboardPageContent() {
},
]}
/>
- {/* Tickets card hidden temporarily */}
+
+ }
+ iconBg="bg-emerald-100 dark:bg-emerald-900/30"
+ label="Tickets"
+ total={stats?.totalTickets ?? 0}
+ loading={statsLoading}
+ href="/tickets"
+ rows={[
+ {
+ label: "Regular",
+ value: stats?.totalNormalTickets ?? 0,
+ href: "/tickets",
+ },
+ {
+ label: "Package",
+ value: stats?.totalPackageTickets ?? 0,
+ href: "/tickets",
+ },
+ ]}
+ />
{/* Revenue card */}
diff --git a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx
index 5c6354fd4..bd12def01 100644
--- a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx
+++ b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx
@@ -10,7 +10,7 @@ import ActionButton from '@/components/ui/ActionButton';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import Modal from '@/components/ui/Modal';
import Image from 'next/image';
-import { ticketsApi, apiClient, stationsApi, excessBaggageApi } from '@/lib/api';
+import { ticketsApi, apiClient, stationsApi, excessBaggageApi, bookingsApi } from '@/lib/api';
import Pagination from '@/components/ui/Pagination';
import { formatDateTime, formatCurrency, formatDateTimeShort } from '@/lib/utils';
import { useAuthStore } from '@/lib/auth-store';
@@ -100,10 +100,21 @@ export default function TicketsPage() {
const [generateMissingResult, setGenerateMissingResult] = useState
(null);
+ const { data: hasMissingTickets } = useQuery({
+ queryKey: ['bookings-missing-tickets'],
+ queryFn: async () => {
+ const res = await bookingsApi.getAll({ status: 'CONFIRMED', paymentStatus: 'SUCCEEDED', take: 50 });
+ const items: any[] = (res as any)?.items ?? (Array.isArray(res) ? res : []);
+ return items.some((b: any) => !b.tickets?.length);
+ },
+ refetchInterval: 30_000,
+ });
+
const generateMissingMutation = useMutation({
mutationFn: () => ticketsApi.generateMissing(),
onSuccess: (result: any) => {
queryClient.invalidateQueries({ queryKey: ['tickets'] });
+ queryClient.invalidateQueries({ queryKey: ['bookings-missing-tickets'] });
setGenerateMissingResult(result);
setSuccessMessage(`Generated ${result.generated} ticket(s) for ${result.processed} booking(s)${result.failed ? ` (${result.failed} failed)` : ''}`);
setTimeout(() => setSuccessMessage(''), 6000);
@@ -562,14 +573,16 @@ export default function TicketsPage() {
Manage tickets and validations
-
generateMissingMutation.mutate()}
- >
- Generate Missing
-
+ {hasMissingTickets && (
+
generateMissingMutation.mutate()}
+ >
+ Generate Missing
+
+ )}
setExportModalOpen(true)}>Export
diff --git a/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx
index 3ac0efd56..67df46686 100644
--- a/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx
+++ b/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx
@@ -128,6 +128,7 @@ function BookingDetailContent() {
) {
apiClient.post(`/tickets/generate/${booking.id}`, {}).then(() => refetch()).catch(() => {});
}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [booking?.id, booking?.status, booking?.tickets?.length]);
const { data: paymentMethods } = useQuery