mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
1169 lines
46 KiB
TypeScript
1169 lines
46 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { Plus, Trash2, Loader2, Edit } from 'lucide-react';
|
|
import DataTable from '@/components/ui/DataTable';
|
|
import Modal from '@/components/ui/Modal';
|
|
import ActionButton from '@/components/ui/ActionButton';
|
|
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
import { apiClient } from '@/lib/api-client';
|
|
|
|
interface Schedule {
|
|
id: string;
|
|
trainId: string;
|
|
departureAt: string;
|
|
arrivalAt: string;
|
|
train?: { id: string; name: string; number: string };
|
|
originStation?: { id: string; name: string };
|
|
destinationStation?: { id: string; name: string };
|
|
}
|
|
|
|
interface Route {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
stops?: Array<{ id: string; sequence: number; stationId: string; station?: { name: string } }>;
|
|
}
|
|
|
|
interface SeatClass {
|
|
id: string;
|
|
name: string;
|
|
baseFare?: number;
|
|
baseFareMinor?: number;
|
|
}
|
|
|
|
export default function PricingPage() {
|
|
const [tab, setTab] = useState<'schedule' | 'segment' | 'baggage'>('schedule');
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [selectedSchedule, setSelectedSchedule] = useState<string>('');
|
|
const [selectedRoute, setSelectedRoute] = useState<string>('');
|
|
const [editingFare, setEditingFare] = useState<any>(null);
|
|
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; id: string | null }>({
|
|
isOpen: false,
|
|
id: null,
|
|
});
|
|
const [error, setError] = useState<string | null>(null);
|
|
const queryClient = useQueryClient();
|
|
|
|
const [fareForm, setFareForm] = useState({
|
|
seatClassId: '',
|
|
baseFare: '',
|
|
nationality: '',
|
|
passengerCategory: '',
|
|
route: '',
|
|
validFrom: new Date().toISOString().split('T')[0],
|
|
validUntil: '',
|
|
});
|
|
|
|
const [segmentForm, setSegmentForm] = useState({
|
|
seatClassId: '',
|
|
originStationId: '',
|
|
destinationStationId: '',
|
|
baseFare: '',
|
|
nationality: '',
|
|
passengerCategory: '',
|
|
validFrom: new Date().toISOString().split('T')[0],
|
|
validUntil: '',
|
|
});
|
|
|
|
const [baggageForm, setBaggageForm] = useState({
|
|
seatClassId: '',
|
|
maxWeightKg: '',
|
|
maxPiecesCount: '',
|
|
excessFeePerKg: '',
|
|
});
|
|
const [editingAllowance, setEditingAllowance] = useState<any>(null);
|
|
const [baggageError, setBaggageError] = useState<string | null>(null);
|
|
const [baggageModal, setBaggageModal] = useState(false);
|
|
const [deleteAllowanceConfirm, setDeleteAllowanceConfirm] = useState<{ isOpen: boolean; id: string | null }>({ isOpen: false, id: null });
|
|
|
|
const { data: schedules = [] } = useQuery({
|
|
queryKey: ['schedules'],
|
|
queryFn: () => apiClient.get('/schedules'),
|
|
});
|
|
|
|
const { data: routes = [] } = useQuery({
|
|
queryKey: ['routes'],
|
|
queryFn: () => apiClient.get('/routes'),
|
|
});
|
|
|
|
const { data: seatClasses = [] } = useQuery({
|
|
queryKey: ['seat-classes'],
|
|
queryFn: () => apiClient.get('/seat-classes'),
|
|
});
|
|
|
|
const { data: stations = [] } = useQuery({
|
|
queryKey: ['stations'],
|
|
queryFn: () => apiClient.get('/stations'),
|
|
});
|
|
|
|
const { data: fares = [], isLoading: faresLoading, refetch: refetchFares } = useQuery({
|
|
queryKey: ['schedule-fares', selectedSchedule],
|
|
queryFn: async () => {
|
|
if (!selectedSchedule) return [];
|
|
try {
|
|
const response = await apiClient.get(`/schedules/${selectedSchedule}/fares/all`);
|
|
return Array.isArray(response) ? response : (response as any)?.data || [];
|
|
} catch (err: any) {
|
|
const errMsg = err.response?.data?.message || err.message || 'Failed to load fares';
|
|
setError(`Error loading fares: ${errMsg}`);
|
|
return [];
|
|
}
|
|
},
|
|
enabled: !!selectedSchedule && tab === 'schedule',
|
|
});
|
|
|
|
const { data: segmentFares = [], isLoading: segmentFaresLoading, refetch: refetchSegmentFares } = useQuery({
|
|
queryKey: ['segment-fares', selectedRoute],
|
|
queryFn: () => (selectedRoute ? apiClient.get(`/schedules/routes/${selectedRoute}/segment-fares`) : Promise.resolve([])),
|
|
enabled: !!selectedRoute && tab === 'segment',
|
|
});
|
|
|
|
const { data: allowances = [], isLoading: allowancesLoading, refetch: refetchAllowances } = useQuery({
|
|
queryKey: ['baggage-allowances'],
|
|
queryFn: () => apiClient.get<any[]>('/agents/excess-baggage/allowances'),
|
|
enabled: tab === 'baggage',
|
|
});
|
|
|
|
const createAllowanceMutation = useMutation({
|
|
mutationFn: (data: any) => apiClient.post('/agents/excess-baggage/allowances', data),
|
|
onSuccess: () => { refetchAllowances(); setBaggageModal(false); setBaggageError(null); },
|
|
onError: (e: any) => setBaggageError(e?.response?.data?.message || 'Failed to save'),
|
|
});
|
|
|
|
const updateAllowanceMutation = useMutation({
|
|
mutationFn: ({ id, ...data }: any) => apiClient.patch(`/agents/excess-baggage/allowances/${id}`, data),
|
|
onSuccess: () => { refetchAllowances(); setBaggageModal(false); setEditingAllowance(null); setBaggageError(null); },
|
|
onError: (e: any) => setBaggageError(e?.response?.data?.message || 'Failed to update'),
|
|
});
|
|
|
|
const deleteAllowanceMutation = useMutation({
|
|
mutationFn: (id: string) => apiClient.delete(`/agents/excess-baggage/allowances/${id}`),
|
|
onSuccess: () => { refetchAllowances(); setDeleteAllowanceConfirm({ isOpen: false, id: null }); },
|
|
onError: (e: any) => setBaggageError(e?.response?.data?.message || 'Failed to delete'),
|
|
});
|
|
|
|
const handleSaveAllowance = async () => {
|
|
setBaggageError(null);
|
|
if (!baggageForm.seatClassId || !baggageForm.maxWeightKg || !baggageForm.maxPiecesCount || !baggageForm.excessFeePerKg) {
|
|
setBaggageError('All fields are required'); return;
|
|
}
|
|
const payload = {
|
|
seatClassId: baggageForm.seatClassId,
|
|
maxWeightKg: parseInt(baggageForm.maxWeightKg),
|
|
maxPiecesCount: parseInt(baggageForm.maxPiecesCount),
|
|
excessFeePerKg: Math.round(parseFloat(baggageForm.excessFeePerKg) * 100),
|
|
};
|
|
if (editingAllowance) {
|
|
await updateAllowanceMutation.mutateAsync({ id: editingAllowance.id, ...payload });
|
|
} else {
|
|
await createAllowanceMutation.mutateAsync(payload);
|
|
}
|
|
};
|
|
|
|
const createFareMutation = useMutation({
|
|
mutationFn: (data: any) => apiClient.post(`/schedules/fares`, data),
|
|
onSuccess: () => {
|
|
refetchFares();
|
|
resetForm();
|
|
setError(null);
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to create fare rule');
|
|
},
|
|
});
|
|
|
|
const updateFareMutation = useMutation({
|
|
mutationFn: (data: any) => apiClient.patch(`/schedules/fares/${data.id}`, data),
|
|
onSuccess: () => {
|
|
refetchFares();
|
|
setEditingFare(null);
|
|
resetForm();
|
|
setError(null);
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to update fare rule');
|
|
},
|
|
});
|
|
|
|
const deleteFareMutation = useMutation({
|
|
mutationFn: (id: string) => apiClient.delete(`/schedules/fares/${id}`),
|
|
onSuccess: () => {
|
|
refetchFares();
|
|
setDeleteConfirm({ isOpen: false, id: null });
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to delete fare rule');
|
|
},
|
|
});
|
|
|
|
const createSegmentFareMutation = useMutation({
|
|
mutationFn: (data: any) => apiClient.post(`/schedules/segment-fares`, data),
|
|
onSuccess: () => {
|
|
refetchSegmentFares();
|
|
resetSegmentForm();
|
|
setError(null);
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to create segment fare rule');
|
|
},
|
|
});
|
|
|
|
const updateSegmentFareMutation = useMutation({
|
|
mutationFn: (data: any) => apiClient.patch(`/schedules/segment-fares/${data.id}`, data),
|
|
onSuccess: () => {
|
|
refetchSegmentFares();
|
|
setEditingFare(null);
|
|
resetSegmentForm();
|
|
setError(null);
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to update segment fare rule');
|
|
},
|
|
});
|
|
|
|
const deleteSegmentFareMutation = useMutation({
|
|
mutationFn: (id: string) => apiClient.delete(`/schedules/segment-fares/${id}`),
|
|
onSuccess: () => {
|
|
refetchSegmentFares();
|
|
setDeleteConfirm({ isOpen: false, id: null });
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to delete segment fare rule');
|
|
},
|
|
});
|
|
|
|
const resetForm = () => {
|
|
setFareForm({
|
|
seatClassId: '',
|
|
baseFare: '',
|
|
nationality: '',
|
|
passengerCategory: '',
|
|
route: '',
|
|
validFrom: new Date().toISOString().split('T')[0],
|
|
validUntil: '',
|
|
});
|
|
setEditingFare(null);
|
|
setShowModal(false);
|
|
};
|
|
|
|
const resetSegmentForm = () => {
|
|
setSegmentForm({
|
|
seatClassId: '',
|
|
originStationId: '',
|
|
destinationStationId: '',
|
|
baseFare: '',
|
|
nationality: '',
|
|
passengerCategory: '',
|
|
validFrom: new Date().toISOString().split('T')[0],
|
|
validUntil: '',
|
|
});
|
|
setEditingFare(null);
|
|
setShowModal(false);
|
|
};
|
|
|
|
const handleEditFare = (fare: any) => {
|
|
setEditingFare(fare);
|
|
setFareForm({
|
|
seatClassId: fare.seatClassId || '',
|
|
baseFare: (fare.baseFare || fare.baseFareMinor || 0).toString(),
|
|
nationality: fare.nationality || '',
|
|
passengerCategory: fare.passengerCategory || '',
|
|
route: fare.route || '',
|
|
validFrom: fare.validFrom ? new Date(fare.validFrom).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
|
validUntil: fare.validUntil ? new Date(fare.validUntil).toISOString().split('T')[0] : '',
|
|
});
|
|
setError(null);
|
|
setShowModal(true);
|
|
};
|
|
|
|
const handleEditSegmentFare = (fare: any) => {
|
|
setEditingFare(fare);
|
|
const routeStops = currentRoute?.stops || [];
|
|
const originStop = routeStops.find((s: any) => s.sequence === fare.originStopSequence);
|
|
const destStop = routeStops.find((s: any) => s.sequence === fare.destinationStopSequence);
|
|
|
|
setSegmentForm({
|
|
seatClassId: fare.seatClassId || '',
|
|
originStationId: originStop?.stationId || '',
|
|
destinationStationId: destStop?.stationId || '',
|
|
baseFare: (fare.baseFare || fare.baseFareMinor || 0).toString(),
|
|
nationality: fare.nationality || '',
|
|
passengerCategory: fare.passengerCategory || '',
|
|
validFrom: fare.validFrom ? new Date(fare.validFrom).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
|
validUntil: fare.validUntil ? new Date(fare.validUntil).toISOString().split('T')[0] : '',
|
|
});
|
|
setError(null);
|
|
setShowModal(true);
|
|
};
|
|
|
|
const handleSaveFare = async () => {
|
|
setError(null);
|
|
if (!fareForm.seatClassId || !fareForm.baseFare) {
|
|
setError('Seat class and base fare are required');
|
|
return;
|
|
}
|
|
|
|
const baseFareMinor = parseInt(fareForm.baseFare, 10);
|
|
|
|
if (editingFare) {
|
|
await updateFareMutation.mutateAsync({
|
|
id: editingFare.id,
|
|
seatClassId: fareForm.seatClassId,
|
|
baseFareMinor,
|
|
nationality: fareForm.nationality || undefined,
|
|
passengerCategory: fareForm.passengerCategory || undefined,
|
|
route: fareForm.route || undefined,
|
|
validFrom: fareForm.validFrom,
|
|
validUntil: fareForm.validUntil || undefined,
|
|
});
|
|
} else {
|
|
await createFareMutation.mutateAsync({
|
|
scheduleId: selectedSchedule || undefined,
|
|
seatClassId: fareForm.seatClassId,
|
|
baseFareMinor,
|
|
nationality: fareForm.nationality || undefined,
|
|
passengerCategory: fareForm.passengerCategory || undefined,
|
|
route: fareForm.route || undefined,
|
|
validFrom: fareForm.validFrom,
|
|
validUntil: fareForm.validUntil || undefined,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSaveSegmentFare = async () => {
|
|
setError(null);
|
|
if (!segmentForm.seatClassId || !segmentForm.baseFare || !segmentForm.originStationId || !segmentForm.destinationStationId) {
|
|
setError('Seat class, origin station, destination station, and fare are required');
|
|
return;
|
|
}
|
|
|
|
const routeStops = currentRoute?.stops || [];
|
|
const originStop = routeStops.find((s: any) => s.stationId === segmentForm.originStationId);
|
|
const destStop = routeStops.find((s: any) => s.stationId === segmentForm.destinationStationId);
|
|
|
|
if (!originStop || !destStop) {
|
|
setError('Selected stations must be on the route');
|
|
return;
|
|
}
|
|
|
|
if (originStop.sequence >= destStop.sequence) {
|
|
setError('Destination station must be after origin station');
|
|
return;
|
|
}
|
|
|
|
const baseFareMinor = parseInt(segmentForm.baseFare, 10);
|
|
|
|
if (editingFare) {
|
|
await updateSegmentFareMutation.mutateAsync({
|
|
id: editingFare.id,
|
|
routeId: selectedRoute,
|
|
seatClassId: segmentForm.seatClassId,
|
|
originStopSequence: originStop.sequence,
|
|
destinationStopSequence: destStop.sequence,
|
|
baseFareMinor,
|
|
nationality: segmentForm.nationality || undefined,
|
|
passengerCategory: segmentForm.passengerCategory || undefined,
|
|
validFrom: segmentForm.validFrom,
|
|
validUntil: segmentForm.validUntil || undefined,
|
|
});
|
|
} else {
|
|
await createSegmentFareMutation.mutateAsync({
|
|
routeId: selectedRoute,
|
|
seatClassId: segmentForm.seatClassId,
|
|
originStopSequence: originStop.sequence,
|
|
destinationStopSequence: destStop.sequence,
|
|
baseFareMinor,
|
|
nationality: segmentForm.nationality || undefined,
|
|
passengerCategory: segmentForm.passengerCategory || undefined,
|
|
validFrom: segmentForm.validFrom,
|
|
validUntil: segmentForm.validUntil || undefined,
|
|
});
|
|
}
|
|
};
|
|
|
|
const confirmDelete = async () => {
|
|
if (deleteConfirm.id) {
|
|
if (tab === 'schedule') {
|
|
await deleteFareMutation.mutateAsync(deleteConfirm.id);
|
|
} else {
|
|
await deleteSegmentFareMutation.mutateAsync(deleteConfirm.id);
|
|
}
|
|
}
|
|
};
|
|
|
|
const schedulesArray = Array.isArray(schedules) ? schedules : (schedules as any)?.items || [];
|
|
const routesArray = Array.isArray(routes) ? routes : (routes as any)?.items || [];
|
|
const seatClassesArray = Array.isArray(seatClasses) ? seatClasses : (seatClasses as any)?.items || [];
|
|
const stationsArray = Array.isArray(stations) ? stations : (stations as any)?.items || [];
|
|
const faresArray = Array.isArray(fares) ? fares : (fares as any)?.items || [];
|
|
const segmentFaresArray = Array.isArray(segmentFares) ? segmentFares : (segmentFares as any)?.items || [];
|
|
const allowancesArray = Array.isArray(allowances) ? allowances : (allowances as any)?.items || [];
|
|
const currentRoute = routesArray.find((r: Route) => r.id === selectedRoute);
|
|
|
|
const fareColumns = [
|
|
{
|
|
key: 'seatClass',
|
|
label: 'Seat Class',
|
|
render: (fare: any) => {
|
|
const className = fare.seatClass?.name || fare.seatClassName || 'N/A';
|
|
return <span className="font-medium">{className}</span>;
|
|
},
|
|
},
|
|
{
|
|
key: 'passengerCategory',
|
|
label: 'Passenger Type',
|
|
render: (fare: any) => (
|
|
<span className="text-sm">{fare.passengerCategory || 'All'}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'baseFare',
|
|
label: 'Fare (ETB)',
|
|
render: (fare: any) => {
|
|
const fareValue = fare.baseFare || fare.baseFareMinor;
|
|
if (!fareValue && fareValue !== 0) return <span>N/A</span>;
|
|
return <span className="font-mono font-medium">{fareValue} ETB</span>;
|
|
},
|
|
},
|
|
{
|
|
key: 'nationality',
|
|
label: 'Nationality',
|
|
render: (fare: any) => (
|
|
<span className="text-sm">{fare.nationality || 'All'}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'route',
|
|
label: 'Route',
|
|
render: (fare: any) => (
|
|
<span className="text-sm font-mono">{fare.route || '-'}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'validFrom',
|
|
label: 'Valid From',
|
|
render: (fare: any) => {
|
|
if (!fare.validFrom) return <span className="text-muted-foreground text-sm">Not set</span>;
|
|
return <span className="text-sm">{new Date(fare.validFrom).toLocaleDateString()}</span>;
|
|
},
|
|
},
|
|
{
|
|
key: 'validUntil',
|
|
label: 'Valid Until',
|
|
render: (fare: any) => {
|
|
if (!fare.validUntil) return <span className="text-sm text-muted-foreground">Indefinite</span>;
|
|
return <span className="text-sm">{new Date(fare.validUntil).toLocaleDateString()}</span>;
|
|
},
|
|
},
|
|
];
|
|
|
|
const segmentFareColumns = [
|
|
{
|
|
key: 'segment',
|
|
label: 'Segment',
|
|
render: (fare: any) => {
|
|
const stops = currentRoute?.stops || [];
|
|
const originStop = stops.find((s: any) => s.sequence === fare.originStopSequence);
|
|
const destStop = stops.find((s: any) => s.sequence === fare.destinationStopSequence);
|
|
return (
|
|
<span className="text-sm font-medium">
|
|
Stop {fare.originStopSequence} → {fare.destinationStopSequence}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
key: 'seatClass',
|
|
label: 'Seat Class',
|
|
render: (fare: any) => {
|
|
const className = fare.seatClass?.name || 'N/A';
|
|
return <span className="font-medium">{className}</span>;
|
|
},
|
|
},
|
|
{
|
|
key: 'passengerCategory',
|
|
label: 'Passenger Type',
|
|
render: (fare: any) => (
|
|
<span className="text-sm">{fare.passengerCategory || 'All'}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'baseFare',
|
|
label: 'Fare (ETB)',
|
|
render: (fare: any) => {
|
|
const fareValue = fare.baseFare || fare.baseFareMinor;
|
|
if (!fareValue && fareValue !== 0) return <span>N/A</span>;
|
|
return <span className="font-mono font-medium">{fareValue} ETB</span>;
|
|
},
|
|
},
|
|
{
|
|
key: 'nationality',
|
|
label: 'Nationality',
|
|
render: (fare: any) => (
|
|
<span className="text-sm">{fare.nationality || 'All'}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'validFrom',
|
|
label: 'Valid From',
|
|
render: (fare: any) => {
|
|
if (!fare.validFrom) return <span className="text-muted-foreground text-sm">Not set</span>;
|
|
return <span className="text-sm">{new Date(fare.validFrom).toLocaleDateString()}</span>;
|
|
},
|
|
},
|
|
{
|
|
key: 'validUntil',
|
|
label: 'Valid Until',
|
|
render: (fare: any) => {
|
|
if (!fare.validUntil) return <span className="text-sm text-muted-foreground">Indefinite</span>;
|
|
return <span className="text-sm">{new Date(fare.validUntil).toLocaleDateString()}</span>;
|
|
},
|
|
},
|
|
];
|
|
|
|
const fareActions = [
|
|
{
|
|
label: 'Edit',
|
|
onClick: tab === 'schedule' ? handleEditFare : handleEditSegmentFare,
|
|
variant: 'secondary' as const,
|
|
icon: Edit,
|
|
disabled: tab === 'schedule', // Schedule fares are computed, not stored
|
|
},
|
|
{
|
|
label: 'Delete',
|
|
onClick: (fare: any) => setDeleteConfirm({ isOpen: true, id: fare.id }),
|
|
variant: 'danger' as const,
|
|
icon: Trash2,
|
|
disabled: tab === 'schedule', // Schedule fares are computed, not stored
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-foreground">Pricing & Fares</h1>
|
|
<p className="text-muted-foreground mt-1">Manage fares by schedule and route segments with passenger type pricing</p>
|
|
</div>
|
|
<ActionButton
|
|
icon={Plus}
|
|
onClick={() => {
|
|
setError(null);
|
|
setEditingFare(null);
|
|
if (tab === 'baggage') {
|
|
setBaggageForm({ seatClassId: '', maxWeightKg: '', maxPiecesCount: '', excessFeePerKg: '' });
|
|
setEditingAllowance(null);
|
|
setBaggageError(null);
|
|
setBaggageModal(true);
|
|
} else if (tab === 'schedule') {
|
|
setFareForm({
|
|
seatClassId: '',
|
|
baseFare: '',
|
|
nationality: '',
|
|
passengerCategory: '',
|
|
route: '',
|
|
validFrom: new Date().toISOString().split('T')[0],
|
|
validUntil: '',
|
|
});
|
|
} else {
|
|
setSegmentForm({
|
|
seatClassId: '',
|
|
originStationId: '',
|
|
destinationStationId: '',
|
|
baseFare: '',
|
|
nationality: '',
|
|
passengerCategory: '',
|
|
validFrom: new Date().toISOString().split('T')[0],
|
|
validUntil: '',
|
|
});
|
|
}
|
|
setShowModal(true);
|
|
}}
|
|
>
|
|
{tab === 'baggage' ? 'Add Allowance Rule' : 'Add Fare Rule'}
|
|
</ActionButton>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<div className="flex gap-4 border-b mb-6">
|
|
<button
|
|
onClick={() => {
|
|
setTab('schedule');
|
|
setError(null);
|
|
}}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
tab === 'schedule' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground'
|
|
}`}
|
|
>
|
|
Schedule Fares
|
|
</button>
|
|
<button
|
|
onClick={() => { setTab('segment'); setError(null); }}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
tab === 'segment' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground'
|
|
}`}
|
|
>
|
|
Segment Fares
|
|
</button>
|
|
<button
|
|
onClick={() => { setTab('baggage'); setError(null); }}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
tab === 'baggage' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground'
|
|
}`}
|
|
>
|
|
Excess Baggage Rates
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
{tab === 'schedule' && (
|
|
<>
|
|
<div>
|
|
<label className="label">Select Schedule</label>
|
|
<select
|
|
value={selectedSchedule}
|
|
onChange={(e) => {
|
|
setSelectedSchedule(e.target.value);
|
|
setError(null);
|
|
}}
|
|
className="input w-full max-w-md"
|
|
>
|
|
<option value="">Choose a schedule...</option>
|
|
{schedulesArray.map((schedule: Schedule) => (
|
|
<option key={schedule.id} value={schedule.id}>
|
|
{schedule.train?.name} ({schedule.train?.number}) - {schedule.originStation?.name} to {schedule.destinationStation?.name} ({new Date(schedule.departureAt).toLocaleDateString()})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{selectedSchedule && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-4">Calculated Fares</h3>
|
|
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded text-sm text-blue-800 dark:text-blue-200">
|
|
These are <strong>dynamically calculated</strong> fares based on the fare engine. To create custom override fares, click "Add Fare Rule" above.
|
|
</div>
|
|
{faresLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="h-6 w-6 animate-spin" />
|
|
</div>
|
|
) : faresArray.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
No fares available for this schedule.
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="mb-4 p-3 bg-muted/50 rounded text-sm text-muted-foreground">
|
|
{faresArray.length} seat class(es) available
|
|
</div>
|
|
<DataTable
|
|
data={faresArray}
|
|
columns={fareColumns}
|
|
actions={fareActions}
|
|
loading={false}
|
|
emptyMessage="No fares available."
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{tab === 'segment' && (
|
|
<>
|
|
<div>
|
|
<label className="label">Select Route</label>
|
|
<select
|
|
value={selectedRoute}
|
|
onChange={(e) => {
|
|
setSelectedRoute(e.target.value);
|
|
setError(null);
|
|
}}
|
|
className="input w-full max-w-md"
|
|
>
|
|
<option value="">Choose a route...</option>
|
|
{routesArray.map((route: Route) => (
|
|
<option key={route.id} value={route.id}>
|
|
{route.code} - {route.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{selectedRoute && (
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-4">Segment Fare Rules</h3>
|
|
{segmentFaresLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="h-6 w-6 animate-spin" />
|
|
</div>
|
|
) : segmentFaresArray.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
{`No segment fares defined. Click "Add Fare Rule" to create one.`}
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="mb-4 p-3 bg-muted/50 rounded text-sm text-muted-foreground">
|
|
{segmentFaresArray.length} segment fare rule(s) found
|
|
</div>
|
|
<DataTable
|
|
data={segmentFaresArray}
|
|
columns={segmentFareColumns}
|
|
actions={fareActions}
|
|
loading={false}
|
|
emptyMessage="No segment fares found."
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
{tab === 'baggage' && (
|
|
<>
|
|
{allowancesLoading ? (
|
|
<div className="flex items-center justify-center py-8"><Loader2 className="h-6 w-6 animate-spin" /></div>
|
|
) : allowancesArray.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
No baggage allowance rules defined. Click "Add Allowance Rule" to create one.
|
|
</div>
|
|
) : (
|
|
<DataTable
|
|
data={allowancesArray}
|
|
columns={[
|
|
{ key: 'seatClass', label: 'Seat Class', render: (a: any) => <span className="font-medium">{a.seatClass?.name ?? a.seatClassId}</span> },
|
|
{ key: 'maxWeightKg', label: 'Free Allowance', render: (a: any) => <span>{a.maxWeightKg} kg, {a.maxPiecesCount} pcs</span> },
|
|
{ key: 'excessFeePerKg', label: 'Excess Fee / kg', render: (a: any) => <span className="font-mono font-semibold">{(a.excessFeePerKg / 100).toFixed(2)} ETB</span> },
|
|
]}
|
|
actions={[
|
|
{
|
|
label: 'Edit', icon: Edit, variant: 'secondary' as const,
|
|
onClick: (a: any) => {
|
|
setEditingAllowance(a);
|
|
setBaggageForm({
|
|
seatClassId: a.seatClassId,
|
|
maxWeightKg: String(a.maxWeightKg),
|
|
maxPiecesCount: String(a.maxPiecesCount),
|
|
excessFeePerKg: (a.excessFeePerKg / 100).toFixed(2),
|
|
});
|
|
setBaggageError(null);
|
|
setBaggageModal(true);
|
|
},
|
|
},
|
|
{
|
|
label: 'Delete', icon: Trash2, variant: 'danger' as const,
|
|
onClick: (a: any) => setDeleteAllowanceConfirm({ isOpen: true, id: a.id }),
|
|
},
|
|
]}
|
|
loading={false}
|
|
emptyMessage="No allowance rules found."
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
|
|
<h3 className="font-semibold text-blue-900 dark:text-blue-200 mb-3">Pricing Structure</h3>
|
|
<ul className="text-sm text-blue-800 dark:text-blue-300 space-y-2">
|
|
<li>
|
|
• <strong>Schedule Fares:</strong> Set custom pricing for each schedule by seat class and passenger type
|
|
</li>
|
|
<li>
|
|
• <strong>Segment Fares:</strong> Set fares for specific stop-to-stop segments (e.g., Addis → Dire Dawa)
|
|
</li>
|
|
<li>
|
|
• <strong>Passenger Type:</strong> ADULT (5+ years) or CHILD (<5) — first child travels free, subsequent children pay full fare
|
|
</li>
|
|
<li>
|
|
• <strong>Nationality-based:</strong> Override fares for specific nationalities (Ethiopian, Djiboutian, Other)
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Delete Confirmation */}
|
|
<ConfirmDialog
|
|
isOpen={deleteConfirm.isOpen}
|
|
onClose={() => setDeleteConfirm({ isOpen: false, id: null })}
|
|
onConfirm={confirmDelete}
|
|
title={`Delete ${tab === 'schedule' ? 'Fare Rule' : 'Segment Fare Rule'}`}
|
|
message={`Are you sure you want to delete this ${tab === 'schedule' ? 'fare rule' : 'segment fare rule'}?`}
|
|
confirmText="Delete"
|
|
isDanger={true}
|
|
warning="This action cannot be undone."
|
|
/>
|
|
|
|
{/* Add/Edit Modal */}
|
|
<Modal
|
|
isOpen={showModal}
|
|
onClose={() => {
|
|
if (tab === 'schedule') resetForm();
|
|
else resetSegmentForm();
|
|
}}
|
|
title={`${editingFare ? 'Edit' : 'Add'} ${tab === 'schedule' ? 'Fare Rule' : 'Segment Fare Rule'}`}
|
|
size="lg"
|
|
>
|
|
<div className="space-y-4">
|
|
{error && (
|
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3 rounded-lg text-sm text-red-800 dark:text-red-200">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{tab === 'schedule' && (
|
|
<>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Schedule {editingFare ? '(View only)' : '(Optional)'}</label>
|
|
<select
|
|
value={selectedSchedule}
|
|
onChange={(e) => !editingFare && setSelectedSchedule(e.target.value)}
|
|
className="input w-full"
|
|
disabled={!!editingFare}
|
|
>
|
|
<option value="">Not scoped to schedule</option>
|
|
{schedulesArray.map((schedule: Schedule) => (
|
|
<option key={schedule.id} value={schedule.id}>
|
|
{schedule.train?.name} ({schedule.train?.number}) - {new Date(schedule.departureAt).toLocaleDateString()}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<p className="text-xs text-muted-foreground mt-1">Leave empty to apply to all schedules</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Route Code (Optional)</label>
|
|
<select
|
|
value={fareForm.route}
|
|
onChange={(e) => setFareForm({ ...fareForm, route: e.target.value })}
|
|
className="input w-full"
|
|
>
|
|
<option value="">All routes</option>
|
|
{routesArray.map((route: Route) => (
|
|
<option key={route.id} value={route.code}>
|
|
{route.code} — {route.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<p className="text-xs text-muted-foreground mt-1">Scope this fare to a specific route</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Seat Class *</label>
|
|
<select
|
|
value={fareForm.seatClassId}
|
|
onChange={(e) => setFareForm({ ...fareForm, seatClassId: e.target.value })}
|
|
className="input w-full"
|
|
required
|
|
>
|
|
<option value="">Select seat class...</option>
|
|
{seatClassesArray.map((sc: SeatClass) => (
|
|
<option key={sc.id} value={sc.id}>
|
|
{sc.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Fare (ETB) *</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
step="1"
|
|
value={fareForm.baseFare}
|
|
onChange={(e) => setFareForm({ ...fareForm, baseFare: e.target.value })}
|
|
className="input w-full"
|
|
placeholder="e.g., 350"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Passenger Type (Optional)</label>
|
|
<select
|
|
value={fareForm.passengerCategory}
|
|
onChange={(e) => setFareForm({ ...fareForm, passengerCategory: e.target.value })}
|
|
className="input w-full"
|
|
>
|
|
<option value="">All Passenger Types</option>
|
|
<option value="ADULT">Adult (5+ years)</option>
|
|
<option value="CHILD">Child (Less than 5 years)</option>
|
|
</select>
|
|
<p className="text-xs text-muted-foreground mt-1">Scope pricing to specific passenger type</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Nationality (Optional)</label>
|
|
<select
|
|
value={fareForm.nationality}
|
|
onChange={(e) => setFareForm({ ...fareForm, nationality: e.target.value })}
|
|
className="input w-full"
|
|
>
|
|
<option value="">All Nationalities</option>
|
|
<option value="Ethiopian">Ethiopian</option>
|
|
<option value="Djiboutian">Djiboutian</option>
|
|
<option value="Other">Other</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Valid From *</label>
|
|
<input
|
|
type="date"
|
|
value={fareForm.validFrom}
|
|
onChange={(e) => setFareForm({ ...fareForm, validFrom: e.target.value })}
|
|
className="input w-full"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Valid Until (Optional)</label>
|
|
<input
|
|
type="date"
|
|
value={fareForm.validUntil}
|
|
onChange={(e) => setFareForm({ ...fareForm, validUntil: e.target.value })}
|
|
className="input w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{tab === 'segment' && (
|
|
<>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Origin Station *</label>
|
|
<select
|
|
value={segmentForm.originStationId}
|
|
onChange={(e) => setSegmentForm({ ...segmentForm, originStationId: e.target.value })}
|
|
className="input w-full"
|
|
required
|
|
>
|
|
<option value="">Select origin station...</option>
|
|
{currentRoute?.stops?.map((stop: any) => {
|
|
const station = stationsArray.find((s: any) => s.id === stop.stationId);
|
|
return (
|
|
<option key={stop.id} value={stop.stationId}>
|
|
Stop {stop.sequence}: {station?.name || 'Unknown'}
|
|
</option>
|
|
);
|
|
})}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="label">Destination Station *</label>
|
|
<select
|
|
value={segmentForm.destinationStationId}
|
|
onChange={(e) => setSegmentForm({ ...segmentForm, destinationStationId: e.target.value })}
|
|
className="input w-full"
|
|
required
|
|
>
|
|
<option value="">Select destination station...</option>
|
|
{currentRoute?.stops?.map((stop: any) => {
|
|
const station = stationsArray.find((s: any) => s.id === stop.stationId);
|
|
return (
|
|
<option key={stop.id} value={stop.stationId}>
|
|
Stop {stop.sequence}: {station?.name || 'Unknown'}
|
|
</option>
|
|
);
|
|
})}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Seat Class *</label>
|
|
<select
|
|
value={segmentForm.seatClassId}
|
|
onChange={(e) => setSegmentForm({ ...segmentForm, seatClassId: e.target.value })}
|
|
className="input w-full"
|
|
required
|
|
>
|
|
<option value="">Select seat class...</option>
|
|
{seatClassesArray.map((sc: SeatClass) => (
|
|
<option key={sc.id} value={sc.id}>
|
|
{sc.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Fare (ETB) *</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
step="1"
|
|
value={segmentForm.baseFare}
|
|
onChange={(e) => setSegmentForm({ ...segmentForm, baseFare: e.target.value })}
|
|
className="input w-full"
|
|
placeholder="e.g., 150"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Passenger Type (Optional)</label>
|
|
<select
|
|
value={segmentForm.passengerCategory}
|
|
onChange={(e) => setSegmentForm({ ...segmentForm, passengerCategory: e.target.value })}
|
|
className="input w-full"
|
|
>
|
|
<option value="">All Passenger Types</option>
|
|
<option value="ADULT">Adult (5+ years)</option>
|
|
<option value="CHILD">Child (Less than 5 years)</option>
|
|
</select>
|
|
<p className="text-xs text-muted-foreground mt-1">Scope pricing to specific passenger type</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="label">Nationality (Optional)</label>
|
|
<select
|
|
value={segmentForm.nationality}
|
|
onChange={(e) => setSegmentForm({ ...segmentForm, nationality: e.target.value })}
|
|
className="input w-full"
|
|
>
|
|
<option value="">All Nationalities</option>
|
|
<option value="Ethiopian">Ethiopian</option>
|
|
<option value="Djiboutian">Djiboutian</option>
|
|
<option value="Other">Other</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Valid From *</label>
|
|
<input
|
|
type="date"
|
|
value={segmentForm.validFrom}
|
|
onChange={(e) => setSegmentForm({ ...segmentForm, validFrom: e.target.value })}
|
|
className="input w-full"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Valid Until (Optional)</label>
|
|
<input
|
|
type="date"
|
|
value={segmentForm.validUntil}
|
|
onChange={(e) => setSegmentForm({ ...segmentForm, validUntil: e.target.value })}
|
|
className="input w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-3 rounded-lg text-xs text-blue-800 dark:text-blue-200">
|
|
<p className="font-semibold mb-1">{tab === 'schedule' ? 'Schedule Fare' : 'Segment Fare'} Options:</p>
|
|
{tab === 'schedule' && (
|
|
<ul className="space-y-1">
|
|
<li>• <strong>Schedule:</strong> Apply to specific schedule only</li>
|
|
<li>• <strong>Route Code:</strong> Apply to all schedules on that route</li>
|
|
<li>• <strong>Passenger Type:</strong> ADULT or CHILD pricing</li>
|
|
<li>• <strong>Nationality:</strong> Override for specific nationalities</li>
|
|
<li>• <strong>All empty:</strong> Apply globally to all schedules</li>
|
|
</ul>
|
|
)}
|
|
{tab === 'segment' && (
|
|
<ul className="space-y-1">
|
|
<li>• <strong>Segments:</strong> Define pricing for specific stop-to-stop segments</li>
|
|
<li>• <strong>Stops:</strong> Use sequence numbers from the route</li>
|
|
<li>• <strong>Passenger Type:</strong> ADULT or CHILD pricing</li>
|
|
<li>• <strong>Nationality:</strong> Optional scope to specific nationalities</li>
|
|
</ul>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-2 justify-end pt-4">
|
|
<ActionButton
|
|
variant="secondary"
|
|
onClick={() => {
|
|
if (tab === 'schedule') resetForm();
|
|
else resetSegmentForm();
|
|
}}
|
|
type="button"
|
|
>
|
|
Cancel
|
|
</ActionButton>
|
|
<ActionButton
|
|
onClick={tab === 'schedule' ? handleSaveFare : handleSaveSegmentFare}
|
|
loading={
|
|
tab === 'schedule'
|
|
? createFareMutation.isPending || updateFareMutation.isPending
|
|
: createSegmentFareMutation.isPending || updateSegmentFareMutation.isPending
|
|
}
|
|
>
|
|
{editingFare ? `Update ${tab === 'schedule' ? 'Fare' : 'Segment Fare'} Rule` : `Save ${tab === 'schedule' ? 'Fare' : 'Segment Fare'} Rule`}
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
{/* Baggage Allowance Modal */}
|
|
<Modal isOpen={baggageModal} onClose={() => { setBaggageModal(false); setEditingAllowance(null); setBaggageError(null); }} title={editingAllowance ? 'Edit Allowance Rule' : 'Add Allowance Rule'} size="md">
|
|
<div className="space-y-4">
|
|
{baggageError && <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3 rounded-lg text-sm text-red-800 dark:text-red-200">{baggageError}</div>}
|
|
<div>
|
|
<label className="label">Seat Class *</label>
|
|
<select value={baggageForm.seatClassId} onChange={(e) => setBaggageForm({ ...baggageForm, seatClassId: e.target.value })} className="input w-full" disabled={!!editingAllowance}>
|
|
<option value="">Select seat class...</option>
|
|
{seatClassesArray.map((sc: SeatClass) => <option key={sc.id} value={sc.id}>{sc.name}</option>)}
|
|
</select>
|
|
{editingAllowance && <p className="text-xs text-muted-foreground mt-1">Seat class cannot be changed. Delete and recreate to change.</p>}
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="label">Free Allowance (kg) *</label>
|
|
<input type="number" min="0" className="input w-full" placeholder="e.g. 20" value={baggageForm.maxWeightKg} onChange={(e) => setBaggageForm({ ...baggageForm, maxWeightKg: e.target.value })} />
|
|
</div>
|
|
<div>
|
|
<label className="label">Max Pieces *</label>
|
|
<input type="number" min="1" className="input w-full" placeholder="e.g. 2" value={baggageForm.maxPiecesCount} onChange={(e) => setBaggageForm({ ...baggageForm, maxPiecesCount: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="label">Excess Fee per kg (ETB) *</label>
|
|
<input type="number" min="0" step="0.01" className="input w-full" placeholder="e.g. 50.00" value={baggageForm.excessFeePerKg} onChange={(e) => setBaggageForm({ ...baggageForm, excessFeePerKg: e.target.value })} />
|
|
<p className="text-xs text-muted-foreground mt-1">Amount charged per kg above the free allowance</p>
|
|
</div>
|
|
<div className="flex justify-end gap-2 pt-2">
|
|
<ActionButton variant="secondary" onClick={() => { setBaggageModal(false); setEditingAllowance(null); setBaggageError(null); }}>Cancel</ActionButton>
|
|
<ActionButton onClick={handleSaveAllowance} loading={createAllowanceMutation.isPending || updateAllowanceMutation.isPending}>
|
|
{editingAllowance ? 'Update' : 'Save'}
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
|
|
{/* Delete Allowance Confirm */}
|
|
<ConfirmDialog
|
|
isOpen={deleteAllowanceConfirm.isOpen}
|
|
onClose={() => setDeleteAllowanceConfirm({ isOpen: false, id: null })}
|
|
onConfirm={() => deleteAllowanceMutation.mutateAsync(deleteAllowanceConfirm.id!)}
|
|
title="Delete Allowance Rule"
|
|
message="Are you sure you want to delete this baggage allowance rule?"
|
|
confirmText="Delete"
|
|
isDanger
|
|
isLoading={deleteAllowanceMutation.isPending}
|
|
warning="The excess baggage fallback rate (50 ETB/kg) will apply until a new rule is created."
|
|
/>
|
|
</div>
|
|
);
|
|
}
|