mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Excess luggage rate UI restored to Tariff Rates page
This commit is contained in:
@@ -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<string>('');
|
||||
const [selectedRoute, setSelectedRoute] = useState<string>('');
|
||||
@@ -67,17 +67,6 @@ export default function PricingPage() {
|
||||
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'),
|
||||
@@ -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<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: () => {
|
||||
@@ -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
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
@@ -604,13 +545,6 @@ export default function PricingPage() {
|
||||
>
|
||||
Schedule 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 Luggage Rates
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -720,52 +654,9 @@ export default function PricingPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{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)} </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>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<ConfirmDialog
|
||||
isOpen={deleteConfirm.isOpen}
|
||||
onClose={() => setDeleteConfirm({ isOpen: false, id: null })}
|
||||
@@ -1095,54 +986,6 @@ export default function PricingPage() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/* Luggage 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<any>(null);
|
||||
@@ -50,8 +53,57 @@ export default function TariffRatesPage() {
|
||||
const [selectedCoachTypeId, setSelectedCoachTypeId] = useState('');
|
||||
const [selectedBedPosition, setSelectedBedPosition] = useState<string>('');
|
||||
const [selectedNationalityType, setSelectedNationalityType] = useState<string>('LOCAL');
|
||||
|
||||
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 queryClient = useQueryClient();
|
||||
|
||||
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 { data: classesData, isLoading } = useQuery({
|
||||
queryKey: ['seat-classes'],
|
||||
queryFn: () => apiClient.get<any>('/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() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Tariff Rates</h1>
|
||||
<p className="text-muted-foreground">
|
||||
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
|
||||
</p>
|
||||
</div>
|
||||
<ActionButton icon={Plus} onClick={() => { setEditingClass(null); setFormError(null); setShowModal(true); }}>
|
||||
Add Rate
|
||||
<ActionButton
|
||||
icon={Plus}
|
||||
onClick={() => {
|
||||
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'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name, nationality, etc."
|
||||
className="input pl-10 w-full"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<div className="flex gap-4 border-b mb-6">
|
||||
<button
|
||||
onClick={() => setTab('tariff')}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${tab === 'tariff' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground'}`}
|
||||
>
|
||||
Seat Class Tariffs
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('baggage')}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${tab === 'baggage' ? 'border-primary text-primary' : 'border-transparent text-muted-foreground'}`}
|
||||
>
|
||||
Excess Luggage Rates
|
||||
</button>
|
||||
</div>
|
||||
<DataTable
|
||||
data={displayed}
|
||||
columns={columns}
|
||||
actions={actions}
|
||||
loading={isLoading}
|
||||
emptyMessage={search ? 'No tariff rates match your search' : 'No tariff rates found'}
|
||||
/>
|
||||
|
||||
{tab === 'tariff' && (
|
||||
<>
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name, nationality, etc."
|
||||
className="input pl-10 w-full"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
data={displayed}
|
||||
columns={columns}
|
||||
actions={actions}
|
||||
loading={isLoading}
|
||||
emptyMessage={search ? 'No tariff rates match your search' : 'No tariff rates found'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'baggage' && (
|
||||
allowancesLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</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>
|
||||
|
||||
<ConfirmDialog
|
||||
@@ -273,6 +403,60 @@ export default function TariffRatesPage() {
|
||||
warning="Bookings in progress may be affected. Ensure a replacement rate exists."
|
||||
/>
|
||||
|
||||
<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."
|
||||
/>
|
||||
|
||||
<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>
|
||||
{allClasses.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>
|
||||
|
||||
<Modal
|
||||
isOpen={showModal}
|
||||
onClose={closeModal}
|
||||
|
||||
Reference in New Issue
Block a user