Package inquiry, UAT related update

This commit is contained in:
Stephanos A
2026-06-26 13:57:26 +03:00
parent eefbe6e8ed
commit 86c7b2e4eb
23 changed files with 648 additions and 93 deletions

View File

@@ -47,8 +47,14 @@ export default function BookingsPage() {
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ['bookings', filters],
queryFn: () => bookingsApi.getAll(filters),
queryKey: ['bookings', filters, extraFilters],
queryFn: () => bookingsApi.getAll({
...filters,
...(extraFilters.bookingType && { bookingType: extraFilters.bookingType }),
...(extraFilters.paymentStatus && { paymentStatus: extraFilters.paymentStatus }),
...(extraFilters.dateFrom && { dateFrom: extraFilters.dateFrom }),
...(extraFilters.dateTo && { dateTo: extraFilters.dateTo }),
}),
});
const cancelMutation = useMutation({
@@ -257,8 +263,8 @@ export default function BookingsPage() {
<select className="input" value={extraFilters.paymentStatus}
onChange={(e) => setExtraFilters({ ...extraFilters, paymentStatus: e.target.value })}>
<option value="">All Payments</option>
<option value="PENDING">Pending</option>
<option value="PAID">Paid</option>
<option value="PENDING">Pending</option>
<option value="FAILED">Failed</option>
<option value="REFUNDED">Refunded</option>
</select>

View File

@@ -20,14 +20,21 @@ const STATUS_VARIANT: Record<string, any> = {
export default function ExcessBaggagePage() {
const queryClient = useQueryClient();
const [filters, setFilters] = useState({ status: '', bookingRef: '', page: '1' });
const [filters, setFilters] = useState({ status: '', bookingRef: '', dateFrom: '', dateTo: '', page: '1' });
const [showExtraFilters, setShowExtraFilters] = useState(false);
const [waiveModal, setWaiveModal] = useState<any>(null);
const [waiveReason, setWaiveReason] = useState('');
const [waiveError, setWaiveError] = useState<string | null>(null);
const { data, isLoading } = useQuery({
queryKey: ['excess-baggage', filters],
queryFn: () => excessBaggageApi.getAll({ status: filters.status || undefined, bookingRef: filters.bookingRef || undefined, page: filters.page }),
queryFn: () => excessBaggageApi.getAll({
status: filters.status || undefined,
bookingRef: filters.bookingRef || undefined,
dateFrom: filters.dateFrom || undefined,
dateTo: filters.dateTo || undefined,
page: filters.page,
}),
});
const waiveMutation = useMutation({
@@ -104,14 +111,14 @@ export default function ExcessBaggagePage() {
icon: Send,
variant: 'secondary' as const,
onClick: (c: any) => resendMutation.mutate(c.id),
hidden: (c: any) => c.status !== 'PENDING',
show: (c: any) => c.status === 'PENDING',
},
{
label: 'Waive',
icon: RefreshCw,
variant: 'secondary' as const,
onClick: (c: any) => { setWaiveModal(c); setWaiveReason(''); setWaiveError(null); },
hidden: (c: any) => ['PAID', 'CASH_COLLECTED', 'WAIVED'].includes(c.status),
show: (c: any) => !['PAID', 'CASH_COLLECTED', 'WAIVED'].includes(c.status),
},
];
@@ -125,31 +132,41 @@ export default function ExcessBaggagePage() {
</div>
<div className="card">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="label">Booking Ref</label>
<input
className="input"
placeholder="Search by booking ref…"
value={filters.bookingRef}
onChange={(e) => setFilters({ ...filters, bookingRef: e.target.value, page: '1' })}
/>
</div>
<div>
<label className="label">Status</label>
<select
className="input"
value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value, page: '1' })}
>
<option value="">All</option>
<div className="mb-4 space-y-3">
<div className="flex flex-wrap gap-3">
<div className="flex-1 min-w-48">
<input className="input" placeholder="Search by booking ref…"
value={filters.bookingRef}
onChange={(e) => setFilters({ ...filters, bookingRef: e.target.value, page: '1' })} />
</div>
<select className="input w-44" value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value, page: '1' })}>
<option value="">All Status</option>
<option value="PENDING">Pending</option>
<option value="PAID">Paid</option>
<option value="CASH_COLLECTED">Cash Collected</option>
<option value="EXPIRED">Expired</option>
<option value="WAIVED">Waived</option>
</select>
<button type="button" className="input w-auto px-4 text-sm font-medium text-primary border-primary/40"
onClick={() => setShowExtraFilters(v => !v)}>
{showExtraFilters ? 'Hide Filters ▲' : 'More Filters ▼'}
</button>
</div>
{showExtraFilters && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
<div>
<label className="label">Date From</label>
<input type="date" className="input" value={filters.dateFrom}
onChange={(e) => setFilters({ ...filters, dateFrom: e.target.value, page: '1' })} />
</div>
<div>
<label className="label">Date To</label>
<input type="date" className="input" value={filters.dateTo}
onChange={(e) => setFilters({ ...filters, dateTo: e.target.value, page: '1' })} />
</div>
</div>
)}
</div>
</div>

View File

@@ -0,0 +1,7 @@
'use client';
import DashboardLayout from '../dashboard/layout';
export default function PackageInquiriesLayout({ children }: { children: React.ReactNode }) {
return <DashboardLayout>{children}</DashboardLayout>;
}

View File

@@ -0,0 +1,193 @@
'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { 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 { packageInquiriesApi, packagesApi } from '@/lib/api';
import { formatDateTime, formatCurrency } from '@/lib/utils';
const STATUSES = ['NEW', 'CONTACTED', 'CONVERTED', 'CLOSED'];
const statusVariant: Record<string, string> = {
NEW: 'info',
CONTACTED: 'warning',
CONVERTED: 'success',
CLOSED: 'default',
};
export default function PackageInquiriesPage() {
const [filters, setFilters] = useState({ packageId: '', status: '' });
const [deleteConfirm, setDeleteConfirm] = useState<any>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['package-inquiries', filters],
queryFn: () => packageInquiriesApi.getAll({ ...filters, pageSize: 50 }),
});
const { data: packagesData } = useQuery({
queryKey: ['packages-all-simple'],
queryFn: () => packagesApi.getAll({ pageSize: 100 }),
});
const packages: any[] = packagesData?.items || [];
const statusMutation = useMutation({
mutationFn: ({ id, status }: { id: string; status: string }) =>
packageInquiriesApi.updateStatus(id, status),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['package-inquiries'] }),
});
const deleteMutation = useMutation({
mutationFn: (id: string) => packageInquiriesApi.remove(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['package-inquiries'] });
setDeleteConfirm(null);
setDeleteError(null);
},
onError: (e: any) => setDeleteError(e?.response?.data?.message || e?.message || 'Failed to delete'),
});
const columns = [
{
key: 'contact',
label: 'Contact',
render: (row: any) => (
<div>
<div className="font-semibold">{row.contactName}</div>
<div className="text-xs text-muted-foreground">{row.contactEmail || row.contactPhone || '—'}</div>
</div>
),
},
{
key: 'package',
label: 'Package',
render: (row: any) => (
<div>
<div className="font-medium">{row.package?.name || '—'}</div>
<div className="text-xs text-muted-foreground font-mono">{row.package?.code}</div>
</div>
),
},
{
key: 'priceTier',
label: 'Price Tier',
render: (row: any) => row.priceTier ? (
<div>
<div className="text-sm font-medium">{row.priceTier.label}</div>
<div className="text-xs text-muted-foreground">{formatCurrency(row.priceTier.priceMinor, 'ETB')} / person</div>
</div>
) : <span className="text-muted-foreground text-sm"></span>,
},
{
key: 'travelerCount',
label: 'Travelers',
render: (row: any) => (
<span className="font-semibold">{row.travelerCount}</span>
),
},
{
key: 'enquiredAt',
label: 'Enquired At',
render: (row: any) => (
<span className="text-sm">{formatDateTime(row.enquiredAt)}</span>
),
},
{
key: 'notes',
label: 'Notes',
render: (row: any) => (
<span className="text-sm text-muted-foreground line-clamp-2 max-w-xs">{row.notes || '—'}</span>
),
},
{
key: 'status',
label: 'Status',
render: (row: any) => (
<select
className="input py-1 text-xs"
value={row.status}
onChange={(e) => statusMutation.mutate({ id: row.id, status: e.target.value })}
>
{STATUSES.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
),
},
];
const actions = [
{
label: 'Delete',
onClick: (row: any) => { setDeleteConfirm(row); setDeleteError(null); },
variant: 'danger' as const,
icon: Trash2,
},
];
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-foreground">Package Inquiries</h1>
<p className="text-muted-foreground">Manage incoming package inquiries</p>
</div>
<div className="card">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="label">Package</label>
<select
className="input"
value={filters.packageId}
onChange={(e) => setFilters({ ...filters, packageId: e.target.value })}
>
<option value="">All Packages</option>
{packages.map((p: any) => (
<option key={p.id} value={p.id}>{p.name} ({p.code})</option>
))}
</select>
</div>
<div>
<label className="label">Status</label>
<select
className="input"
value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
>
<option value="">All Statuses</option>
{STATUSES.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</div>
</div>
</div>
<DataTable
data={data?.items || []}
columns={columns}
actions={actions}
loading={isLoading}
emptyMessage="No inquiries found"
/>
<ConfirmDialog
isOpen={!!deleteConfirm}
onClose={() => { setDeleteConfirm(null); setDeleteError(null); }}
onConfirm={() => deleteMutation.mutate(deleteConfirm.id)}
title="Delete Inquiry"
message={`Delete inquiry from ${deleteConfirm?.contactName}? This cannot be undone.`}
confirmText="Delete"
isDanger
isLoading={deleteMutation.isPending}
error={deleteError ?? undefined}
/>
</div>
);
}

View File

@@ -29,6 +29,11 @@ const emptyForm = {
export default function PackagesPage() {
const [page] = useState(1);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [showExtraFilters, setShowExtraFilters] = useState(false);
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [form, setForm] = useState(emptyForm);
const [modalMode, setModalMode] = useState<'create' | 'edit' | null>(null);
const [editingId, setEditingId] = useState<string | null>(null);
@@ -282,6 +287,15 @@ export default function PackagesPage() {
const isPending = createMutation.isPending || updateMutation.isPending;
const allItems: any[] = data?.items || [];
const filteredItems = allItems.filter((p) => {
if (search && !p.name.toLowerCase().includes(search.toLowerCase()) && !p.code.toLowerCase().includes(search.toLowerCase())) return false;
if (statusFilter && p.status !== statusFilter) return false;
if (dateFrom && new Date(p.validFrom).toISOString().split('T')[0] < dateFrom) return false;
if (dateTo && new Date(p.validUntil).toISOString().split('T')[0] > dateTo) return false;
return true;
});
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@@ -292,13 +306,47 @@ export default function PackagesPage() {
<ActionButton icon={Plus} onClick={openCreate}>New Package</ActionButton>
</div>
<DataTable
data={data?.items || []}
columns={columns}
actions={actions}
loading={isLoading}
emptyMessage="No packages found"
/>
<div className="card">
<div className="mb-4 space-y-3">
<div className="flex flex-wrap gap-3">
<div className="flex-1 min-w-48">
<input type="text" placeholder="Search by name or code..." className="input"
value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<select className="input w-44" value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
<option value="">All Status</option>
<option value="DRAFT">Draft</option>
<option value="ACTIVE">Active</option>
<option value="SOLD_OUT">Sold Out</option>
<option value="EXPIRED">Expired</option>
<option value="CANCELLED">Cancelled</option>
</select>
<button type="button" className="input w-auto px-4 text-sm font-medium text-primary border-primary/40"
onClick={() => setShowExtraFilters(v => !v)}>
{showExtraFilters ? 'Hide Filters ▲' : 'More Filters ▼'}
</button>
</div>
{showExtraFilters && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
<div>
<label className="label">Valid From</label>
<input type="date" className="input" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)} />
</div>
<div>
<label className="label">Valid Until</label>
<input type="date" className="input" value={dateTo} onChange={(e) => setDateTo(e.target.value)} />
</div>
</div>
)}
</div>
<DataTable
data={filteredItems}
columns={columns}
actions={actions}
loading={isLoading}
emptyMessage="No packages found"
/>
</div>
{/* View Modal */}
<Modal isOpen={!!viewPackage} onClose={() => setViewPackage(null)} title="Package Details" size="lg">

View File

@@ -63,8 +63,14 @@ export default function PassengersPage() {
});
const { data, isLoading, error } = useQuery({
queryKey: ['passengers', filters],
queryFn: () => passengersApi.getAll(filters),
queryKey: ['passengers', filters, extraFilters],
queryFn: () => passengersApi.getAll({
...filters,
...(extraFilters.gender && { gender: extraFilters.gender }),
...(extraFilters.nationality && { nationality: extraFilters.nationality }),
...(extraFilters.dateFrom && { dateFrom: extraFilters.dateFrom }),
...(extraFilters.dateTo && { dateTo: extraFilters.dateTo }),
}),
});
const PASSENGER_COLS = [

View File

@@ -78,6 +78,8 @@ export default function TicketsPage() {
originStationId: filters.originStationId || undefined,
destinationStationId: filters.destinationStationId || undefined,
arrivalDate: filters.arrivalDate || undefined,
dateFrom: filters.dateFrom || undefined,
dateTo: filters.dateTo || undefined,
skip: 0,
take: 50,
}),
@@ -483,7 +485,7 @@ export default function TicketsPage() {
Error loading tickets: {error instanceof Error ? error.message : 'Unknown error'}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4">
<div>
<label className="label">Search</label>
<input
@@ -529,20 +531,40 @@ export default function TicketsPage() {
onChange={(e) => setFilters({ ...filters, arrivalDate: e.target.value })}
/>
</div>
<div>
<label className="label">Status</label>
<select
className="input"
value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
>
<option value="">All Status</option>
<option value="ACTIVE">Active</option>
<option value="USED">Used</option>
<option value="CANCELLED">Cancelled</option>
</select>
<div className="flex items-end">
<button type="button" className="input w-full px-4 text-sm font-medium text-primary border-primary/40"
onClick={() => setShowExtraFilters(v => !v)}>
{showExtraFilters ? 'Hide Filters ▲' : 'More Filters ▼'}
</button>
</div>
</div>
{showExtraFilters && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-3">
<div>
<label className="label">Status</label>
<select
className="input"
value={filters.status}
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
>
<option value="">All Status</option>
<option value="ACTIVE">Active</option>
<option value="USED">Used</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<div>
<label className="label">Issued From</label>
<input type="date" className="input" value={filters.dateFrom}
onChange={(e) => setFilters({ ...filters, dateFrom: e.target.value })} />
</div>
<div>
<label className="label">Issued To</label>
<input type="date" className="input" value={filters.dateTo}
onChange={(e) => setFilters({ ...filters, dateTo: e.target.value })} />
</div>
</div>
)}
</div>
{/* Tickets Table */}

View File

@@ -60,6 +60,7 @@ const navigationSections = [
title: 'Tourism',
items: [
{ name: 'Packages', href: '/packages', icon: Package },
{ name: 'Inquiries', href: '/package-inquiries', icon: MessageSquare },
]
},
{

View File

@@ -397,6 +397,22 @@ export const packagesApi = {
deleteTier: (tierId: string) => apiClient.delete(`/packages/tiers/${tierId}`),
};
// Package Inquiries API
export const packageInquiriesApi = {
getAll: async (params?: any) => {
const cleanParams = Object.fromEntries(
Object.entries(params || {}).filter(([_, v]) => v !== '' && v !== undefined && v !== null)
) as Record<string, string>;
const query = new URLSearchParams(cleanParams).toString();
const response = await apiClient.get<any>(`/packages/inquiries${query ? `?${query}` : ''}`);
if (response?.data) return Array.isArray(response.data) ? { items: response.data } : response;
return Array.isArray(response) ? { items: response } : response;
},
create: (data: any) => apiClient.post<any>('/packages/inquiries', data),
updateStatus: (id: string, status: string) => apiClient.patch<any>(`/packages/inquiries/${id}/status`, { status }),
remove: (id: string) => apiClient.delete(`/packages/inquiries/${id}`),
};
// Excess Baggage API
export const excessBaggageApi = {
logCharge: (data: any) => apiClient.post<any>('/agents/excess-baggage', data),