diff --git a/apps/edr-passenger-web/backoffice/src/app/pricing/page.tsx b/apps/edr-passenger-web/backoffice/src/app/pricing/page.tsx index 73ce87383..5de572542 100644 --- a/apps/edr-passenger-web/backoffice/src/app/pricing/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/pricing/page.tsx @@ -34,7 +34,7 @@ interface SeatClass { } export default function PricingPage() { - const [tab, setTab] = useState<'segment' | 'schedule' | 'baggage'>('segment'); + const [tab, setTab] = useState<'segment' | 'schedule'>('segment'); const [showModal, setShowModal] = useState(false); const [selectedSchedule, setSelectedSchedule] = useState(''); const [selectedRoute, setSelectedRoute] = useState(''); @@ -67,17 +67,6 @@ export default function PricingPage() { validUntil: '', }); - const [baggageForm, setBaggageForm] = useState({ - seatClassId: '', - maxWeightKg: '', - maxPiecesCount: '', - excessFeePerKg: '', - }); - const [editingAllowance, setEditingAllowance] = useState(null); - const [baggageError, setBaggageError] = useState(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'), @@ -120,48 +109,6 @@ export default function PricingPage() { enabled: !!selectedRoute && tab === 'segment', }); - const { data: allowances = [], isLoading: allowancesLoading, refetch: refetchAllowances } = useQuery({ - queryKey: ['baggage-allowances'], - queryFn: () => apiClient.get('/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: () => { @@ -401,7 +348,6 @@ export default function PricingPage() { 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 = [ @@ -554,12 +500,7 @@ export default function PricingPage() { onClick={() => { setError(null); setEditingFare(null); - if (tab === 'baggage') { - setBaggageForm({ seatClassId: '', maxWeightKg: '', maxPiecesCount: '', excessFeePerKg: '' }); - setEditingAllowance(null); - setBaggageError(null); - setBaggageModal(true); - } else if (tab === 'schedule') { + if (tab === 'schedule') { setFareForm({ seatClassId: '', baseFare: '', @@ -584,7 +525,7 @@ export default function PricingPage() { setShowModal(true); }} > - {tab === 'baggage' ? 'Add Allowance Rule' : 'Add Fare Rule'} + Add Fare Rule @@ -604,13 +545,6 @@ export default function PricingPage() { > Schedule Fares -
@@ -720,52 +654,9 @@ export default function PricingPage() { )} - {tab === 'baggage' && ( - <> - {allowancesLoading ? ( -
- ) : allowancesArray.length === 0 ? ( -
- No baggage allowance rules defined. Click "Add Allowance Rule" to create one. -
- ) : ( - {a.seatClass?.name ?? a.seatClassId} }, - { key: 'maxWeightKg', label: 'Free Allowance', render: (a: any) => {a.maxWeightKg} kg, {a.maxPiecesCount} pcs }, - { key: 'excessFeePerKg', label: 'Excess Fee / kg', render: (a: any) => {(a.excessFeePerKg / 100).toFixed(2)} }, - ]} - 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." - /> - )} - - )}
- {/* Delete Confirmation */} setDeleteConfirm({ isOpen: false, id: null })} @@ -1095,54 +986,6 @@ export default function PricingPage() { - {/* Luggage Allowance Modal */} - { setBaggageModal(false); setEditingAllowance(null); setBaggageError(null); }} title={editingAllowance ? 'Edit Allowance Rule' : 'Add Allowance Rule'} size="md"> -
- {baggageError &&
{baggageError}
} -
- - - {editingAllowance &&

Seat class cannot be changed. Delete and recreate to change.

} -
-
-
- - setBaggageForm({ ...baggageForm, maxWeightKg: e.target.value })} /> -
-
- - setBaggageForm({ ...baggageForm, maxPiecesCount: e.target.value })} /> -
-
-
- - setBaggageForm({ ...baggageForm, excessFeePerKg: e.target.value })} /> -

Amount charged per kg above the free allowance

-
-
- { setBaggageModal(false); setEditingAllowance(null); setBaggageError(null); }}>Cancel - - {editingAllowance ? 'Update' : 'Save'} - -
-
-
- - {/* Delete Allowance Confirm */} - 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." - /> ); } diff --git a/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx b/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx index 590c30848..530b335e7 100644 --- a/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx @@ -10,6 +10,8 @@ import Modal from '@/components/ui/Modal'; import ConfirmDialog from '@/components/ui/ConfirmDialog'; import { apiClient } from '@/lib/api-client'; +interface SeatClass { id: string; name: string; } + const BED_POSITIONS = ['UPPER', 'MIDDLE', 'LOWER'] as const; const COACH_TYPE_LABELS: Record = { HSC: 'Regular Seat (Hard Seat)', @@ -42,6 +44,7 @@ function getTariffRef(nationalityType: string, coachCode: string, bedPosition: s } export default function TariffRatesPage() { + const [tab, setTab] = useState<'tariff' | 'baggage'>('tariff'); const [search, setSearch] = useState(''); const [showModal, setShowModal] = useState(false); const [editingClass, setEditingClass] = useState(null); @@ -50,8 +53,57 @@ export default function TariffRatesPage() { const [selectedCoachTypeId, setSelectedCoachTypeId] = useState(''); const [selectedBedPosition, setSelectedBedPosition] = useState(''); const [selectedNationalityType, setSelectedNationalityType] = useState('LOCAL'); + + const [baggageForm, setBaggageForm] = useState({ seatClassId: '', maxWeightKg: '', maxPiecesCount: '', excessFeePerKg: '' }); + const [editingAllowance, setEditingAllowance] = useState(null); + const [baggageError, setBaggageError] = useState(null); + const [baggageModal, setBaggageModal] = useState(false); + const [deleteAllowanceConfirm, setDeleteAllowanceConfirm] = useState<{ isOpen: boolean; id: string | null }>({ isOpen: false, id: null }); + const queryClient = useQueryClient(); + const { data: allowances, isLoading: allowancesLoading, refetch: refetchAllowances } = useQuery({ + queryKey: ['baggage-allowances'], + queryFn: () => apiClient.get('/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 { data: classesData, isLoading } = useQuery({ queryKey: ['seat-classes'], queryFn: () => apiClient.get('/seat-classes'), @@ -126,6 +178,8 @@ export default function TariffRatesPage() { ? classesData : (classesData as any)?.items || (classesData as any)?.data || []; + const allowancesArray: any[] = Array.isArray(allowances) ? allowances : (allowances as any)?.items || []; + const tariffClasses = allClasses.filter((c: any) => c.nationalityType); const displayed = tariffClasses.filter((c: any) => { @@ -232,32 +286,108 @@ export default function TariffRatesPage() {

Tariff Rates

- Manage per-km fare rates by nationality, coach type, and bed position per the official EDR tariff policy + Manage per-km fare rates and excess luggage allowances per the official EDR tariff policy

- { setEditingClass(null); setFormError(null); setShowModal(true); }}> - Add Rate + { + if (tab === 'baggage') { + setBaggageForm({ seatClassId: '', maxWeightKg: '', maxPiecesCount: '', excessFeePerKg: '' }); + setEditingAllowance(null); + setBaggageError(null); + setBaggageModal(true); + } else { + setEditingClass(null); + setFormError(null); + setShowModal(true); + } + }} + > + {tab === 'baggage' ? 'Add Allowance Rule' : 'Add Rate'}
-
- - setSearch(e.target.value)} - /> +
+ +
- + + {tab === 'tariff' && ( + <> +
+ + setSearch(e.target.value)} + /> +
+ + + )} + + {tab === 'baggage' && ( + allowancesLoading ? ( +
+
+
+ ) : allowancesArray.length === 0 ? ( +
+ No baggage allowance rules defined. Click "Add Allowance Rule" to create one. +
+ ) : ( + {a.seatClass?.name ?? a.seatClassId} }, + { key: 'maxWeightKg', label: 'Free Allowance', render: (a: any) => {a.maxWeightKg} kg, {a.maxPiecesCount} pcs }, + { key: 'excessFeePerKg', label: 'Excess Fee / kg', render: (a: any) => {(a.excessFeePerKg / 100).toFixed(2)} ETB }, + ]} + 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." + /> + ) + )}
+ 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." + /> + + { setBaggageModal(false); setEditingAllowance(null); setBaggageError(null); }} + title={editingAllowance ? 'Edit Allowance Rule' : 'Add Allowance Rule'} + size="md" + > +
+ {baggageError && ( +
{baggageError}
+ )} +
+ + + {editingAllowance &&

Seat class cannot be changed. Delete and recreate to change.

} +
+
+
+ + setBaggageForm({ ...baggageForm, maxWeightKg: e.target.value })} /> +
+
+ + setBaggageForm({ ...baggageForm, maxPiecesCount: e.target.value })} /> +
+
+
+ + setBaggageForm({ ...baggageForm, excessFeePerKg: e.target.value })} /> +

Amount charged per kg above the free allowance

+
+
+ { setBaggageModal(false); setEditingAllowance(null); setBaggageError(null); }}>Cancel + + {editingAllowance ? 'Update' : 'Save'} + +
+
+
+