Files
edr-platform/apps/edr-passenger-web/backoffice/src/app/pricing/page.tsx
2026-06-16 14:41:27 +03:00

1010 lines
38 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'>('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 { 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 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 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 === '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);
}}
>
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>
</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>
)}
</>
)}
</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 (&lt;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>
<input
type="text"
value={fareForm.route}
onChange={(e) => setFareForm({ ...fareForm, route: e.target.value })}
className="input w-full"
placeholder="e.g., ADD-DJI"
/>
<p className="text-xs text-muted-foreground mt-1">e.g., ADD-DJI for full 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>
</div>
);
}