Merge pull request #895 from Tria-plc/alpha

Tables gagination updates
This commit is contained in:
Stephanos A.
2026-07-22 08:04:22 +03:00
committed by GitHub
11 changed files with 120 additions and 38 deletions

View File

@@ -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}

View File

@@ -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>
)}

View File

@@ -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">

View File

@@ -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>
)}
</>

View File

@@ -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>

View File

@@ -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>
)}
</>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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 };
}