mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +00:00
237 lines
8.0 KiB
TypeScript
237 lines
8.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Download, Plus, Edit, Trash2 } from 'lucide-react';
|
|
import DataTable from '@/components/ui/DataTable';
|
|
import Badge from '@/components/ui/Badge';
|
|
import ActionButton from '@/components/ui/ActionButton';
|
|
import Modal from '@/components/ui/Modal';
|
|
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
import { seatClassesApi } from '@/lib/api';
|
|
import { formatDateTime, formatCurrency } from '@/lib/utils';
|
|
|
|
export default function SeatClassesPage() {
|
|
const [filters, setFilters] = useState({ search: '' });
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [editingSeatClass, setEditingSeatClass] = useState<any>(null);
|
|
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; seatClass: any | null }>({ isOpen: false, seatClass: null });
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['seat-classes', filters],
|
|
queryFn: () => seatClassesApi.getAll(),
|
|
});
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: seatClassesApi.create,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['seat-classes'] });
|
|
setShowModal(false);
|
|
setEditingSeatClass(null);
|
|
},
|
|
});
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: any }) => seatClassesApi.update(id, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['seat-classes'] });
|
|
setShowModal(false);
|
|
setEditingSeatClass(null);
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: seatClassesApi.delete,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['seat-classes'] });
|
|
},
|
|
});
|
|
|
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.currentTarget);
|
|
const seatClassData = {
|
|
name: formData.get('name') as string,
|
|
description: formData.get('description') as string,
|
|
basePrice: Math.round(parseFloat(formData.get('basePrice') as string) * 100), // Convert to minor units
|
|
isActive: formData.get('isActive') === 'true',
|
|
};
|
|
|
|
if (editingSeatClass) {
|
|
await updateMutation.mutateAsync({ id: editingSeatClass.id, data: seatClassData });
|
|
} else {
|
|
await createMutation.mutateAsync(seatClassData);
|
|
}
|
|
};
|
|
|
|
const handleDelete = (seatClass: any) => {
|
|
setDeleteConfirm({ isOpen: true, seatClass });
|
|
};
|
|
|
|
const confirmDelete = async () => {
|
|
if (deleteConfirm.seatClass) {
|
|
await deleteMutation.mutateAsync(deleteConfirm.seatClass.id);
|
|
setDeleteConfirm({ isOpen: false, seatClass: null });
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{ key: 'name', label: 'Name', render: (cls: any) => <span className="font-medium">{cls.name}</span> },
|
|
{ key: 'description', label: 'Description', render: (cls: any) => cls.description || 'N/A' },
|
|
{ key: 'basePrice', label: 'Base Price', render: (cls: any) => formatCurrency(cls.basePrice, 'ETB') },
|
|
{ key: 'isActive', label: 'Status', render: (cls: any) => <Badge variant="status" status={cls.isActive ? 'CONFIRMED' : 'CANCELLED'}>{cls.isActive ? 'Active' : 'Inactive'}</Badge> },
|
|
];
|
|
|
|
const actions = [
|
|
{
|
|
label: 'Edit',
|
|
onClick: (seatClass: any) => {
|
|
setEditingSeatClass(seatClass);
|
|
setShowModal(true);
|
|
},
|
|
variant: 'secondary' as const,
|
|
icon: Edit,
|
|
},
|
|
{
|
|
label: 'Delete',
|
|
onClick: handleDelete,
|
|
variant: 'danger' as const,
|
|
icon: Trash2,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-foreground">Classes</h1>
|
|
<p className="text-muted-foreground">Manage seat class configurations</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<ActionButton icon={Download} variant="secondary">Export</ActionButton>
|
|
<ActionButton
|
|
icon={Plus}
|
|
onClick={() => {
|
|
setEditingSeatClass(null);
|
|
setShowModal(true);
|
|
}}
|
|
>
|
|
Add Seat Class
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="label">Search</label>
|
|
<input type="text" placeholder="Search..." className="input" value={filters.search} onChange={(e) => setFilters({ ...filters, search: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
data={(data as any)?.items || (Array.isArray(data) ? data : [])}
|
|
columns={columns}
|
|
actions={actions}
|
|
loading={isLoading}
|
|
emptyMessage="No seat classes found"
|
|
/>
|
|
|
|
{/* Delete Confirmation */}
|
|
<ConfirmDialog
|
|
isOpen={deleteConfirm.isOpen}
|
|
onClose={() => setDeleteConfirm({ isOpen: false, seatClass: null })}
|
|
onConfirm={confirmDelete}
|
|
title="Delete Seat Class"
|
|
message={`Are you sure you want to delete ${deleteConfirm.seatClass?.name}?`}
|
|
confirmText="Delete"
|
|
isDanger={true}
|
|
warning="This seat class may be used by coaches and trips. Deleting it may impact fare calculations and seat assignments."
|
|
/>
|
|
|
|
{/* Add/Edit Modal */}
|
|
<Modal
|
|
isOpen={showModal}
|
|
onClose={() => {
|
|
setShowModal(false);
|
|
setEditingSeatClass(null);
|
|
}}
|
|
title={`${editingSeatClass ? 'Edit' : 'Add'} Seat Class`}
|
|
size="lg"
|
|
>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label className="label">Class Name *</label>
|
|
<input
|
|
type="text"
|
|
name="name"
|
|
className="input"
|
|
defaultValue={editingSeatClass?.name}
|
|
required
|
|
placeholder="e.g., Economy Regular"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Description</label>
|
|
<textarea
|
|
name="description"
|
|
className="input"
|
|
rows={3}
|
|
defaultValue={editingSeatClass?.description}
|
|
placeholder="Describe the seat class..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="label">Base Price (ETB) *</label>
|
|
<input
|
|
type="number"
|
|
name="basePrice"
|
|
className="input"
|
|
defaultValue={editingSeatClass?.basePrice ? (editingSeatClass.basePrice / 100).toFixed(2) : ''}
|
|
required
|
|
min="0"
|
|
step="0.01"
|
|
placeholder="e.g., 450.00"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Enter amount in ETB (e.g., 450.00)</p>
|
|
</div>
|
|
<div>
|
|
<label className="label">Status</label>
|
|
<select
|
|
name="isActive"
|
|
className="input"
|
|
defaultValue={editingSeatClass?.isActive?.toString() || 'true'}
|
|
>
|
|
<option value="true">Active</option>
|
|
<option value="false">Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-4">
|
|
<ActionButton
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={() => {
|
|
setShowModal(false);
|
|
setEditingSeatClass(null);
|
|
}}
|
|
>
|
|
Cancel
|
|
</ActionButton>
|
|
<ActionButton
|
|
type="submit"
|
|
loading={createMutation.isPending || updateMutation.isPending}
|
|
>
|
|
{editingSeatClass ? 'Update' : 'Create'} Seat Class
|
|
</ActionButton>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|