'use client'; import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Plus, Edit, CheckCircle, Eye, Layers, Trash2 } from 'lucide-react'; import DataTable from '@/components/ui/DataTable'; import Badge from '@/components/ui/Badge'; import ActionButton from '@/components/ui/ActionButton'; import ConfirmDialog from '@/components/ui/ConfirmDialog'; import Modal from '@/components/ui/Modal'; import { packagesApi, stationsApi, schedulesApi } from '@/lib/api'; import { formatDateTime, formatCurrency } from '@/lib/utils'; const toLocal = (iso?: string) => { if (!iso) return ''; const d = new Date(iso); return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16); }; const emptyForm = { code: '', name: '', description: '', outboundScheduleId: '', returnScheduleId: '', originStationId: '', destinationStationId: '', boardingTime: '', departureTime: '', arrivalTime: '', totalCapacity: '', coachConfiguration: '', includedServices: '', busTransferIncluded: 'false', busTransferRoute: '', validFrom: '', validUntil: '', }; export default function PackagesPage() { const [page] = useState(1); const [form, setForm] = useState(emptyForm); const [modalMode, setModalMode] = useState<'create' | 'edit' | null>(null); const [editingId, setEditingId] = useState(null); const [viewPackage, setViewPackage] = useState(null); const [activateConfirm, setActivateConfirm] = useState(null); const [deactivateConfirm, setDeactivateConfirm] = useState(null); const [tiersPackage, setTiersPackage] = useState(null); const [editingTier, setEditingTier] = useState(null); const [tierForm, setTierForm] = useState({ seatType: '', label: '', priceMinor: '', availableSeats: '' }); const [deleteTierConfirm, setDeleteTierConfirm] = useState(null); const [tierError, setTierError] = useState(null); const [deletePackageConfirm, setDeletePackageConfirm] = useState(null); const [deletePackageError, setDeletePackageError] = useState(null); const queryClient = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['packages', page], queryFn: () => packagesApi.getAll({ page, pageSize: 20 }), }); const { data: stationsData } = useQuery({ queryKey: ['stations-all'], queryFn: () => stationsApi.getAll(), }); const { data: schedulesData } = useQuery({ queryKey: ['schedules-all'], queryFn: () => schedulesApi.getAll(), }); const stations: any[] = stationsData?.items || stationsData?.data || (Array.isArray(stationsData) ? stationsData : []); const schedules: any[] = schedulesData?.items || schedulesData?.data || (Array.isArray(schedulesData) ? schedulesData : []); const createMutation = useMutation({ mutationFn: packagesApi.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packages'] }); setModalMode(null); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: any }) => packagesApi.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packages'] }); setModalMode(null); }, }); const activateMutation = useMutation({ mutationFn: packagesApi.activate, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packages'] }); setActivateConfirm(null); }, }); const deactivateMutation = useMutation({ mutationFn: packagesApi.deactivate, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packages'] }); setDeactivateConfirm(null); }, }); const emptyTierForm = { seatType: '', label: '', priceMinor: '', availableSeats: '' }; const addTierMutation = useMutation({ mutationFn: ({ packageId, data }: { packageId: string; data: any }) => packagesApi.addTier(packageId, data), onSuccess: (newTier) => { setTiersPackage((prev: any) => prev ? { ...prev, priceTiers: [...(prev.priceTiers || []), newTier] } : prev); queryClient.invalidateQueries({ queryKey: ['packages'] }); setEditingTier(null); setTierForm(emptyTierForm); setTierError(null); }, onError: (e: any) => setTierError(e?.response?.data?.message || e?.message || 'Failed to add tier'), }); const updateTierMutation = useMutation({ mutationFn: ({ tierId, data }: { tierId: string; data: any }) => packagesApi.updateTier(tierId, data), onSuccess: (updated) => { setTiersPackage((prev: any) => prev ? { ...prev, priceTiers: prev.priceTiers.map((t: any) => t.id === updated.id ? updated : t) } : prev); queryClient.invalidateQueries({ queryKey: ['packages'] }); setEditingTier(null); setTierForm(emptyTierForm); setTierError(null); }, onError: (e: any) => setTierError(e?.response?.data?.message || e?.message || 'Failed to update tier'), }); const deletePackageMutation = useMutation({ mutationFn: (id: string) => packagesApi.remove(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['packages'] }); setDeletePackageConfirm(null); setDeletePackageError(null); }, onError: (e: any) => setDeletePackageError(e?.response?.data?.message || e?.message || 'Failed to delete package'), }); const deleteTierMutation = useMutation({ mutationFn: (tierId: string) => packagesApi.deleteTier(tierId), onSuccess: (_, tierId) => { setTiersPackage((prev: any) => prev ? { ...prev, priceTiers: prev.priceTiers.filter((t: any) => t.id !== tierId) } : prev); queryClient.invalidateQueries({ queryKey: ['packages'] }); setDeleteTierConfirm(null); }, onError: (e: any) => setTierError(e?.response?.data?.message || e?.message || 'Failed to delete tier'), }); const openEditTier = (tier: any) => { setEditingTier(tier); setTierForm({ seatType: tier.seatType, label: tier.label, priceMinor: String(tier.priceMinor), availableSeats: String(tier.availableSeats) }); setTierError(null); }; const handleTierSubmit = async (e: React.FormEvent) => { e.preventDefault(); const payload = { seatType: tierForm.seatType, label: tierForm.label, priceMinor: parseInt(tierForm.priceMinor), availableSeats: parseInt(tierForm.availableSeats) }; if (editingTier) { await updateTierMutation.mutateAsync({ tierId: editingTier.id, data: payload }); } else { await addTierMutation.mutateAsync({ packageId: tiersPackage.id, data: payload }); } }; const openCreate = () => { setForm(emptyForm); setEditingId(null); setModalMode('create'); }; const openEdit = (pkg: any) => { setForm({ code: pkg.code ?? '', name: pkg.name ?? '', description: pkg.description ?? '', outboundScheduleId: pkg.outboundScheduleId ?? '', returnScheduleId: pkg.returnScheduleId ?? '', originStationId: pkg.originStationId ?? '', destinationStationId: pkg.destinationStationId ?? '', boardingTime: toLocal(pkg.boardingTime), departureTime: toLocal(pkg.departureTime), arrivalTime: toLocal(pkg.arrivalTime), totalCapacity: String(pkg.totalCapacity ?? ''), coachConfiguration: pkg.coachConfiguration ?? '', includedServices: (pkg.includedServices ?? []).join('\n'), busTransferIncluded: pkg.busTransferIncluded ? 'true' : 'false', busTransferRoute: pkg.busTransferRoute ?? '', validFrom: toLocal(pkg.validFrom), validUntil: toLocal(pkg.validUntil), }); setEditingId(pkg.id); setModalMode('edit'); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const services = form.includedServices.split('\n').map((s) => s.trim()).filter(Boolean); const payload = { code: form.code, name: form.name, description: form.description || undefined, outboundScheduleId: form.outboundScheduleId, returnScheduleId: form.returnScheduleId, originStationId: form.originStationId, destinationStationId: form.destinationStationId, boardingTime: form.boardingTime, departureTime: form.departureTime, arrivalTime: form.arrivalTime, totalCapacity: parseInt(form.totalCapacity), coachConfiguration: form.coachConfiguration || undefined, includedServices: services, busTransferIncluded: form.busTransferIncluded === 'true', busTransferRoute: form.busTransferRoute || undefined, validFrom: form.validFrom, validUntil: form.validUntil, priceTiers: [], }; if (modalMode === 'edit' && editingId) { await updateMutation.mutateAsync({ id: editingId, data: payload }); } else { await createMutation.mutateAsync(payload); } }; const field = (key: keyof typeof form) => ({ value: form[key], onChange: (e: React.ChangeEvent) => setForm((f) => ({ ...f, [key]: e.target.value })), }); const scheduleLabel = (s: any) => { const from = s.originStation?.name ?? s.originStationId ?? '?'; const to = s.destinationStation?.name ?? s.destinationStationId ?? '?'; const dep = s.departureAt ? new Date(s.departureAt).toLocaleString() : ''; return `${from} → ${to}${dep ? ' | ' + dep : ''}`; }; const columns = [ { key: 'code', label: 'Package', render: (pkg: any) => (
{pkg.code}
{pkg.name}
), }, { key: 'capacity', label: 'Capacity', render: (p: any) => (
{p.totalCapacity} seats
{p.priceTiers?.length > 0 &&
{p.priceTiers.length} tiers
}
), }, { key: 'validity', label: 'Valid Period', render: (p: any) => (
{new Date(p.validFrom).toLocaleDateString()}
→ {new Date(p.validUntil).toLocaleDateString()}
), }, { key: 'status', label: 'Status', render: (p: any) => ( {p.status} ), }, { key: 'createdAt', label: 'Created', render: (p: any) => {formatDateTime(p.createdAt)}, }, ]; const actions = [ { label: 'View', onClick: (p: any) => setViewPackage(p), variant: 'secondary' as const, icon: Eye }, { label: 'Edit', onClick: openEdit, variant: 'secondary' as const, icon: Edit }, { label: 'Tiers', icon: Layers, variant: 'secondary' as const, onClick: (p: any) => { setTiersPackage(p); setEditingTier(null); setTierForm({ seatType: '', label: '', priceMinor: '', availableSeats: '' }); setTierError(null); }, }, { label: 'Activate', icon: CheckCircle, variant: 'primary' as const, onClick: (p: any) => setActivateConfirm(p), show: (p: any) => p.status !== 'ACTIVE', }, { label: 'Deactivate', icon: CheckCircle, variant: 'secondary' as const, onClick: (p: any) => setDeactivateConfirm(p), show: (p: any) => p.status === 'ACTIVE', }, { label: 'Delete', icon: Trash2, variant: 'danger' as const, onClick: (p: any) => { setDeletePackageError(null); setDeletePackageConfirm(p); }, }, ]; const isPending = createMutation.isPending || updateMutation.isPending; return (

Packages

Manage travel packages and pilgrimages

New Package
{/* View Modal */} setViewPackage(null)} title="Package Details" size="lg"> {viewPackage && (
Code

{viewPackage.code}

Status

{viewPackage.status}

Name

{viewPackage.name}

{viewPackage.description &&
Description

{viewPackage.description}

}
Total Capacity

{viewPackage.totalCapacity}

Coach Config

{viewPackage.coachConfiguration || '—'}

Boarding

{formatDateTime(viewPackage.boardingTime)}

Departure

{formatDateTime(viewPackage.departureTime)}

Arrival

{formatDateTime(viewPackage.arrivalTime)}

Bus Transfer

{viewPackage.busTransferIncluded ? `Yes — ${viewPackage.busTransferRoute || ''}` : 'No'}

Valid From

{new Date(viewPackage.validFrom).toLocaleDateString()}

Valid Until

{new Date(viewPackage.validUntil).toLocaleDateString()}

{viewPackage.includedServices?.length > 0 && (
Included Services
    {viewPackage.includedServices.map((s: string, i: number) =>
  • {s}
  • )}
)} {viewPackage.priceTiers?.length > 0 && (
Price Tiers
{viewPackage.priceTiers.map((t: any) => (
{t.label} ({t.seatType}) {formatCurrency(t.priceMinor, 'ETB')} — {t.bookedSeats}/{t.availableSeats} booked
))}
)}
)}
{/* Activate Confirmation */} setActivateConfirm(null)} onConfirm={() => activateMutation.mutate(activateConfirm.id)} title="Activate Package" message={`Activate "${activateConfirm?.name}"? It will become publicly available for booking.`} confirmText="Activate" isDanger={false} /> {/* Deactivate Confirmation */} setDeactivateConfirm(null)} onConfirm={() => deactivateMutation.mutate(deactivateConfirm.id)} title="Deactivate Package" message={`Deactivate "${deactivateConfirm?.name}"? It will no longer be available for booking.`} confirmText="Deactivate" isDanger={false} isLoading={deactivateMutation.isPending} /> {/* Tiers Modal */} { setTiersPackage(null); setEditingTier(null); setTierError(null); }} title={`Price Tiers — ${tiersPackage?.name ?? ''}`} size="lg"> {tiersPackage && (
{tierError && (
{tierError}
)} {/* Existing tiers list */}
{(tiersPackage.priceTiers ?? []).length === 0 && (

No tiers yet. Add one below.

)} {(tiersPackage.priceTiers ?? []).map((t: any) => (
{t.label} ({t.seatType})
{formatCurrency(t.priceMinor, 'ETB')} · {t.bookedSeats}/{t.availableSeats} booked
openEditTier(t)}>Edit { setTierError(null); setDeleteTierConfirm(t); }} disabled={t.bookedSeats > 0} >Delete
))}
{/* Add / Edit tier form */}

{editingTier ? 'Edit Tier' : 'Add New Tier'}

setTierForm((f) => ({ ...f, seatType: e.target.value }))} />
setTierForm((f) => ({ ...f, label: e.target.value }))} />
setTierForm((f) => ({ ...f, priceMinor: e.target.value }))} />
setTierForm((f) => ({ ...f, availableSeats: e.target.value }))} />
{editingTier && ( { setEditingTier(null); setTierForm({ seatType: '', label: '', priceMinor: '', availableSeats: '' }); setTierError(null); }}>Cancel )} {editingTier ? 'Update Tier' : 'Add Tier'}
)}
{/* Delete Package Confirmation */} { setDeletePackageConfirm(null); setDeletePackageError(null); }} onConfirm={() => deletePackageMutation.mutate(deletePackageConfirm.id)} title="Delete Package" message={`Delete "${deletePackageConfirm?.name}"? This will also remove all price tiers and cannot be undone.`} confirmText="Delete" isDanger isLoading={deletePackageMutation.isPending} error={deletePackageError ?? undefined} /> {/* Delete Tier Confirmation */} { setDeleteTierConfirm(null); setTierError(null); }} onConfirm={() => deleteTierMutation.mutate(deleteTierConfirm.id)} title="Delete Tier" message={`Delete tier "${deleteTierConfirm?.label}"? This cannot be undone.`} confirmText="Delete" isDanger isLoading={deleteTierMutation.isPending} error={tierError ?? undefined} /> {/* Create / Edit Modal */} setModalMode(null)} title={modalMode === 'edit' ? 'Edit Package' : 'New Package'} size="lg" >