mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
@@ -9,6 +9,8 @@ import Badge from '@/components/ui/Badge';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import { agentsApi, apiClient } from '@/lib/api';
|
||||
import Pagination from '@/components/ui/Pagination';
|
||||
import { usePagination } from '@/lib/use-pagination';
|
||||
import { formatCurrency, formatDateTime } from '@/lib/utils';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
|
||||
@@ -90,6 +92,9 @@ export default function AgentsPage() {
|
||||
queryFn: () => agentsApi.getAll(filters),
|
||||
});
|
||||
|
||||
const allAgents = data?.items || [];
|
||||
const { paged: pagedAgents, page, totalPages, setPage } = usePagination(allAgents, 20);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'agentCode',
|
||||
@@ -182,12 +187,13 @@ export default function AgentsPage() {
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={data?.items || []}
|
||||
data={pagedAgents}
|
||||
columns={columns}
|
||||
actions={actions}
|
||||
loading={isLoading}
|
||||
emptyMessage="No agents found"
|
||||
/>
|
||||
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={deleteConfirm.isOpen}
|
||||
|
||||
@@ -8,6 +8,8 @@ import ActionButton from '@/components/ui/ActionButton';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import { fleetApi, apiClient } from '@/lib/api';
|
||||
import Pagination from '@/components/ui/Pagination';
|
||||
import { usePagination } from '@/lib/use-pagination';
|
||||
|
||||
type Tab = 'types' | 'coaches' | 'utilization';
|
||||
|
||||
@@ -315,6 +317,9 @@ export default function CoachesPage() {
|
||||
);
|
||||
});
|
||||
|
||||
const { paged: pagedCoachTypes, page: typesPage, totalPages: typesTotalPages, setPage: setTypesPage } = usePagination(filteredCoachTypes as any[], 20);
|
||||
const { paged: pagedCoaches, page: coachesPage, totalPages: coachesTotalPages, setPage: setCoachesPage } = usePagination(filteredCoaches as any[], 20);
|
||||
|
||||
const typeColorMap: Record<string, string> = {
|
||||
passenger: 'edr-badge-info',
|
||||
sleeper: 'edr-badge-warning',
|
||||
@@ -551,11 +556,12 @@ export default function CoachesPage() {
|
||||
</div>
|
||||
<DataTable
|
||||
columns={coachTypeColumns}
|
||||
data={filteredCoachTypes}
|
||||
data={pagedCoachTypes}
|
||||
actions={coachTypeActions}
|
||||
loading={typesLoading}
|
||||
emptyMessage={search ? "No coach types match your search" : "No coach types found. Create one to get started."}
|
||||
/>
|
||||
<Pagination currentPage={typesPage} totalPages={typesTotalPages} onPageChange={setTypesPage} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -574,11 +580,12 @@ export default function CoachesPage() {
|
||||
</div>
|
||||
<DataTable
|
||||
columns={coachColumns}
|
||||
data={filteredCoaches}
|
||||
data={pagedCoaches}
|
||||
actions={coachActions}
|
||||
loading={coachesLoading}
|
||||
emptyMessage={search ? "No coaches match your search" : "No coaches found. Create one to get started."}
|
||||
/>
|
||||
<Pagination currentPage={coachesPage} totalPages={coachesTotalPages} onPageChange={setCoachesPage} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import ActionButton from '@/components/ui/ActionButton';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import { paymentsApi, apiClient } from '@/lib/api';
|
||||
import Pagination from '@/components/ui/Pagination';
|
||||
import { usePagination } from '@/lib/use-pagination';
|
||||
import { formatDateTime, formatCurrency } from '@/lib/utils';
|
||||
import SupplementaryChargesModal from './SupplementaryChargesModal';
|
||||
import {
|
||||
@@ -109,6 +111,9 @@ export default function PaymentsPage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const allPayments = (data as any)?.items || (Array.isArray(data) ? data : []);
|
||||
const { paged: pagedPayments, page: paymentsPage, totalPages: paymentsTotalPages, setPage: setPaymentsPage } = usePagination(allPayments, 20);
|
||||
|
||||
const PAYMENT_COLS = [
|
||||
{ key: 'reference', label: 'Reference' },
|
||||
{ key: 'booking', label: 'Booking Reference' },
|
||||
@@ -311,12 +316,13 @@ export default function PaymentsPage() {
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={(data as any)?.items || (Array.isArray(data) ? data : [])}
|
||||
data={pagedPayments}
|
||||
columns={columns}
|
||||
actions={paymentActions}
|
||||
loading={isLoading}
|
||||
emptyMessage="No payments found"
|
||||
/>
|
||||
<Pagination currentPage={paymentsPage} totalPages={paymentsTotalPages} onPageChange={setPaymentsPage} />
|
||||
|
||||
{/* Payment Details Modal */}
|
||||
<Modal isOpen={!!selectedPayment} onClose={() => setSelectedPayment(null)} title="Payment Details" size="xl">
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Users, Armchair, BarChart3, Train, Download } from "lucide-react";
|
||||
import { apiClient } from "@/lib/api-client";
|
||||
import { formatDateTime } from "@/lib/utils";
|
||||
import ActionButton from "@/components/ui/ActionButton";
|
||||
import Pagination from "@/components/ui/Pagination";
|
||||
import { usePagination } from "@/lib/use-pagination";
|
||||
|
||||
interface ScheduleOption {
|
||||
id: string;
|
||||
@@ -127,6 +129,8 @@ export default function PassengersReportPage() {
|
||||
})
|
||||
.sort((a, b) => a.bookingRef.localeCompare(b.bookingRef));
|
||||
|
||||
const { paged: pagedList, page: listPage, totalPages: listTotalPages, setPage: setListPage, reset: resetListPage } = usePagination(filteredList, 50);
|
||||
|
||||
const downloadCsv = (csv: string, filename: string) => {
|
||||
const blob = new Blob([csv], { type: "text/csv" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -457,12 +461,12 @@ export default function PassengersReportPage() {
|
||||
className="input max-w-sm flex-1"
|
||||
placeholder="Search by name or booking ref…"
|
||||
value={listSearch}
|
||||
onChange={(e) => setListSearch(e.target.value)}
|
||||
onChange={(e) => { setListSearch(e.target.value); resetListPage(); }}
|
||||
/>
|
||||
<select
|
||||
className="input w-40"
|
||||
value={filterSeatClass}
|
||||
onChange={(e) => setFilterSeatClass(e.target.value)}
|
||||
onChange={(e) => { setFilterSeatClass(e.target.value); resetListPage(); }}
|
||||
>
|
||||
<option value="">All classes</option>
|
||||
{seatClassOptions.map((c) => (
|
||||
@@ -474,7 +478,7 @@ export default function PassengersReportPage() {
|
||||
<select
|
||||
className="input w-36"
|
||||
value={filterCoachNumber}
|
||||
onChange={(e) => setFilterCoachNumber(e.target.value)}
|
||||
onChange={(e) => { setFilterCoachNumber(e.target.value); resetListPage(); }}
|
||||
>
|
||||
<option value="">All coaches</option>
|
||||
{coachNumberOptions.map((c) => (
|
||||
@@ -486,7 +490,7 @@ export default function PassengersReportPage() {
|
||||
<select
|
||||
className="input w-36"
|
||||
value={filterOrigin}
|
||||
onChange={(e) => setFilterOrigin(e.target.value)}
|
||||
onChange={(e) => { setFilterOrigin(e.target.value); resetListPage(); }}
|
||||
>
|
||||
<option value="">All origins</option>
|
||||
{originOptions.map((o) => (
|
||||
@@ -518,7 +522,7 @@ export default function PassengersReportPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{filteredList.map((p, i) => (
|
||||
{pagedList.map((p, i) => (
|
||||
<tr key={`${p.bookingRef}-${i}`} className="hover:bg-muted/30">
|
||||
<td className="py-2 pr-4 font-medium whitespace-nowrap">{p.passengerName}</td>
|
||||
<td className="py-2 pr-4 text-xs">
|
||||
@@ -549,6 +553,7 @@ export default function PassengersReportPage() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination currentPage={listPage} totalPages={listTotalPages} onPageChange={setListPage} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import DatePicker from '@/components/ui/DatePicker';
|
||||
import { parse, isValid } from 'date-fns';
|
||||
import Pagination from '@/components/ui/Pagination';
|
||||
import { usePagination } from '@/lib/use-pagination';
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -158,6 +160,8 @@ export default function PaymentDiscrepancyPage() {
|
||||
|
||||
const rows = data?.rows ?? [];
|
||||
|
||||
const { paged: pagedRows, page: discPage, totalPages: discTotalPages, setPage: setDiscPage } = usePagination(rows.filter(r => r.balanceMinor > 0), 20);
|
||||
|
||||
const parsedFrom = from ? parse(from, 'yyyy-MM-dd', new Date()) : undefined;
|
||||
const fromDate = parsedFrom && isValid(parsedFrom) ? parsedFrom : undefined;
|
||||
|
||||
@@ -316,7 +320,7 @@ export default function PaymentDiscrepancyPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{rows.filter(row => row.balanceMinor > 0).map((row, i) => {
|
||||
{pagedRows.map((row, i) => {
|
||||
const isExpanded = expandedPnr === row.pnr;
|
||||
const hasMultiple = row.passengerCount > 1;
|
||||
return (
|
||||
@@ -447,8 +451,11 @@ export default function PaymentDiscrepancyPage() {
|
||||
</table>
|
||||
</div>
|
||||
{!isSearchMode && (
|
||||
<div className="px-4 py-3 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-400 dark:text-gray-500">
|
||||
{rows.filter(r => r.balanceMinor > 0).length} record{rows.filter(r => r.balanceMinor > 0).length !== 1 ? 's' : ''} — click a phone number to call directly, or export CSV for bulk follow-up
|
||||
<div className="px-4 py-3 border-t border-gray-100 dark:border-gray-800 flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{rows.filter(r => r.balanceMinor > 0).length} record{rows.filter(r => r.balanceMinor > 0).length !== 1 ? 's' : ''} — click a phone number to call directly, or export CSV for bulk follow-up
|
||||
</span>
|
||||
<Pagination currentPage={discPage} totalPages={discTotalPages} onPageChange={setDiscPage} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { apiClient } from "@/lib/api-client";
|
||||
import Badge from "@/components/ui/Badge";
|
||||
import ActionButton from "@/components/ui/ActionButton";
|
||||
import { formatDateTime, formatCurrency } from "@/lib/utils";
|
||||
import Pagination from "@/components/ui/Pagination";
|
||||
import { usePagination } from "@/lib/use-pagination";
|
||||
|
||||
interface ScheduleOption {
|
||||
id: string;
|
||||
@@ -92,6 +94,9 @@ export default function SeatStatusReportPage() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const { paged: pagedSeats, page: seatsPage, totalPages: seatsTotalPages, setPage: setSeatsPage, reset: resetSeatsPage } = usePagination(filtered, 50);
|
||||
const { paged: pagedBlocked, page: blockedPage, totalPages: blockedTotalPages, setPage: setBlockedPage } = usePagination(data?.blockedSeats ?? [], 50);
|
||||
|
||||
const doExport = () => {
|
||||
if (!filtered.length) return;
|
||||
const headers = ["Booking Ref", "Passenger", "Category", "Seat Class", "Coach", "Seat", "Fare", "Payment", "Booking Status", "Booked At"];
|
||||
@@ -252,12 +257,12 @@ export default function SeatStatusReportPage() {
|
||||
className="input max-w-xs"
|
||||
placeholder="Booking ref, passenger, seat…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onChange={(e) => { setSearch(e.target.value); resetSeatsPage(); }}
|
||||
/>
|
||||
<select
|
||||
className="input w-40"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as "ALL" | "PAID" | "UNPAID")}
|
||||
onChange={(e) => { setStatusFilter(e.target.value as "ALL" | "PAID" | "UNPAID"); resetSeatsPage(); }}
|
||||
>
|
||||
<option value="ALL">All Seats</option>
|
||||
<option value="PAID">Paid Only</option>
|
||||
@@ -280,7 +285,7 @@ export default function SeatStatusReportPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{filtered.map((row, i) => {
|
||||
{pagedSeats.map((row, i) => {
|
||||
const isPaid = row.bookingStatus === "CONFIRMED" || row.bookingStatus === "BOARDED";
|
||||
return (
|
||||
<tr key={i} className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
||||
@@ -306,7 +311,7 @@ export default function SeatStatusReportPage() {
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{filtered.length === 0 && (
|
||||
{pagedSeats.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={7} className="py-8 text-center text-sm text-muted-foreground">
|
||||
No seats found
|
||||
@@ -316,6 +321,7 @@ export default function SeatStatusReportPage() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination currentPage={seatsPage} totalPages={seatsTotalPages} onPageChange={setSeatsPage} />
|
||||
</div>}
|
||||
|
||||
{/* Blocked Seats Tab */}
|
||||
@@ -338,12 +344,12 @@ export default function SeatStatusReportPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{data.blockedSeats.length === 0 && (
|
||||
{pagedBlocked.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-sm text-muted-foreground">No blocked seats</td>
|
||||
</tr>
|
||||
)}
|
||||
{data.blockedSeats.map((b) => (
|
||||
{pagedBlocked.map((b) => (
|
||||
<tr key={b.id} className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
||||
<td className="px-4 py-3 whitespace-nowrap text-xs">
|
||||
<span className="font-medium">{b.seatClassName ?? "—"}</span>
|
||||
@@ -365,6 +371,7 @@ export default function SeatStatusReportPage() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination currentPage={blockedPage} totalPages={blockedTotalPages} onPageChange={setBlockedPage} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -9,6 +9,8 @@ import Modal from '@/components/ui/Modal';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import { routeCoachTemplatesApi } from '@/lib/api';
|
||||
import Pagination from '@/components/ui/Pagination';
|
||||
import { usePagination } from '@/lib/use-pagination';
|
||||
import { formatDateTime } from '@/lib/utils';
|
||||
|
||||
interface Schedule {
|
||||
@@ -368,6 +370,8 @@ export default function SchedulesPage() {
|
||||
);
|
||||
});
|
||||
|
||||
const { paged: pagedSchedules, page: schedulePage, totalPages: scheduleTotalPages, setPage: setSchedulePage } = usePagination(filteredSchedules as Schedule[], 20);
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
SCHEDULED: 'edr-badge-info',
|
||||
BOARDING: 'edr-badge-warning',
|
||||
@@ -621,13 +625,16 @@ export default function SchedulesPage() {
|
||||
No schedules found. {filters.search && 'Try adjusting your search.'}
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
columns={scheduleColumns}
|
||||
data={filteredSchedules}
|
||||
actions={scheduleActions}
|
||||
loading={false}
|
||||
emptyMessage="No schedules found."
|
||||
/>
|
||||
<>
|
||||
<DataTable
|
||||
columns={scheduleColumns}
|
||||
data={pagedSchedules}
|
||||
actions={scheduleActions}
|
||||
loading={false}
|
||||
emptyMessage="No schedules found."
|
||||
/>
|
||||
<Pagination currentPage={schedulePage} totalPages={scheduleTotalPages} onPageChange={setSchedulePage} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,8 @@ import ActionButton from '@/components/ui/ActionButton';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import { stationsApi } from '@/lib/api';
|
||||
import Pagination from '@/components/ui/Pagination';
|
||||
import { usePagination } from '@/lib/use-pagination';
|
||||
|
||||
export default function StationsPage() {
|
||||
const [filters, setFilters] = useState({ search: '', country: '', operational: '' });
|
||||
@@ -90,6 +92,9 @@ export default function StationsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const stationItems = data?.items || [];
|
||||
const { paged: pagedStations, page, totalPages, setPage } = usePagination(stationItems, 20);
|
||||
|
||||
const handleDelete = (station: any) => {
|
||||
setDeleteConfirm({ isOpen: true, station, error: undefined });
|
||||
};
|
||||
@@ -231,12 +236,13 @@ export default function StationsPage() {
|
||||
|
||||
{/* Stations Table */}
|
||||
<DataTable
|
||||
data={data?.items || []}
|
||||
data={pagedStations}
|
||||
columns={columns}
|
||||
actions={actions}
|
||||
loading={isLoading}
|
||||
emptyMessage="No stations found"
|
||||
/>
|
||||
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -11,11 +11,15 @@ 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 Pagination from '@/components/ui/Pagination';
|
||||
import { formatDateTime, formatCurrency, formatDateTimeShort } from '@/lib/utils';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
|
||||
export default function TicketsPage() {
|
||||
const [filters, setFilters] = useState({ search: '', status: '', originStationId: '', destinationStationId: '', departureDate: '', arrivalDate: '', dateFrom: '', dateTo: '', coachId: '' });
|
||||
const [ticketPage, setTicketPage] = useState(1);
|
||||
const resetTicketPage = () => setTicketPage(1);
|
||||
const PAGE_SIZE = 50;
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [ticketToDelete, setTicketToDelete] = useState<any>(null);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
@@ -65,7 +69,7 @@ export default function TicketsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['tickets', filters],
|
||||
queryKey: ['tickets', filters, ticketPage],
|
||||
queryFn: () => ticketsApi.getAll({
|
||||
search: filters.search || undefined,
|
||||
status: filters.status || undefined,
|
||||
@@ -76,11 +80,14 @@ export default function TicketsPage() {
|
||||
dateFrom: filters.dateFrom || undefined,
|
||||
dateTo: filters.dateTo || undefined,
|
||||
coachId: filters.coachId || undefined,
|
||||
skip: 0,
|
||||
take: 50,
|
||||
skip: (ticketPage - 1) * PAGE_SIZE,
|
||||
take: PAGE_SIZE,
|
||||
}),
|
||||
});
|
||||
|
||||
const ticketMeta = (data as any)?.meta;
|
||||
const ticketTotalPages = ticketMeta ? ticketMeta.totalPages : Math.max(1, Math.ceil(((data as any)?.total ?? (data?.items?.length ?? 0)) / PAGE_SIZE));
|
||||
|
||||
const { data: stationsData } = useQuery({
|
||||
queryKey: ['stations'],
|
||||
queryFn: () => stationsApi.getAll(),
|
||||
@@ -562,7 +569,7 @@ export default function TicketsPage() {
|
||||
placeholder="Search by ticket number..."
|
||||
className="input"
|
||||
value={filters.search}
|
||||
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
|
||||
onChange={(e) => { resetTicketPage(); setFilters({ ...filters, search: e.target.value }); }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -570,7 +577,7 @@ export default function TicketsPage() {
|
||||
<select
|
||||
className="input"
|
||||
value={filters.originStationId}
|
||||
onChange={(e) => setFilters({ ...filters, originStationId: e.target.value })}
|
||||
onChange={(e) => { resetTicketPage(); setFilters({ ...filters, originStationId: e.target.value }); }}
|
||||
>
|
||||
<option value="">All Origins</option>
|
||||
{stations.map((station: any) => (
|
||||
@@ -583,7 +590,7 @@ export default function TicketsPage() {
|
||||
<select
|
||||
className="input"
|
||||
value={filters.destinationStationId}
|
||||
onChange={(e) => setFilters({ ...filters, destinationStationId: e.target.value })}
|
||||
onChange={(e) => { resetTicketPage(); setFilters({ ...filters, destinationStationId: e.target.value }); }}
|
||||
>
|
||||
<option value="">All Destinations</option>
|
||||
{stations.map((station: any) => (
|
||||
@@ -597,7 +604,7 @@ export default function TicketsPage() {
|
||||
type="date"
|
||||
className="input"
|
||||
value={filters.departureDate}
|
||||
onChange={(e) => setFilters({ ...filters, departureDate: e.target.value })}
|
||||
onChange={(e) => { resetTicketPage(); setFilters({ ...filters, departureDate: e.target.value }); }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -605,7 +612,7 @@ export default function TicketsPage() {
|
||||
<select
|
||||
className="input"
|
||||
value={filters.coachId}
|
||||
onChange={(e) => setFilters({ ...filters, coachId: e.target.value })}
|
||||
onChange={(e) => { resetTicketPage(); setFilters({ ...filters, coachId: e.target.value }); }}
|
||||
>
|
||||
<option value="">All Coaches</option>
|
||||
{(Array.isArray(coachesData) ? coachesData : (coachesData as any)?.data || []).map((coach: any) => (
|
||||
@@ -627,7 +634,7 @@ export default function TicketsPage() {
|
||||
<select
|
||||
className="input"
|
||||
value={filters.status}
|
||||
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
||||
onChange={(e) => { resetTicketPage(); setFilters({ ...filters, status: e.target.value }); }}
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="ACTIVE">Active</option>
|
||||
@@ -638,12 +645,12 @@ export default function TicketsPage() {
|
||||
<div>
|
||||
<label className="label">Issued From</label>
|
||||
<input type="date" className="input" value={filters.dateFrom}
|
||||
onChange={(e) => setFilters({ ...filters, dateFrom: e.target.value })} />
|
||||
onChange={(e) => { resetTicketPage(); setFilters({ ...filters, dateFrom: e.target.value }); }} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Issued To</label>
|
||||
<input type="date" className="input" value={filters.dateTo}
|
||||
onChange={(e) => setFilters({ ...filters, dateTo: e.target.value })} />
|
||||
onChange={(e) => { resetTicketPage(); setFilters({ ...filters, dateTo: e.target.value }); }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -657,6 +664,7 @@ export default function TicketsPage() {
|
||||
loading={isLoading}
|
||||
emptyMessage="No tickets found"
|
||||
/>
|
||||
<Pagination currentPage={ticketPage} totalPages={ticketTotalPages} onPageChange={setTicketPage} />
|
||||
|
||||
{/* Board Confirmation Modal */}
|
||||
<Modal
|
||||
|
||||
@@ -9,6 +9,8 @@ import Modal from '@/components/ui/Modal';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import { fleetApi } from '@/lib/api';
|
||||
import Pagination from '@/components/ui/Pagination';
|
||||
import { usePagination } from '@/lib/use-pagination';
|
||||
import { Train as TrainType } from '@/types';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
|
||||
@@ -119,6 +121,8 @@ export default function TrainsPage() {
|
||||
);
|
||||
});
|
||||
|
||||
const { paged: pagedTrains, page, totalPages, setPage } = usePagination(filteredTrains as TrainType[], 20);
|
||||
|
||||
const trainColumns = [
|
||||
{
|
||||
key: 'number',
|
||||
@@ -224,12 +228,13 @@ export default function TrainsPage() {
|
||||
|
||||
{/* Trains Table */}
|
||||
<DataTable
|
||||
data={filteredTrains}
|
||||
data={pagedTrains}
|
||||
columns={trainColumns}
|
||||
actions={actions}
|
||||
loading={trainsLoading}
|
||||
emptyMessage={search ? "No trains match your search" : "No trains found"}
|
||||
/>
|
||||
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<ConfirmDialog
|
||||
|
||||
18
apps/edr-passenger-web/backoffice/src/lib/use-pagination.ts
Normal file
18
apps/edr-passenger-web/backoffice/src/lib/use-pagination.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
|
||||
export function usePagination<T>(items: T[], pageSize = 20) {
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
|
||||
const safePage = Math.min(page, totalPages);
|
||||
|
||||
const paged = useMemo(
|
||||
() => items.slice((safePage - 1) * pageSize, safePage * pageSize),
|
||||
[items, safePage, pageSize],
|
||||
);
|
||||
|
||||
// Reset to page 1 whenever the source list changes length (e.g. after a filter)
|
||||
const reset = () => setPage(1);
|
||||
|
||||
return { paged, page: safePage, totalPages, setPage, reset };
|
||||
}
|
||||
Reference in New Issue
Block a user