Files
edr-platform/apps/edr-passenger-web/backoffice/src/app/stations/page.tsx
2026-07-22 08:02:23 +03:00

382 lines
13 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Globe, Plus, Edit, Trash2 } from 'lucide-react';
import DataTable from '@/components/ui/DataTable';
import Badge from '@/components/ui/Badge';
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: '' });
const [showModal, setShowModal] = useState(false);
const [editingStation, setEditingStation] = useState<any>(null);
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; station: any | null; error?: string; cascade?: boolean; cascadeChecked?: boolean }>({ isOpen: false, station: null });
const [formError, setFormError] = useState<string | null>(null);
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ['stations', filters],
queryFn: () => stationsApi.getAll(filters),
});
const createMutation = useMutation({
mutationFn: stationsApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['stations'] });
setShowModal(false);
setEditingStation(null);
setFormError(null);
},
onError: (e: any) => setFormError(e?.response?.data?.message || e?.message || 'Failed to create station'),
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: any }) => stationsApi.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['stations'] });
setShowModal(false);
setEditingStation(null);
setFormError(null);
},
onError: (e: any) => setFormError(e?.response?.data?.message || e?.message || 'Failed to update station'),
});
const deleteMutation = useMutation({
mutationFn: ({ id, cascade }: { id: string; cascade?: boolean }) => stationsApi.delete(id, cascade),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['stations'] });
},
onError: (e: any) => {
const msg = e?.response?.data?.message || e?.message || 'Failed to delete station';
const isFkError = msg?.includes('Cannot delete') || e?.response?.status === 400;
if (isFkError && !deleteConfirm.cascade) {
setDeleteConfirm(prev => ({ ...prev, cascade: true, cascadeChecked: false, error: Array.isArray(msg) ? msg.join(' ') : msg }));
} else {
setDeleteConfirm(prev => ({ ...prev, error: Array.isArray(msg) ? msg.join(' ') : msg }));
}
},
});
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const sequence = parseInt(formData.get('sequence') as string);
if (isNaN(sequence)) {
alert('Sequence Number is required');
return;
}
const stationData = {
code: formData.get('code') as string,
name: formData.get('name') as string,
city: formData.get('city') as string,
countryCode: formData.get('countryCode') as string,
lat: parseFloat(formData.get('lat') as string) || undefined,
lng: parseFloat(formData.get('lng') as string) || undefined,
timezone: formData.get('timezone') as string,
sequence,
isOperational: formData.get('isOperational') === 'true',
};
if (editingStation) {
await updateMutation.mutateAsync({ id: editingStation.id, data: stationData });
} else {
await createMutation.mutateAsync(stationData);
}
};
const stationItems = data?.items || [];
const { paged: pagedStations, page, totalPages, setPage } = usePagination(stationItems, 20);
const handleDelete = (station: any) => {
setDeleteConfirm({ isOpen: true, station, error: undefined });
};
const confirmDelete = async () => {
if (!deleteConfirm.station) return;
try {
await deleteMutation.mutateAsync({ id: deleteConfirm.station.id, cascade: deleteConfirm.cascade && deleteConfirm.cascadeChecked });
setDeleteConfirm({ isOpen: false, station: null });
} catch {
// error is set by onError handler
}
};
const columns = [
{
key: 'sequence',
label: 'Sequence',
sortable: true,
render: (station: any) => (
<span className="font-mono font-semibold text-sm">{station.sequence || 0}</span>
),
},
{
key: 'code',
label: 'Code',
sortable: true,
render: (station: any) => (
<span className="font-mono font-semibold">{station.code || 'N/A'}</span>
),
},
{
key: 'name',
label: 'Station Name',
sortable: true,
render: (station: any) => (
<div>
<div className="font-medium">{station.name || 'N/A'}</div>
<div className="text-sm text-muted-foreground">{station.city || 'N/A'}</div>
</div>
),
},
{
key: 'countryCode',
label: 'Country',
render: (station: any) => (
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<span>{station.countryCode || 'N/A'}</span>
</div>
),
},
{
key: 'isOperational',
label: 'Status',
render: (station: any) => (
<Badge variant="status" status={station.isOperational ? 'CONFIRMED' : 'CANCELLED'}>
{station.isOperational ? 'Operational' : 'Closed'}
</Badge>
),
},
];
const actions = [
{
label: 'Edit',
onClick: (station: any) => {
setEditingStation(station);
setFormError(null);
setShowModal(true);
},
variant: 'secondary' as const,
icon: Edit,
},
{
label: 'Delete',
onClick: handleDelete,
variant: 'danger' as const,
icon: Trash2,
},
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground">Stations</h1>
<p className="text-muted-foreground">Manage railway stations and their operational status</p>
</div>
<ActionButton
icon={Plus}
onClick={() => {
setEditingStation(null);
setFormError(null);
setShowModal(true);
}}
>
Add Station
</ActionButton>
</div>
{/* Filters */}
<div className="card">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<input
type="text"
placeholder="Search stations..."
className="input"
value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
/>
</div>
<div>
<select
className="input"
value={filters.country}
onChange={(e) => setFilters({ ...filters, country: e.target.value })}
>
<option value="">All Countries</option>
<option value="ET">Ethiopia</option>
<option value="DJ">Djibouti</option>
</select>
</div>
<div>
<select
className="input"
value={filters.operational}
onChange={(e) => setFilters({ ...filters, operational: e.target.value })}
>
<option value="">All Status</option>
<option value="true">Operational</option>
<option value="false">Closed</option>
</select>
</div>
</div>
</div>
{/* Stations Table */}
<DataTable
data={pagedStations}
columns={columns}
actions={actions}
loading={isLoading}
emptyMessage="No stations found"
/>
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
{/* Delete Confirmation */}
<ConfirmDialog
isOpen={deleteConfirm.isOpen}
onClose={() => setDeleteConfirm({ isOpen: false, station: null })}
onConfirm={confirmDelete}
title="Delete Station"
message={`Are you sure you want to delete ${deleteConfirm.station?.name}?`}
confirmText="Delete"
isDanger={true}
isLoading={deleteMutation.isPending}
error={deleteConfirm.error}
warning={!deleteConfirm.cascade ? "This station may be referenced by routes, schedules, and bookings. Deleting it may impact these systems." : undefined}
cascadeWarning={deleteConfirm.cascade ? "This station has related records (route stops, schedules, or stop times) that will also be permanently deleted." : undefined}
cascadeChecked={deleteConfirm.cascadeChecked}
onCascadeChange={(checked) => setDeleteConfirm(prev => ({ ...prev, cascadeChecked: checked }))}
/>
{/* Add/Edit Modal */}
<Modal
isOpen={showModal}
onClose={() => {
setShowModal(false);
setEditingStation(null);
setFormError(null);
}}
title={`${editingStation ? 'Edit' : 'Add'} Station`}
size="lg"
>
<form onSubmit={handleSubmit} className="space-y-4">
{formError && (
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3 text-sm text-red-800 dark:text-red-200">
{formError}
</div>
)}
{editingStation && (
<div className="rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 p-3 text-sm text-yellow-800 dark:text-yellow-200">
<p className="font-semibold"> Warning</p>
<p className="mt-1">Editing this station may impact routes, schedules, and bookings that reference it. Proceed with caution.</p>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="label">Station Code *</label>
<input
type="text"
name="code"
className="input"
defaultValue={editingStation?.code}
required
placeholder="e.g., ADD"
/>
</div>
<div>
<label className="label">Station Name *</label>
<input
type="text"
name="name"
className="input"
defaultValue={editingStation?.name}
required
placeholder="e.g., Lebu"
/>
</div>
<div>
<label className="label">City *</label>
<input
type="text"
name="city"
className="input"
defaultValue={editingStation?.city}
required
placeholder="e.g., Addis Ababa"
/>
</div>
<div>
<label className="label">Country Code *</label>
<select
name="countryCode"
className="input"
defaultValue={editingStation?.countryCode || 'ET'}
required
>
<option value="ET">Ethiopia (ET)</option>
<option value="DJ">Djibouti (DJ)</option>
</select>
</div>
<div>
<label className="label">Sequence Number *</label>
<input
type="number"
name="sequence"
className="input"
defaultValue={editingStation?.sequence || 0}
min="0"
required
placeholder="e.g., 1"
/>
<p className="text-xs text-muted-foreground mt-1">Used for ordering stations in routes</p>
</div>
<div>
<label className="label">Status</label>
<select
name="isOperational"
className="input"
defaultValue={editingStation?.isOperational?.toString() || 'true'}
>
<option value="true">Operational</option>
<option value="false">Closed</option>
</select>
</div>
</div>
<div className="flex justify-end gap-2 pt-4">
<ActionButton
type="button"
variant="secondary"
onClick={() => {
setShowModal(false);
setEditingStation(null);
}}
>
Cancel
</ActionButton>
<ActionButton
type="submit"
loading={createMutation.isPending || updateMutation.isPending}
>
{editingStation ? 'Update' : 'Create'} Station
</ActionButton>
</div>
</form>
</Modal>
</div>
);
}