mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 03:38:17 +00:00
Round trip journeys, feedback items, backoffice documentation
This commit is contained in:
@@ -80,6 +80,45 @@ export default function BookingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportBookings = async () => {
|
||||
const selectedColumns = prompt(
|
||||
'Select columns to export (comma-separated):\n\n' +
|
||||
'Available: bookingRef, passenger, status, bookingType, passengerCount, totalMinor, paymentStatus, createdAt\n\n' +
|
||||
'Default: bookingRef, passenger, status, totalMinor, paymentStatus, createdAt',
|
||||
'bookingRef, passenger, status, totalMinor, paymentStatus, createdAt'
|
||||
);
|
||||
|
||||
if (!selectedColumns) return;
|
||||
|
||||
const cols = selectedColumns.split(',').map(c => c.trim());
|
||||
const csv = [
|
||||
cols.join(','),
|
||||
...data?.items?.map((booking: any) => {
|
||||
const values = cols.map(col => {
|
||||
switch(col) {
|
||||
case 'bookingRef': return booking.bookingRef;
|
||||
case 'passenger': return booking.passenger?.fullName || booking.contactEmail || 'Guest';
|
||||
case 'status': return booking.status;
|
||||
case 'bookingType': return booking.bookingType || 'N/A';
|
||||
case 'passengerCount': return booking.adultCount + booking.childCount;
|
||||
case 'totalMinor': return booking.totalMinor;
|
||||
case 'paymentStatus': return booking.paymentIntent?.status || 'PENDING';
|
||||
case 'createdAt': return booking.createdAt;
|
||||
default: return '';
|
||||
}
|
||||
});
|
||||
return values.map(v => `"${v}"`).join(',');
|
||||
}) || []
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `bookings-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'bookingRef',
|
||||
@@ -99,6 +138,17 @@ export default function BookingsPage() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'bookingType',
|
||||
label: 'Class',
|
||||
sortable: true,
|
||||
render: (booking: any) => booking.bookingType || 'ONE_WAY',
|
||||
},
|
||||
{
|
||||
key: 'passengerCount',
|
||||
label: 'Passengers',
|
||||
render: (booking: any) => `${(booking.adultCount || 0) + (booking.childCount || 0)}`,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
@@ -158,7 +208,7 @@ export default function BookingsPage() {
|
||||
<h1 className="text-2xl font-bold">Bookings</h1>
|
||||
<p className="text-muted-foreground">Manage all passenger bookings</p>
|
||||
</div>
|
||||
<ActionButton variant="export" icon={Download}>Export</ActionButton>
|
||||
<ActionButton variant="export" icon={Download} onClick={handleExportBookings}>Export</ActionButton>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
|
||||
@@ -75,7 +75,9 @@ export default function ClassesPage() {
|
||||
coachTypeId: selectedCoachTypeId,
|
||||
name: formData.get('name') as string,
|
||||
description: formData.get('description') as string,
|
||||
baseFareMinor: parseInt(formData.get('baseFareMinor') as string) || 0,
|
||||
baseFareMinor: Math.round(parseFloat(formData.get('baseFareMinor') as string) * 100) || 0,
|
||||
premiumMinor: Math.round(parseFloat(formData.get('premiumMinor') as string) * 100) || 0,
|
||||
insuranceFeeMinor: Math.round(parseFloat(formData.get('insuranceFeeMinor') as string) * 100) || 0,
|
||||
isActive: formData.get('isActive') === 'true',
|
||||
};
|
||||
|
||||
@@ -136,9 +138,23 @@ export default function ClassesPage() {
|
||||
},
|
||||
{
|
||||
key: 'baseFareMinor',
|
||||
label: 'Base Fare (ETB)',
|
||||
label: 'Base Fare',
|
||||
render: (cls: any) => (
|
||||
<span className="font-mono text-sm">{formatCurrency(cls.baseFareMinor, 'ETB')}</span>
|
||||
<span className="font-mono text-sm">{(cls.baseFareMinor / 100).toFixed(2)} ETB</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'premiumMinor',
|
||||
label: 'Premium',
|
||||
render: (cls: any) => (
|
||||
<span className="font-mono text-sm">{cls.premiumMinor ? (cls.premiumMinor / 100).toFixed(2) : '0.00'} ETB</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'insuranceFeeMinor',
|
||||
label: 'Insurance',
|
||||
render: (cls: any) => (
|
||||
<span className="font-mono text-sm">{cls.insuranceFeeMinor ? (cls.insuranceFeeMinor / 100).toFixed(2) : '0.00'} ETB</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -181,7 +197,7 @@ export default function ClassesPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Classes</h1>
|
||||
<p className="text-muted-foreground">Manage class configurations by coach type</p>
|
||||
<p className="text-muted-foreground">Manage class configurations with pricing by coach type</p>
|
||||
</div>
|
||||
<ActionButton
|
||||
icon={Plus}
|
||||
@@ -278,18 +294,60 @@ export default function ClassesPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Base Fare (ETB cents) *</label>
|
||||
<input
|
||||
type="number"
|
||||
name="baseFareMinor"
|
||||
className="input"
|
||||
defaultValue={editingClass?.baseFareMinor || ''}
|
||||
required
|
||||
min="0"
|
||||
placeholder="e.g., 45000 (450 ETB)"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Enter amount in cents (100 cents = 1 ETB)</p>
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="font-semibold text-foreground mb-4">Pricing Configuration</h3>
|
||||
|
||||
<div>
|
||||
<label className="label">Base Fare (ETB) *</label>
|
||||
<input
|
||||
type="number"
|
||||
name="baseFareMinor"
|
||||
className="input"
|
||||
defaultValue={editingClass?.baseFareMinor ? (editingClass.baseFareMinor / 100).toFixed(2) : ''}
|
||||
required
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="e.g., 350.00"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Per-km distance-based fare rate</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div>
|
||||
<label className="label">Premium Fee (ETB)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="premiumMinor"
|
||||
className="input"
|
||||
defaultValue={editingClass?.premiumMinor ? (editingClass.premiumMinor / 100).toFixed(2) : '0.00'}
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="e.g., 50.00"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Flat fee per passenger (e.g., lounge access, extra legroom)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Insurance Fee (ETB)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="insuranceFeeMinor"
|
||||
className="input"
|
||||
defaultValue={editingClass?.insuranceFeeMinor ? (editingClass.insuranceFeeMinor / 100).toFixed(2) : '0.00'}
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="e.g., 25.00"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Flat fee per passenger (e.g., travel insurance)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded text-sm text-blue-800 dark:text-blue-200">
|
||||
<p className="font-medium mb-1">Total Fare Calculation:</p>
|
||||
<p>Total = (Base Fare × Distance) + Premium + Insurance</p>
|
||||
<p className="mt-2 text-xs">• Premium applies per passenger (including free child)</p>
|
||||
<p className="text-xs">• Insurance applies per passenger (including free child)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -236,6 +236,7 @@ export default function CoachesPage() {
|
||||
coachTypeId: formData.get('coachTypeId') as string,
|
||||
arrangement: formData.get('arrangement') as string,
|
||||
capacity: parseInt(formData.get('capacity') as string),
|
||||
sequence: parseInt(formData.get('sequence') as string),
|
||||
status: formData.get('status') as string,
|
||||
};
|
||||
|
||||
@@ -624,7 +625,7 @@ export default function CoachesPage() {
|
||||
<form onSubmit={handleCoachSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="label">Coach Type</label>
|
||||
<label className="label">Coach Type *</label>
|
||||
<select
|
||||
name="coachTypeId"
|
||||
className="input"
|
||||
@@ -641,7 +642,7 @@ export default function CoachesPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Number</label>
|
||||
<label className="label">Number *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="number"
|
||||
@@ -653,20 +654,20 @@ export default function CoachesPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Arrangement</label>
|
||||
<label className="label">Arrangement *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="arrangement"
|
||||
className="input"
|
||||
defaultValue={editingItem?.arrangement || editingItem?.seatArrangement || '2+2'}
|
||||
defaultValue={editingItem?.arrangement || editingItem?.seatArrangement }
|
||||
required
|
||||
placeholder="e.g., 2+2, 3+2"
|
||||
placeholder="e.g., 3+2, 3+0, 2+0"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Format: separate columns with +</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Capacity</label>
|
||||
<label className="label">Capacity *</label>
|
||||
<input
|
||||
type="number"
|
||||
name="capacity"
|
||||
@@ -678,12 +679,27 @@ export default function CoachesPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="label">Status</label>
|
||||
<div>
|
||||
<label className="label">Sequence Number *</label>
|
||||
<input
|
||||
type="number"
|
||||
name="sequence"
|
||||
className="input"
|
||||
defaultValue={editingItem?.sequence || 0}
|
||||
min="0"
|
||||
required
|
||||
placeholder="e.g., 1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Used for ordering coaches in trains</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Status *</label>
|
||||
<select
|
||||
name="status"
|
||||
className="input"
|
||||
defaultValue={editingItem?.status || 'ACTIVE'}
|
||||
required
|
||||
>
|
||||
<option value="ACTIVE">Active</option>
|
||||
<option value="MAINTENANCE">Maintenance</option>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Sidebar from '@/components/layout/Sidebar';
|
||||
import Header from '@/components/layout/Header';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
|
||||
export default function CurrenciesLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
router.push('/login');
|
||||
}
|
||||
}, [isAuthenticated, router, isLoading]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-gray-50 dark:bg-slate-950">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-edr-green-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-gray-50 dark:bg-slate-950">
|
||||
<Sidebar />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<Header />
|
||||
<main className="flex-1 overflow-y-auto bg-gray-50 dark:bg-slate-950 p-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
475
apps/edr-passenger-web/backoffice/src/app/currencies/page.tsx
Normal file
475
apps/edr-passenger-web/backoffice/src/app/currencies/page.tsx
Normal file
@@ -0,0 +1,475 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Plus, Trash2, Loader2, Edit, RefreshCw } from 'lucide-react';
|
||||
import DataTable from '@/components/ui/DataTable';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import ActionButton from '@/components/ui/ActionButton';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
|
||||
interface Currency {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
baseCurrencyCode: string;
|
||||
exchangeRate: number;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export default function CurrenciesPage() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingCurrency, setEditingCurrency] = useState<Currency | null>(null);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; id: string | null }>({
|
||||
isOpen: false,
|
||||
id: null,
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [currencyForm, setCurrencyForm] = useState({
|
||||
code: '',
|
||||
name: '',
|
||||
symbol: '',
|
||||
baseCurrencyCode: 'ETB',
|
||||
exchangeRate: '',
|
||||
});
|
||||
|
||||
const { data: currencies = [], isLoading } = useQuery({
|
||||
queryKey: ['currencies'],
|
||||
queryFn: () => apiClient.get('/currencies'),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => apiClient.post('/currencies', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['currencies'] });
|
||||
resetForm();
|
||||
setError(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setError(err.response?.data?.message || 'Failed to create currency');
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: any) => apiClient.patch(`/currencies/${data.id}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['currencies'] });
|
||||
setEditingCurrency(null);
|
||||
resetForm();
|
||||
setError(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setError(err.response?.data?.message || 'Failed to update currency');
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => apiClient.delete(`/currencies/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['currencies'] });
|
||||
setDeleteConfirm({ isOpen: false, id: null });
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setError(err.response?.data?.message || 'Failed to delete currency');
|
||||
},
|
||||
});
|
||||
|
||||
const syncRatesMutation = useMutation({
|
||||
mutationFn: () => apiClient.post('/currencies/sync-rates', {}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['currencies'] });
|
||||
setError(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setError(err.response?.data?.message || 'Failed to sync exchange rates');
|
||||
},
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
setCurrencyForm({
|
||||
code: '',
|
||||
name: '',
|
||||
symbol: '',
|
||||
baseCurrencyCode: 'ETB',
|
||||
exchangeRate: '',
|
||||
});
|
||||
setEditingCurrency(null);
|
||||
setShowModal(false);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleEditCurrency = (currency: Currency) => {
|
||||
setEditingCurrency(currency);
|
||||
setCurrencyForm({
|
||||
code: currency.code,
|
||||
name: currency.name,
|
||||
symbol: currency.symbol,
|
||||
baseCurrencyCode: currency.baseCurrencyCode,
|
||||
exchangeRate: currency.exchangeRate.toString(),
|
||||
});
|
||||
setError(null);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleSaveCurrency = async () => {
|
||||
setError(null);
|
||||
if (!currencyForm.code || !currencyForm.name || !currencyForm.symbol || !currencyForm.exchangeRate) {
|
||||
setError('All fields are required');
|
||||
return;
|
||||
}
|
||||
|
||||
const rate = parseFloat(currencyForm.exchangeRate);
|
||||
if (isNaN(rate) || rate <= 0) {
|
||||
setError('Exchange rate must be a positive number');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
code: currencyForm.code.toUpperCase(),
|
||||
name: currencyForm.name,
|
||||
symbol: currencyForm.symbol,
|
||||
baseCurrencyCode: currencyForm.baseCurrencyCode,
|
||||
exchangeRate: rate,
|
||||
};
|
||||
|
||||
if (editingCurrency) {
|
||||
await updateMutation.mutateAsync({ id: editingCurrency.id, ...payload });
|
||||
} else {
|
||||
await createMutation.mutateAsync(payload);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (deleteConfirm.id) {
|
||||
await deleteMutation.mutateAsync(deleteConfirm.id);
|
||||
}
|
||||
};
|
||||
|
||||
const currenciesArray = Array.isArray(currencies) ? currencies : (currencies as any)?.items || [];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'code',
|
||||
label: 'Code',
|
||||
render: (currency: Currency) => (
|
||||
<span className="font-mono font-semibold text-primary">{currency.code}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
render: (currency: Currency) => (
|
||||
<span className="font-medium">{currency.name}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'symbol',
|
||||
label: 'Symbol',
|
||||
render: (currency: Currency) => (
|
||||
<span className="text-lg">{currency.symbol}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'baseCurrencyCode',
|
||||
label: 'Base Currency',
|
||||
render: (currency: Currency) => (
|
||||
<span className="font-mono text-sm">{currency.baseCurrencyCode}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'exchangeRate',
|
||||
label: 'Exchange Rate',
|
||||
render: (currency: Currency) => (
|
||||
<div className="space-y-1">
|
||||
<div className="font-mono font-semibold">
|
||||
1 {currency.baseCurrencyCode} = {currency.exchangeRate.toFixed(4)} {currency.code}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
1 {currency.code} = {(1 / currency.exchangeRate).toFixed(6)} {currency.baseCurrencyCode}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'isActive',
|
||||
label: 'Status',
|
||||
render: (currency: Currency) => (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
currency.isActive
|
||||
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
|
||||
: 'bg-gray-100 dark:bg-gray-900/20 text-gray-800 dark:text-gray-300'
|
||||
}`}>
|
||||
{currency.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'updatedAt',
|
||||
label: 'Last Updated',
|
||||
render: (currency: Currency) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{new Date(currency.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Edit',
|
||||
onClick: handleEditCurrency,
|
||||
variant: 'secondary' as const,
|
||||
icon: Edit,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: (currency: Currency) => setDeleteConfirm({ isOpen: true, id: currency.id }),
|
||||
variant: 'danger' as const,
|
||||
icon: Trash2,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Currencies</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage exchange rates and display currencies</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<ActionButton
|
||||
icon={RefreshCw}
|
||||
variant="secondary"
|
||||
onClick={() => syncRatesMutation.mutate()}
|
||||
loading={syncRatesMutation.isPending}
|
||||
>
|
||||
Sync Rates
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
icon={Plus}
|
||||
onClick={() => {
|
||||
setError(null);
|
||||
setEditingCurrency(null);
|
||||
setCurrencyForm({
|
||||
code: '',
|
||||
name: '',
|
||||
symbol: '',
|
||||
baseCurrencyCode: 'ETB',
|
||||
exchangeRate: '',
|
||||
});
|
||||
setShowModal(true);
|
||||
}}
|
||||
>
|
||||
Add Currency
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="p-4 bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-900/10 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="text-sm text-blue-600 dark:text-blue-400 font-medium">Total Currencies</div>
|
||||
<div className="text-2xl font-bold text-blue-900 dark:text-blue-200 mt-2">
|
||||
{currenciesArray.length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-900/10 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="text-sm text-green-600 dark:text-green-400 font-medium">Active</div>
|
||||
<div className="text-2xl font-bold text-green-900 dark:text-green-200 mt-2">
|
||||
{currenciesArray.filter((c: Currency) => c.isActive).length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-900/10 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div className="text-sm text-purple-600 dark:text-purple-400 font-medium">Base Currency</div>
|
||||
<div className="text-2xl font-bold text-purple-900 dark:text-purple-200 mt-2">ETB</div>
|
||||
</div>
|
||||
<div className="p-4 bg-gradient-to-br from-orange-50 to-orange-100 dark:from-orange-900/20 dark:to-orange-900/10 rounded-lg border border-orange-200 dark:border-orange-800">
|
||||
<div className="text-sm text-orange-600 dark:text-orange-400 font-medium">Last Sync</div>
|
||||
<div className="text-lg font-bold text-orange-900 dark:text-orange-200 mt-2">
|
||||
{currenciesArray.length > 0
|
||||
? new Date(currenciesArray[0]?.updatedAt).toLocaleDateString()
|
||||
: 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
) : currenciesArray.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<p>No currencies configured. Click "Add Currency" to create one.</p>
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
data={currenciesArray}
|
||||
columns={columns}
|
||||
actions={actions}
|
||||
loading={false}
|
||||
emptyMessage="No currencies found."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
|
||||
<h3 className="font-semibold text-blue-900 dark:text-blue-200 mb-3">Currency Management</h3>
|
||||
<ul className="text-sm text-blue-800 dark:text-blue-300 space-y-2">
|
||||
<li>
|
||||
• <strong>Base Currency:</strong> All exchange rates are calculated relative to this currency (typically ETB)
|
||||
</li>
|
||||
<li>
|
||||
• <strong>Exchange Rate:</strong> How many units of the currency equal 1 unit of the base currency
|
||||
</li>
|
||||
<li>
|
||||
• <strong>Display Currencies:</strong> Configure which currencies customers can view prices in
|
||||
</li>
|
||||
<li>
|
||||
• <strong>Sync Rates:</strong> Automatically update exchange rates from external sources
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<ConfirmDialog
|
||||
isOpen={deleteConfirm.isOpen}
|
||||
onClose={() => setDeleteConfirm({ isOpen: false, id: null })}
|
||||
onConfirm={confirmDelete}
|
||||
title="Delete Currency"
|
||||
message="Are you sure you want to delete this currency? This action cannot be undone."
|
||||
confirmText="Delete"
|
||||
isDanger={true}
|
||||
warning="This will remove the currency from the system."
|
||||
/>
|
||||
|
||||
{/* Add/Edit Modal */}
|
||||
<Modal
|
||||
isOpen={showModal}
|
||||
onClose={resetForm}
|
||||
title={`${editingCurrency ? 'Edit' : 'Add'} Currency`}
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<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">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="label">Currency Code *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currencyForm.code}
|
||||
onChange={(e) => setCurrencyForm({ ...currencyForm, code: e.target.value.toUpperCase() })}
|
||||
className="input w-full"
|
||||
placeholder="e.g., USD"
|
||||
maxLength="3"
|
||||
disabled={!!editingCurrency}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">3-letter ISO code (e.g., USD, DJF, GBP)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Currency Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currencyForm.name}
|
||||
onChange={(e) => setCurrencyForm({ ...currencyForm, name: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="e.g., United States Dollar"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="label">Symbol *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currencyForm.symbol}
|
||||
onChange={(e) => setCurrencyForm({ ...currencyForm, symbol: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="e.g., $"
|
||||
maxLength="3"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Base Currency *</label>
|
||||
<select
|
||||
value={currencyForm.baseCurrencyCode}
|
||||
onChange={(e) => setCurrencyForm({ ...currencyForm, baseCurrencyCode: e.target.value })}
|
||||
className="input w-full"
|
||||
disabled
|
||||
>
|
||||
<option value="ETB">ETB (Ethiopian Birr)</option>
|
||||
<option value="USD">USD (US Dollar)</option>
|
||||
<option value="DJF">DJF (Djiboutian Franc)</option>
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground mt-1">All rates relative to this currency</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Exchange Rate *</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.0001"
|
||||
value={currencyForm.exchangeRate}
|
||||
onChange={(e) => setCurrencyForm({ ...currencyForm, exchangeRate: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="e.g., 0.018"
|
||||
required
|
||||
/>
|
||||
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
1 {currencyForm.baseCurrencyCode} = ? {currencyForm.code}
|
||||
</div>
|
||||
</div>
|
||||
{currencyForm.exchangeRate && parseFloat(currencyForm.exchangeRate) > 0 && (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
≈ 1 {currencyForm.code} = {(1 / parseFloat(currencyForm.exchangeRate)).toFixed(6)} {currencyForm.baseCurrencyCode}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-3 rounded-lg text-xs text-blue-800 dark:text-blue-200">
|
||||
<p className="font-semibold mb-1">Exchange Rate Example:</p>
|
||||
<p>If 1 ETB = 0.018 USD, enter 0.018</p>
|
||||
<p>If 1 ETB = 3.25 DJF, enter 3.25</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end pt-4">
|
||||
<ActionButton
|
||||
variant="secondary"
|
||||
onClick={resetForm}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={handleSaveCurrency}
|
||||
loading={createMutation.isPending || updateMutation.isPending}
|
||||
>
|
||||
{editingCurrency ? 'Update Currency' : 'Add Currency'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
949
apps/edr-passenger-web/backoffice/src/app/docs/page.tsx
Normal file
949
apps/edr-passenger-web/backoffice/src/app/docs/page.tsx
Normal file
@@ -0,0 +1,949 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ChevronDown, ChevronRight, FileText, Home } from 'lucide-react';
|
||||
|
||||
const DocPage = () => {
|
||||
const [expandedSections, setExpandedSections] = useState<{ [key: string]: boolean }>({
|
||||
overview: true,
|
||||
operations: true,
|
||||
masterdata: false,
|
||||
financial: false,
|
||||
services: false,
|
||||
security: false,
|
||||
analytics: false,
|
||||
system: false,
|
||||
});
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSections(prev => (({
|
||||
...prev,
|
||||
[section]: !prev[section]
|
||||
})));
|
||||
};
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
const headerOffset = 120;
|
||||
const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
|
||||
const offsetPosition = elementPosition - headerOffset;
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: 'overview',
|
||||
title: '📋 Overview & Getting Started',
|
||||
items: [
|
||||
{ id: 'about', label: 'Application Overview' },
|
||||
{ id: 'features', label: 'Key Features' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
title: '📊 Operations',
|
||||
items: [
|
||||
{ id: 'bookings', label: 'Bookings' },
|
||||
{ id: 'bookings-how', label: '→ How-To' },
|
||||
{ id: 'passengers', label: 'Passengers' },
|
||||
{ id: 'passengers-how', label: '→ How-To' },
|
||||
{ id: 'tickets', label: 'Tickets' },
|
||||
{ id: 'tickets-how', label: '→ How-To' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'masterdata',
|
||||
title: '🏢 Master Data',
|
||||
items: [
|
||||
{ id: 'stations', label: 'Stations' },
|
||||
{ id: 'stations-how', label: '→ How-To' },
|
||||
{ id: 'trains', label: 'Trains' },
|
||||
{ id: 'trains-how', label: '→ How-To' },
|
||||
{ id: 'coaches', label: 'Coaches' },
|
||||
{ id: 'coaches-how', label: '→ How-To' },
|
||||
{ id: 'seats', label: 'Seats' },
|
||||
{ id: 'seats-how', label: '→ How-To' },
|
||||
{ id: 'classes', label: 'Seat Classes' },
|
||||
{ id: 'classes-how', label: '→ How-To' },
|
||||
{ id: 'routes', label: 'Routes' },
|
||||
{ id: 'routes-how', label: '→ How-To' },
|
||||
{ id: 'schedules', label: 'Schedules' },
|
||||
{ id: 'schedules-how', label: '→ How-To' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'financial',
|
||||
title: '💰 Financial',
|
||||
items: [
|
||||
{ id: 'pricing', label: 'Pricing & Fares' },
|
||||
{ id: 'pricing-how', label: '→ How-To' },
|
||||
{ id: 'currencies', label: 'Currencies' },
|
||||
{ id: 'currencies-how', label: '→ How-To' },
|
||||
{ id: 'payments', label: 'Payments' },
|
||||
{ id: 'payments-how', label: '→ How-To' },
|
||||
{ id: 'promos', label: 'Promo Codes' },
|
||||
{ id: 'promos-how', label: '→ How-To' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'services',
|
||||
title: '🎁 Customer Services',
|
||||
items: [
|
||||
{ id: 'loyalty', label: 'Loyalty' },
|
||||
{ id: 'loyalty-how', label: '→ How-To' },
|
||||
{ id: 'support', label: 'Support' },
|
||||
{ id: 'support-how', label: '→ How-To' },
|
||||
{ id: 'notifications', label: 'Notifications' },
|
||||
{ id: 'notifications-how', label: '→ How-To' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
title: '🔒 Security',
|
||||
items: [
|
||||
{ id: 'audit', label: 'Audit Logs' },
|
||||
{ id: 'audit-how', label: '→ How-To' },
|
||||
{ id: 'fraud', label: 'Fraud Detection' },
|
||||
{ id: 'fraud-how', label: '→ How-To' },
|
||||
{ id: 'verifayda', label: 'Verifayda' },
|
||||
{ id: 'verifayda-how', label: '→ How-To' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
title: '📈 Analytics',
|
||||
items: [
|
||||
{ id: 'reports', label: 'Reports' },
|
||||
{ id: 'reports-how', label: '→ How-To' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
title: '⚙️ System',
|
||||
items: [
|
||||
{ id: 'agents', label: 'Agents' },
|
||||
{ id: 'agents-how', label: '→ How-To' },
|
||||
{ id: 'users', label: 'Users' },
|
||||
{ id: 'users-how', label: '→ How-To' },
|
||||
{ id: 'settings', label: 'Settings' },
|
||||
{ id: 'settings-how', label: '→ How-To' },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
const HowToStep = ({ number, title, children }: { number: number; title: string; children: React.ReactNode }) => (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 text-white font-bold flex-shrink-0">{number}</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">{title}</h4>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
||||
<div className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="h-8 w-8 text-emerald-600" />
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">Documentation</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<a href="http://localhost:4000/api-docs" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 transition" title="Opens API documentation in new tab">
|
||||
<FileText className="h-4 w-4" />
|
||||
View API Docs
|
||||
</a>
|
||||
<Link href="/dashboard" target="_blank" className="flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-600 text-white hover:bg-emerald-700 transition">
|
||||
<Home className="h-4 w-4" />
|
||||
Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 sticky top-24 h-fit">
|
||||
<nav>
|
||||
{sections.map(section => (
|
||||
<div key={section.id}>
|
||||
<button onClick={() => toggleSection(section.id)} className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-700 border-b border-slate-100 dark:border-slate-700">
|
||||
<span>{section.title}</span>
|
||||
{expandedSections[section.id] ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</button>
|
||||
{expandedSections[section.id] && (
|
||||
<div className="bg-slate-50 dark:bg-slate-700/50">
|
||||
{section.items.map(item => (
|
||||
<button key={item.id} onClick={() => scrollToSection(item.id)} className="w-full text-left px-6 py-2 text-sm text-slate-600 dark:text-slate-300 hover:text-emerald-600 dark:hover:text-emerald-400 hover:bg-white dark:hover:bg-slate-700 transition">
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-3">
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-8 space-y-12">
|
||||
|
||||
<div id="about">
|
||||
<h2 className="text-3xl font-bold text-slate-900 dark:text-white mb-4">Welcome to EDR Passenger Backoffice</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Comprehensive management system for the Ethio-Djibouti Railway passenger platform. This documentation provides complete guidance on all features, operations, and best practices.</p>
|
||||
</div>
|
||||
|
||||
<div id="features" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🌟 Key Features</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Complete booking, passenger, fleet, and financial management.</p>
|
||||
</div>
|
||||
|
||||
{/* BOOKINGS */}
|
||||
<div id="bookings" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">📋 Bookings</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage passenger bookings with search, view, modify, and refund capabilities.</p>
|
||||
</div>
|
||||
|
||||
<div id="bookings-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">📋 How-To: Manage Bookings</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Bookings">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Bookings" in Operations section</li>
|
||||
<li>View all bookings in table format</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Search & Filter">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Use search box for reference, email, or phone</li>
|
||||
<li>Use Status dropdown to filter</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Manage Bookings">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "View Details" for full information</li>
|
||||
<li>Click "Cancel Booking" to process refunds</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PASSENGERS */}
|
||||
<div id="passengers" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">👥 Passengers</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage passenger profiles, loyalty, and verification status.</p>
|
||||
</div>
|
||||
|
||||
<div id="passengers-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">👥 How-To: Manage Passengers</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Passengers">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Passengers" in Operations</li>
|
||||
<li>View all profiles with pagination</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Search & Filter">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Search by name, email, phone, ID</li>
|
||||
<li>Filter by nationality, verification, loyalty tier</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="View Profile">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click passenger row to open modal</li>
|
||||
<li>View account, loyalty, wallet, booking history</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TICKETS */}
|
||||
<div id="tickets" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🎫 Tickets</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage ticket generation, tracking, and validation.</p>
|
||||
</div>
|
||||
|
||||
<div id="tickets-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🎫 How-To: Manage Tickets</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Tickets">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Tickets" in Operations</li>
|
||||
<li>View all issued tickets with status</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Search Tickets">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Search by booking reference or ticket number</li>
|
||||
<li>Filter by validation status</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Download PDF">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click ticket to view details</li>
|
||||
<li>Click "Download PDF" for printable version</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* STATIONS */}
|
||||
<div id="stations" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🏢 Stations</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Configure railway stations with locations and timezones.</p>
|
||||
</div>
|
||||
|
||||
<div id="stations-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🏢 How-To: Manage Stations</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Stations">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Stations" in Master Data</li>
|
||||
<li>View all configured stations</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Create Station">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add Station"</li>
|
||||
<li>Enter code, name, city, timezone, coordinates</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Edit Station">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click station to open details</li>
|
||||
<li>Update information and save</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TRAINS */}
|
||||
<div id="trains" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🚂 Trains</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage train fleet with coach assignments.</p>
|
||||
</div>
|
||||
|
||||
<div id="trains-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🚂 How-To: Manage Trains</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Trains">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Trains" in Master Data</li>
|
||||
<li>View all trains and coaches</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Create Train">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add Train"</li>
|
||||
<li>Enter code and select coaches</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Assign Coaches">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click train to edit</li>
|
||||
<li>Add/remove coaches with position numbers</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* COACHES */}
|
||||
<div id="coaches" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🚃 Coaches</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage coach inventory with seat configurations.</p>
|
||||
</div>
|
||||
|
||||
<div id="coaches-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🚃 How-To: Manage Coaches</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Coaches">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Coaches" in Master Data</li>
|
||||
<li>View all coaches and assignments</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Create Coach">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add Coach"</li>
|
||||
<li>Enter code, select train, define seat layout</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Configure Seats">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click coach to edit</li>
|
||||
<li>Add seats and assign classes</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SEATS */}
|
||||
<div id="seats" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">💺 Seats</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage seat inventory with visual maps.</p>
|
||||
</div>
|
||||
|
||||
<div id="seats-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">💺 How-To: Manage Seats</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Seat Map">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Go to "Seats" in Master Data</li>
|
||||
<li>Select coach from dropdown</li>
|
||||
<li>Visual map shows: Green=Available, Red=Blocked</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Block Seat">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click available seat</li>
|
||||
<li>Click "Block" and select reason</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Unblock Seat">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click blocked seat</li>
|
||||
<li>Click "Unblock" to restore</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SEAT CLASSES */}
|
||||
<div id="classes" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🎯 Seat Classes</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Define seat class types with pricing.</p>
|
||||
</div>
|
||||
|
||||
<div id="classes-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🎯 How-To: Manage Seat Classes</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Classes">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Seat Classes" in Master Data</li>
|
||||
<li>View all class types</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Create Class">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add Class"</li>
|
||||
<li>Enter name, base fare, premium, insurance</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Update Pricing">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click class to edit</li>
|
||||
<li>Update fares and save</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ROUTES */}
|
||||
<div id="routes" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🛤️ Routes</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Define railway routes with ordered stops.</p>
|
||||
</div>
|
||||
|
||||
<div id="routes-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🛤️ How-To: Manage Routes</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Routes">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Routes" in Master Data</li>
|
||||
<li>View all routes and stops</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Create Route">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add Route"</li>
|
||||
<li>Enter code and description</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Add Stops">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click route to edit</li>
|
||||
<li>Click "Add Stop" and select station</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SCHEDULES */}
|
||||
<div id="schedules" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">📅 Schedules</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Create and manage train schedules.</p>
|
||||
</div>
|
||||
|
||||
<div id="schedules-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">📅 How-To: Create Schedules</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Create Single">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Go to "Schedules" in Master Data</li>
|
||||
<li>Click "Create Schedule"</li>
|
||||
<li>Fill train, route, departure/arrival times</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Bulk Generate">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Bulk Generate"</li>
|
||||
<li>Set recurring parameters and generate</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Manage Status">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click schedule to edit</li>
|
||||
<li>Update times and view fares</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PRICING */}
|
||||
<div id="pricing" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">💰 Pricing & Fares</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Configure dynamic pricing with segments.</p>
|
||||
</div>
|
||||
|
||||
<div id="pricing-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">💰 How-To: Configure Pricing</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Pricing">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Pricing & Fares" in Financial</li>
|
||||
<li>Two tabs: Schedule Fares, Segment Fares</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Schedule Fares">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add Fare Rule"</li>
|
||||
<li>Fill schedule, seat class, fare, nationality</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Segment Fares">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Switch to "Segment Fares" tab</li>
|
||||
<li>Select route and add origin/destination fare</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CURRENCIES */}
|
||||
<div id="currencies" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">💵 Currencies</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage exchange rates for multiple currencies.</p>
|
||||
</div>
|
||||
|
||||
<div id="currencies-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">💵 How-To: Manage Currencies</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Rates">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Currencies" in Financial</li>
|
||||
<li>View all configured rates</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Add Rate">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add Rate"</li>
|
||||
<li>Select currency and enter exchange rate</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Sync Rates">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click rate to edit</li>
|
||||
<li>Click "Sync" to update from provider</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PAYMENTS */}
|
||||
<div id="payments" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">💳 Payments</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Monitor and process transactions.</p>
|
||||
</div>
|
||||
|
||||
<div id="payments-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">💳 How-To: Manage Payments</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Transactions">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Payments" in Financial</li>
|
||||
<li>View all transactions</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Search & Filter">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Search by booking or transaction ID</li>
|
||||
<li>Filter by status and payment method</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Process Refunds">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click transaction</li>
|
||||
<li>Click "Refund" if eligible</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PROMOS */}
|
||||
<div id="promos" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🎁 Promo Codes</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Create and manage promotional campaigns.</p>
|
||||
</div>
|
||||
|
||||
<div id="promos-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🎁 How-To: Manage Promo Codes</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Promos">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Promo Codes" in Financial</li>
|
||||
<li>View all active codes</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Create Code">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add Promo Code"</li>
|
||||
<li>Enter code, discount type, validity dates</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Track Usage">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click code to view analytics</li>
|
||||
<li>View usage count and savings</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LOYALTY */}
|
||||
<div id="loyalty" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🏆 Loyalty</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage loyalty program and rewards.</p>
|
||||
</div>
|
||||
|
||||
<div id="loyalty-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🏆 How-To: Manage Loyalty</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Accounts">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Loyalty Program" in Services</li>
|
||||
<li>View all loyalty accounts</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Adjust Points">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click account</li>
|
||||
<li>Click "Adjust Points" and enter amount</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Award Rewards">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click account</li>
|
||||
<li>Click "Grant Reward" and select reward</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SUPPORT */}
|
||||
<div id="support" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">💬 Support</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage support tickets and conversations.</p>
|
||||
</div>
|
||||
|
||||
<div id="support-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">💬 How-To: Manage Support</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Tickets">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Support Center" in Services</li>
|
||||
<li>View all support tickets</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Manage Ticket">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click ticket to open conversation</li>
|
||||
<li>Add replies and update status</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Manage FAQ">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Go to FAQ management</li>
|
||||
<li>Add or edit FAQ articles</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NOTIFICATIONS */}
|
||||
<div id="notifications" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🔔 Notifications</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Send notifications via multiple channels.</p>
|
||||
</div>
|
||||
|
||||
<div id="notifications-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🔔 How-To: Manage Notifications</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Notifications">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Notifications" in Services</li>
|
||||
<li>View notification history</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Send Notification">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Send Notification"</li>
|
||||
<li>Select channel and message</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Manage Templates">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Go to Templates section</li>
|
||||
<li>Create or edit templates with variables</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AUDIT */}
|
||||
<div id="audit" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">📋 Audit Logs</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Monitor system activities and user actions.</p>
|
||||
</div>
|
||||
|
||||
<div id="audit-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">📋 How-To: View Audit Logs</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Logs">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Audit Logs" in Security</li>
|
||||
<li>View all recorded activities</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Filter Logs">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Filter by user, action, or date</li>
|
||||
<li>Search by entity ID</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Export Logs">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click log entry for details</li>
|
||||
<li>Click "Export" to download CSV</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FRAUD */}
|
||||
<div id="fraud" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">🛡️ Fraud Detection</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Monitor and manage fraud alerts.</p>
|
||||
</div>
|
||||
|
||||
<div id="fraud-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">🛡️ How-To: Manage Fraud Detection</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Alerts">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Fraud Detection" in Security</li>
|
||||
<li>View all fraud alerts</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Investigate">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click alert to view details</li>
|
||||
<li>Review triggered rules and patterns</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Take Action">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Allow" or "Block" with notes</li>
|
||||
<li>Update user status</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VERIFAYDA */}
|
||||
<div id="verifayda" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">✅ Verifayda</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Verify passenger identities against government database.</p>
|
||||
</div>
|
||||
|
||||
<div id="verifayda-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">✅ How-To: Manage Verifayda</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Verification">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Verifayda Integration" in Security</li>
|
||||
<li>View verification history</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Verify Passenger">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Enter national ID or passport number</li>
|
||||
<li>Click "Verify" to check database</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Review Results">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>View verified passenger data</li>
|
||||
<li>Match with booking details</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* REPORTS */}
|
||||
<div id="reports" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">📊 Reports</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Generate business analytics and reports.</p>
|
||||
</div>
|
||||
|
||||
<div id="reports-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">📊 How-To: Generate Reports</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Reports">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Reports" in Analytics</li>
|
||||
<li>View available report types</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Generate Report">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click report type</li>
|
||||
<li>Select date range and parameters</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Export Report">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>View report with charts</li>
|
||||
<li>Click "Export" for PDF or CSV</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AGENTS */}
|
||||
<div id="agents" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">👤 Agents</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage booking agents and commissions.</p>
|
||||
</div>
|
||||
|
||||
<div id="agents-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">👤 How-To: Manage Agents</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Agents">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Agents" in System</li>
|
||||
<li>View all agents</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Create Agent">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add Agent"</li>
|
||||
<li>Enter name, email, commission rate</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Create Shift">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click agent to edit</li>
|
||||
<li>Click "Create Shift" to assign schedule</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* USERS */}
|
||||
<div id="users" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">👥 Users</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Manage backoffice user accounts and permissions.</p>
|
||||
</div>
|
||||
|
||||
<div id="users-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">👥 How-To: Manage Users</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="View Users">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Users" in System</li>
|
||||
<li>View all user accounts</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Create User">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Add User"</li>
|
||||
<li>Enter email, name, select role</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Manage Permissions">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click user to edit</li>
|
||||
<li>Adjust roles and permissions</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SETTINGS */}
|
||||
<div id="settings" className="border-t pt-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-4">⚙️ Settings</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300">Configure system-wide settings and integrations.</p>
|
||||
</div>
|
||||
|
||||
<div id="settings-how" className="border-t pt-8">
|
||||
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">⚙️ How-To: Configure Settings</h3>
|
||||
<div className="space-y-4">
|
||||
<HowToStep number={1} title="Access Settings">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Click "Settings" in System</li>
|
||||
<li>View configuration options</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={2} title="Configure Email">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Go to Email tab</li>
|
||||
<li>Enter SendGrid API key and email</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
<HowToStep number={3} title="Configure API Keys">
|
||||
<ol className="list-decimal pl-5 space-y-1 text-sm text-slate-700 dark:text-slate-300">
|
||||
<li>Go to API tab</li>
|
||||
<li>Add payment and Verifayda keys</li>
|
||||
</ol>
|
||||
</HowToStep>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 bg-slate-900 text-white py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 text-center text-slate-400">
|
||||
<p>© 2026 Ethio-Djibouti Railway | Passenger Backoffice Documentation v1.0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocPage;
|
||||
537
apps/edr-passenger-web/backoffice/src/app/how-to/page.tsx
Normal file
537
apps/edr-passenger-web/backoffice/src/app/how-to/page.tsx
Normal file
@@ -0,0 +1,537 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ChevronDown, ChevronRight, FileText, Home } from 'lucide-react';
|
||||
|
||||
const HowToPage = () => {
|
||||
const scrollToSection = (id: string) => {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
const headerOffset = 120;
|
||||
const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
|
||||
const offsetPosition = elementPosition - headerOffset;
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const guides = [
|
||||
{ id: 'bookings', title: 'How to Manage Bookings', icon: '📋' },
|
||||
{ id: 'passengers', title: 'How to Manage Passengers', icon: '👥' },
|
||||
{ id: 'pricing', title: 'How to Configure Pricing', icon: '💰' },
|
||||
{ id: 'schedules', title: 'How to Create Schedules', icon: '📅' },
|
||||
{ id: 'seats', title: 'How to Manage Seats', icon: '💺' },
|
||||
{ id: 'loyalty', title: 'How to Manage Loyalty', icon: '🏆' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
||||
<div className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="h-8 w-8 text-emerald-600" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">How-To Guides</h1>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">Step-by-step instructions for common tasks</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/docs" className="flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-600 text-white hover:bg-emerald-700 transition">
|
||||
<FileText className="h-4 w-4" />
|
||||
Back to Docs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 sticky top-24 h-fit">
|
||||
<nav className="p-4 space-y-2">
|
||||
{guides.map(guide => (
|
||||
<button
|
||||
key={guide.id}
|
||||
onClick={() => scrollToSection(guide.id)}
|
||||
className="w-full text-left px-4 py-3 rounded-lg text-sm font-medium text-slate-600 dark:text-slate-300 hover:text-emerald-600 dark:hover:text-emerald-400 hover:bg-slate-50 dark:hover:bg-slate-700 transition"
|
||||
>
|
||||
{guide.icon} {guide.title}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-3">
|
||||
<div className="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-8 space-y-12">
|
||||
|
||||
{/* Bookings How-To */}
|
||||
<div id="bookings" className="pt-4">
|
||||
<h2 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">📋 How to Manage Bookings</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300 mb-6">Learn how to search, view, modify, and cancel passenger bookings in the system.</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 text-white font-bold flex-shrink-0">1</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Access the Bookings Page</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click on <strong>"Bookings"</strong> in the Operations section of the sidebar</li>
|
||||
<li>The page loads showing a table with all bookings</li>
|
||||
<li>You'll see columns: Reference, Passenger, Status, Amount, Payment, Created date</li>
|
||||
</ol>
|
||||
<div className="mt-4 p-3 bg-white dark:bg-slate-800 rounded border-l-4 border-blue-600">
|
||||
<p className="text-sm font-mono text-slate-600 dark:text-slate-400">📍 Path: Sidebar → Operations → Bookings</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-green-600 text-white font-bold flex-shrink-0">2</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Search for a Booking</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Find the search box at the top of the booking table</li>
|
||||
<li>Type in: booking reference (e.g., "BK123"), email, or phone number</li>
|
||||
<li>Results update in real-time as you type</li>
|
||||
<li>Optional: Use the Status dropdown to filter (All, Pending Payment, Confirmed, Cancelled, Completed)</li>
|
||||
</ol>
|
||||
<div className="mt-4 p-3 bg-white dark:bg-slate-800 rounded">
|
||||
<p className="text-sm"><strong>💡 Tip:</strong> Search is case-insensitive and supports partial matches</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-purple-600 text-white font-bold flex-shrink-0">3</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">View Booking Details</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Find the booking in the table</li>
|
||||
<li>Click the <strong>"View Details"</strong> button on the right side</li>
|
||||
<li>Modal window opens showing complete information:
|
||||
<ul className="list-disc pl-8 mt-2 space-y-1">
|
||||
<li>Booking reference and status</li>
|
||||
<li>Passenger name and contact details</li>
|
||||
<li>Journey information (schedule, adults, children)</li>
|
||||
<li>Payment details and amount</li>
|
||||
<li>All metadata and timestamps</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 dark:bg-orange-900/20 p-6 rounded-lg border border-orange-200 dark:border-orange-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-orange-600 text-white font-bold flex-shrink-0">4</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Cancel a Booking with Refund</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Find the booking in the table</li>
|
||||
<li>Click the <strong>"Cancel Booking"</strong> button (red)</li>
|
||||
<li>Confirmation dialog appears</li>
|
||||
<li>Click <strong>"Confirm"</strong> to proceed</li>
|
||||
<li>System calculates and processes refund:
|
||||
<ul className="list-disc pl-8 mt-2 text-sm">
|
||||
<li>Confirmed bookings: 80% refund</li>
|
||||
<li>Pending bookings: 0% refund</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Status changes to <strong>"CANCELLED"</strong></li>
|
||||
<li>Success message appears</li>
|
||||
</ol>
|
||||
<div className="mt-4 p-3 bg-white dark:bg-slate-800 rounded border-l-4 border-orange-600">
|
||||
<p className="text-sm"><strong>⚠️ Important:</strong> Cannot be undone. Seats are automatically released.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 dark:bg-red-900/20 p-6 rounded-lg border border-red-200 dark:border-red-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-red-600 text-white font-bold flex-shrink-0">5</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Export Bookings</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click the <strong>"Export"</strong> button (top-right)</li>
|
||||
<li>CSV file downloads automatically</li>
|
||||
<li>Includes all current filters applied</li>
|
||||
<li>Use for external analysis or backup</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Passengers How-To */}
|
||||
<div id="passengers" className="border-t pt-8">
|
||||
<h2 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">👥 How to Manage Passengers</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300 mb-6">Learn how to search, filter, and view passenger profiles with loyalty and verification data.</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 text-white font-bold flex-shrink-0">1</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Access Passengers Page</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click <strong>"Passengers"</strong> in the Operations section</li>
|
||||
<li>Page displays all passenger profiles</li>
|
||||
<li>Default view shows 20 passengers per page</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-green-600 text-white font-bold flex-shrink-0">2</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Search & Filter</h3>
|
||||
<div className="space-y-3 text-slate-700 dark:text-slate-300">
|
||||
<div>
|
||||
<p className="font-semibold mb-2">Search by:</p>
|
||||
<ul className="list-disc pl-5 space-y-1 text-sm">
|
||||
<li>Full name</li>
|
||||
<li>Email address</li>
|
||||
<li>Phone number</li>
|
||||
<li>National ID</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold mb-2">Filter by:</p>
|
||||
<ul className="list-disc pl-5 space-y-1 text-sm">
|
||||
<li><strong>Nationality:</strong> Ethiopian, Djiboutian, Other</li>
|
||||
<li><strong>Verifayda Status:</strong> Verified, Unverified, Pending</li>
|
||||
<li><strong>Loyalty Tier:</strong> Bronze, Silver, Gold, Platinum</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-purple-600 text-white font-bold flex-shrink-0">3</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">View Complete Profile</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click on any passenger row</li>
|
||||
<li>Detailed profile modal opens showing:
|
||||
<ul className="list-disc pl-8 mt-2 space-y-1 text-sm">
|
||||
<li>Account info (email, phone, nationality)</li>
|
||||
<li>Verifayda verification status</li>
|
||||
<li>Loyalty tier and points</li>
|
||||
<li>Wallet balance</li>
|
||||
<li>Booking history with links</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<div className="mt-4 p-3 bg-white dark:bg-slate-800 rounded">
|
||||
<p className="text-sm"><strong>ℹ️ Note:</strong> Read-only view. Updates via passenger portal.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing How-To */}
|
||||
<div id="pricing" className="border-t pt-8">
|
||||
<h2 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">💰 How to Configure Pricing</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300 mb-6">Learn how to set up dynamic fares with segment pricing and nationality overrides.</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 text-white font-bold flex-shrink-0">1</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Access Pricing Page</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click <strong>"Pricing & Fares"</strong> in Financial section</li>
|
||||
<li>Two tabs: <strong>Schedule Fares</strong> and <strong>Segment Fares</strong></li>
|
||||
<li>Default tab shows Schedule Fares</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-green-600 text-white font-bold flex-shrink-0">2</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Create Schedule Fare Rule</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click <strong>"Add Fare Rule"</strong></li>
|
||||
<li>Fill in form:
|
||||
<ul className="list-disc pl-8 mt-2 space-y-1 text-sm">
|
||||
<li><strong>Schedule (optional):</strong> Leave empty for global</li>
|
||||
<li><strong>Route Code (optional):</strong> e.g., "ADD-DJI"</li>
|
||||
<li><strong>Seat Class (required):</strong> Economy Regular, VIP Bed, etc.</li>
|
||||
<li><strong>Fare in ETB (required):</strong> e.g., 350.00</li>
|
||||
<li><strong>Passenger Type (optional):</strong> ADULT or CHILD</li>
|
||||
<li><strong>Nationality (optional):</strong> Ethiopian, Djiboutian, Other</li>
|
||||
<li><strong>Valid From & Until:</strong> Set date range</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click <strong>"Save Fare Rule"</strong></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-purple-600 text-white font-bold flex-shrink-0">3</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Create Segment Fare Rule</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click <strong>"Add Fare Rule"</strong></li>
|
||||
<li>Switch to <strong>"Segment Fares"</strong> tab</li>
|
||||
<li>Select route from dropdown</li>
|
||||
<li>Fill in form:
|
||||
<ul className="list-disc pl-8 mt-2 space-y-1 text-sm">
|
||||
<li><strong>Origin Station (required):</strong> Starting point</li>
|
||||
<li><strong>Destination Station (required):</strong> Must be after origin</li>
|
||||
<li><strong>Seat Class (required):</strong> Class type</li>
|
||||
<li><strong>Fare in ETB (required):</strong> Segment price</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click <strong>"Save Segment Fare Rule"</strong></li>
|
||||
</ol>
|
||||
<div className="mt-4 p-3 bg-white dark:bg-slate-800 rounded">
|
||||
<p className="text-sm"><strong>Example:</strong> ADD (Stop 1) to DDA (Stop 4) at 250 ETB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schedules How-To */}
|
||||
<div id="schedules" className="border-t pt-8">
|
||||
<h2 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">📅 How to Create Schedules</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300 mb-6">Learn how to create schedules manually or in bulk with recurring patterns.</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 text-white font-bold flex-shrink-0">1</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Create Single Schedule</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Go to <strong>Schedules</strong> page (Master Data)</li>
|
||||
<li>Click <strong>"Create Schedule"</strong></li>
|
||||
<li>Fill in required fields:
|
||||
<ul className="list-disc pl-8 mt-2 space-y-1 text-sm">
|
||||
<li><strong>Train:</strong> Select from dropdown</li>
|
||||
<li><strong>Route:</strong> Select from dropdown</li>
|
||||
<li><strong>Departure Date & Time:</strong> Pick from date/time picker</li>
|
||||
<li><strong>Arrival Date & Time:</strong> Must be after departure</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Select coaches to assign</li>
|
||||
<li>Click <strong>"Create Schedule"</strong></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-green-600 text-white font-bold flex-shrink-0">2</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Bulk Generate Recurring Schedules</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click <strong>"Bulk Generate"</strong> button</li>
|
||||
<li>Fill in generation form:
|
||||
<ul className="list-disc pl-8 mt-2 space-y-1 text-sm">
|
||||
<li><strong>Train (required):</strong> Select train</li>
|
||||
<li><strong>Route (required):</strong> Select route</li>
|
||||
<li><strong>Start Date & Time (required):</strong> First departure</li>
|
||||
<li><strong>Duration (Hours):</strong> Trip length</li>
|
||||
<li><strong>Repeat Every (Days):</strong> Daily or custom</li>
|
||||
<li><strong>For Next (Days):</strong> How many days</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Review preview showing number of schedules</li>
|
||||
<li>Click <strong>"Generate Schedules"</strong></li>
|
||||
</ol>
|
||||
<div className="mt-4 p-3 bg-white dark:bg-slate-800 rounded border-l-4 border-green-600">
|
||||
<p className="text-sm"><strong>Example:</strong> 30 days ÷ 1 day = ~30 daily schedules</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seats How-To */}
|
||||
<div id="seats" className="border-t pt-8">
|
||||
<h2 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">💺 How to Manage Seats</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300 mb-6">Learn how to view, block, and manage seat inventory using visual seat maps.</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 text-white font-bold flex-shrink-0">1</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">View Seat Map</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Go to <strong>Seats</strong> page (Master Data)</li>
|
||||
<li>Select a coach from dropdown</li>
|
||||
<li>Visual seat map displays</li>
|
||||
<li>Color-coded by status:
|
||||
<ul className="list-disc pl-8 mt-2 text-sm">
|
||||
<li>🟢 Green: Available</li>
|
||||
<li>🟡 Yellow: Held</li>
|
||||
<li>🔵 Blue: Booked</li>
|
||||
<li>🔴 Red: Blocked</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-green-600 text-white font-bold flex-shrink-0">2</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Block a Seat</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click on an available (green) seat</li>
|
||||
<li>Click <strong>"Block"</strong> button</li>
|
||||
<li>Select reason:
|
||||
<ul className="list-disc pl-8 mt-2 text-sm">
|
||||
<li>Maintenance</li>
|
||||
<li>Reserved</li>
|
||||
<li>Damaged</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Set until date (optional)</li>
|
||||
<li>Add notes</li>
|
||||
<li>Click <strong>"Block Seat"</strong></li>
|
||||
<li>Seat turns red</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-purple-600 text-white font-bold flex-shrink-0">3</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Unblock a Seat</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click on a blocked (red) seat</li>
|
||||
<li>Click <strong>"Unblock"</strong> button</li>
|
||||
<li>Confirm action</li>
|
||||
<li>Seat becomes available (green)</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loyalty How-To */}
|
||||
<div id="loyalty" className="border-t pt-8">
|
||||
<h2 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">🏆 How to Manage Loyalty Program</h2>
|
||||
<p className="text-slate-600 dark:text-slate-300 mb-6">Learn how to view loyalty accounts, manage points, and administer rewards.</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 text-white font-bold flex-shrink-0">1</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">View Loyalty Accounts</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Go to <strong>Loyalty Program</strong> (Customer Services)</li>
|
||||
<li>Table displays all loyalty accounts</li>
|
||||
<li>Columns: Name, Tier, Points Balance, Lifetime Points</li>
|
||||
<li>Search by name or filter by tier</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-green-600 text-white font-bold flex-shrink-0">2</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Adjust Points</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click on a loyalty account</li>
|
||||
<li>Click <strong>"Adjust Points"</strong> button</li>
|
||||
<li>Enter points to add/subtract</li>
|
||||
<li>Select reason: Bonus, Correction, Promotion, etc.</li>
|
||||
<li>Add optional notes</li>
|
||||
<li>Click <strong>"Apply"</strong></li>
|
||||
<li>Balance updates immediately</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-purple-600 text-white font-bold flex-shrink-0">3</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-2">Award Rewards</h3>
|
||||
<ol className="list-decimal pl-5 space-y-2 text-slate-700 dark:text-slate-300">
|
||||
<li>Click on a loyalty account</li>
|
||||
<li>Click <strong>"Grant Reward"</strong> button</li>
|
||||
<li>Select reward from list</li>
|
||||
<li>Specify quantity if applicable</li>
|
||||
<li>Click <strong>"Award"</strong></li>
|
||||
<li>Confirmation email sent to passenger</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common Tips */}
|
||||
<div className="bg-gradient-to-r from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/20 p-6 rounded-lg border border-emerald-200 dark:border-emerald-800 border-t pt-8">
|
||||
<h3 className="text-lg font-semibold text-emerald-900 dark:text-emerald-200 mb-4">💡 Common Tips & Tricks</h3>
|
||||
<ul className="list-disc pl-6 space-y-2 text-emerald-800 dark:text-emerald-300 text-sm">
|
||||
<li><strong>Keyboard Shortcuts:</strong> Tab to navigate, Enter to submit</li>
|
||||
<li><strong>Pagination:</strong> Change page size or jump to specific page</li>
|
||||
<li><strong>Sidebar Collapse:</strong> Use chevron to minimize sidebar</li>
|
||||
<li><strong>Dark Mode:</strong> Toggle with sun/moon icon in header</li>
|
||||
<li><strong>Error Messages:</strong> Red text above forms if validation fails</li>
|
||||
<li><strong>Success Notifications:</strong> Green banner appears for 3 seconds</li>
|
||||
<li><strong>Undo Not Available:</strong> Most actions cannot be undone</li>
|
||||
<li><strong>Real-time Updates:</strong> Refresh page to see changes by other users</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 bg-slate-900 text-white py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 text-center text-slate-400">
|
||||
<p>© 2026 Ethio-Djibouti Railway | How-To Guides v1.0</p>
|
||||
<p className="text-sm mt-2">Last Updated: January 15, 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HowToPage;
|
||||
@@ -52,6 +52,44 @@ export default function PassengersPage() {
|
||||
console.error('Passengers API Error:', error);
|
||||
}
|
||||
|
||||
const handleExportPassengers = async () => {
|
||||
const selectedColumns = prompt(
|
||||
'Select columns to export (comma-separated):\n\n' +
|
||||
'Available: fullName, email, phone, dateOfBirth, gender, nationality, verified\n\n' +
|
||||
'Default: fullName, email, phone, gender, nationality, verified',
|
||||
'fullName, email, phone, gender, nationality, verified'
|
||||
);
|
||||
|
||||
if (!selectedColumns) return;
|
||||
|
||||
const cols = selectedColumns.split(',').map(c => c.trim());
|
||||
const csv = [
|
||||
cols.join(','),
|
||||
...data?.items?.map((passenger: any) => {
|
||||
const values = cols.map(col => {
|
||||
switch(col) {
|
||||
case 'fullName': return passenger.fullName;
|
||||
case 'email': return passenger.email || '';
|
||||
case 'phone': return passenger.phone || '';
|
||||
case 'dateOfBirth': return passenger.dateOfBirth ? formatDate(passenger.dateOfBirth) : '';
|
||||
case 'gender': return passenger.gender || '';
|
||||
case 'nationality': return passenger.nationality || '';
|
||||
case 'verified': return passenger.nationalId ? 'Yes' : 'No';
|
||||
default: return '';
|
||||
}
|
||||
});
|
||||
return values.map(v => `"${v}"`).join(',');
|
||||
}) || []
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `passengers-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'fullName',
|
||||
@@ -67,16 +105,25 @@ export default function PassengersPage() {
|
||||
{
|
||||
key: 'phone',
|
||||
label: 'Phone',
|
||||
sortable: true,
|
||||
render: (passenger: any) => passenger.phone,
|
||||
},
|
||||
{
|
||||
key: 'nationalId',
|
||||
label: 'National ID',
|
||||
render: (passenger: any) => passenger.nationalId || 'N/A',
|
||||
key: 'gender',
|
||||
label: 'Gender',
|
||||
sortable: true,
|
||||
render: (passenger: any) => passenger.gender || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'nationality',
|
||||
label: 'Nationality',
|
||||
sortable: true,
|
||||
render: (passenger: any) => passenger.nationality || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'dateOfBirth',
|
||||
label: 'Date of Birth',
|
||||
sortable: true,
|
||||
render: (passenger: any) => passenger.dateOfBirth ? formatDate(passenger.dateOfBirth) : 'N/A',
|
||||
},
|
||||
{
|
||||
@@ -113,7 +160,7 @@ export default function PassengersPage() {
|
||||
<p className="text-muted-foreground">Manage passenger profiles and verification</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ActionButton variant="export" icon={Download}>Export</ActionButton>
|
||||
<ActionButton variant="export" icon={Download} onClick={handleExportPassengers}>Export</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -230,10 +277,6 @@ export default function PassengersPage() {
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-3">Identification</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-muted-foreground">National ID</label>
|
||||
<p className="text-lg font-mono font-semibold">{selectedPassenger.nationalId || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-muted-foreground">Passport Number</label>
|
||||
<p className="text-lg font-mono font-semibold">{selectedPassenger.passportNumber || 'N/A'}</p>
|
||||
|
||||
@@ -50,6 +50,7 @@ export default function PricingPage() {
|
||||
seatClassId: '',
|
||||
baseFare: '',
|
||||
nationality: '',
|
||||
passengerCategory: '',
|
||||
route: '',
|
||||
validFrom: new Date().toISOString().split('T')[0],
|
||||
validUntil: '',
|
||||
@@ -61,6 +62,7 @@ export default function PricingPage() {
|
||||
destinationStationId: '',
|
||||
baseFare: '',
|
||||
nationality: '',
|
||||
passengerCategory: '',
|
||||
validFrom: new Date().toISOString().split('T')[0],
|
||||
validUntil: '',
|
||||
});
|
||||
@@ -87,7 +89,17 @@ export default function PricingPage() {
|
||||
|
||||
const { data: fares = [], isLoading: faresLoading, refetch: refetchFares } = useQuery({
|
||||
queryKey: ['schedule-fares', selectedSchedule],
|
||||
queryFn: () => (selectedSchedule ? apiClient.get(`/schedules/${selectedSchedule}/fares/all`) : Promise.resolve([])),
|
||||
queryFn: async () => {
|
||||
if (!selectedSchedule) return [];
|
||||
try {
|
||||
const response = await apiClient.get(`/schedules/${selectedSchedule}/fares/all`);
|
||||
return Array.isArray(response) ? response : response.data || [];
|
||||
} catch (err: any) {
|
||||
const errMsg = err.response?.data?.message || err.message || 'Failed to load fares';
|
||||
setError(`Error loading fares: ${errMsg}`);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
enabled: !!selectedSchedule && tab === 'schedule',
|
||||
});
|
||||
|
||||
@@ -174,6 +186,7 @@ export default function PricingPage() {
|
||||
seatClassId: '',
|
||||
baseFare: '',
|
||||
nationality: '',
|
||||
passengerCategory: '',
|
||||
route: '',
|
||||
validFrom: new Date().toISOString().split('T')[0],
|
||||
validUntil: '',
|
||||
@@ -189,6 +202,7 @@ export default function PricingPage() {
|
||||
destinationStationId: '',
|
||||
baseFare: '',
|
||||
nationality: '',
|
||||
passengerCategory: '',
|
||||
validFrom: new Date().toISOString().split('T')[0],
|
||||
validUntil: '',
|
||||
});
|
||||
@@ -198,13 +212,11 @@ export default function PricingPage() {
|
||||
|
||||
const handleEditFare = (fare: any) => {
|
||||
setEditingFare(fare);
|
||||
const fareValue = fare.baseFare || fare.baseFareMinor || 0;
|
||||
const etbValue = fareValue > 100 ? (fareValue / 100).toString() : fareValue.toString();
|
||||
|
||||
setFareForm({
|
||||
seatClassId: fare.seatClassId || '',
|
||||
baseFare: etbValue,
|
||||
baseFare: (fare.baseFare || fare.baseFareMinor || 0).toString(),
|
||||
nationality: fare.nationality || '',
|
||||
passengerCategory: fare.passengerCategory || '',
|
||||
route: fare.route || '',
|
||||
validFrom: fare.validFrom ? new Date(fare.validFrom).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
||||
validUntil: fare.validUntil ? new Date(fare.validUntil).toISOString().split('T')[0] : '',
|
||||
@@ -215,9 +227,6 @@ export default function PricingPage() {
|
||||
|
||||
const handleEditSegmentFare = (fare: any) => {
|
||||
setEditingFare(fare);
|
||||
const fareValue = fare.baseFare || fare.baseFareMinor || 0;
|
||||
const etbValue = fareValue > 100 ? (fareValue / 100).toString() : fareValue.toString();
|
||||
|
||||
const routeStops = currentRoute?.stops || [];
|
||||
const originStop = routeStops.find((s: any) => s.sequence === fare.originStopSequence);
|
||||
const destStop = routeStops.find((s: any) => s.sequence === fare.destinationStopSequence);
|
||||
@@ -226,8 +235,9 @@ export default function PricingPage() {
|
||||
seatClassId: fare.seatClassId || '',
|
||||
originStationId: originStop?.stationId || '',
|
||||
destinationStationId: destStop?.stationId || '',
|
||||
baseFare: etbValue,
|
||||
baseFare: (fare.baseFare || fare.baseFareMinor || 0).toString(),
|
||||
nationality: fare.nationality || '',
|
||||
passengerCategory: fare.passengerCategory || '',
|
||||
validFrom: fare.validFrom ? new Date(fare.validFrom).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
|
||||
validUntil: fare.validUntil ? new Date(fare.validUntil).toISOString().split('T')[0] : '',
|
||||
});
|
||||
@@ -242,7 +252,7 @@ export default function PricingPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseFareMinor = Math.round(parseFloat(fareForm.baseFare) * 100);
|
||||
const baseFareMinor = parseInt(fareForm.baseFare, 10);
|
||||
|
||||
if (editingFare) {
|
||||
await updateFareMutation.mutateAsync({
|
||||
@@ -250,6 +260,7 @@ export default function PricingPage() {
|
||||
seatClassId: fareForm.seatClassId,
|
||||
baseFareMinor,
|
||||
nationality: fareForm.nationality || undefined,
|
||||
passengerCategory: fareForm.passengerCategory || undefined,
|
||||
route: fareForm.route || undefined,
|
||||
validFrom: fareForm.validFrom,
|
||||
validUntil: fareForm.validUntil || undefined,
|
||||
@@ -260,6 +271,7 @@ export default function PricingPage() {
|
||||
seatClassId: fareForm.seatClassId,
|
||||
baseFareMinor,
|
||||
nationality: fareForm.nationality || undefined,
|
||||
passengerCategory: fareForm.passengerCategory || undefined,
|
||||
route: fareForm.route || undefined,
|
||||
validFrom: fareForm.validFrom,
|
||||
validUntil: fareForm.validUntil || undefined,
|
||||
@@ -288,7 +300,7 @@ export default function PricingPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseFareMinor = Math.round(parseFloat(segmentForm.baseFare) * 100);
|
||||
const baseFareMinor = parseInt(segmentForm.baseFare, 10);
|
||||
|
||||
if (editingFare) {
|
||||
await updateSegmentFareMutation.mutateAsync({
|
||||
@@ -299,6 +311,7 @@ export default function PricingPage() {
|
||||
destinationStopSequence: destStop.sequence,
|
||||
baseFareMinor,
|
||||
nationality: segmentForm.nationality || undefined,
|
||||
passengerCategory: segmentForm.passengerCategory || undefined,
|
||||
validFrom: segmentForm.validFrom,
|
||||
validUntil: segmentForm.validUntil || undefined,
|
||||
});
|
||||
@@ -310,6 +323,7 @@ export default function PricingPage() {
|
||||
destinationStopSequence: destStop.sequence,
|
||||
baseFareMinor,
|
||||
nationality: segmentForm.nationality || undefined,
|
||||
passengerCategory: segmentForm.passengerCategory || undefined,
|
||||
validFrom: segmentForm.validFrom,
|
||||
validUntil: segmentForm.validUntil || undefined,
|
||||
});
|
||||
@@ -343,14 +357,20 @@ export default function PricingPage() {
|
||||
return <span className="font-medium">{className}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'passengerCategory',
|
||||
label: 'Passenger Type',
|
||||
render: (fare: any) => (
|
||||
<span className="text-sm">{fare.passengerCategory || 'All'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'baseFare',
|
||||
label: 'Fare (ETB)',
|
||||
render: (fare: any) => {
|
||||
const fareValue = fare.baseFare || fare.baseFareMinor;
|
||||
if (!fareValue && fareValue !== 0) return <span>N/A</span>;
|
||||
const etbValue = fareValue > 100 ? (fareValue / 100).toFixed(2) : parseFloat(fareValue).toFixed(2);
|
||||
return <span className="font-mono font-medium">{etbValue} ETB</span>;
|
||||
return <span className="font-mono font-medium">{fareValue} ETB</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -408,14 +428,20 @@ export default function PricingPage() {
|
||||
return <span className="font-medium">{className}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'passengerCategory',
|
||||
label: 'Passenger Type',
|
||||
render: (fare: any) => (
|
||||
<span className="text-sm">{fare.passengerCategory || 'All'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'baseFare',
|
||||
label: 'Fare (ETB)',
|
||||
render: (fare: any) => {
|
||||
const fareValue = fare.baseFare || fare.baseFareMinor;
|
||||
if (!fareValue && fareValue !== 0) return <span>N/A</span>;
|
||||
const etbValue = fareValue > 100 ? (fareValue / 100).toFixed(2) : parseFloat(fareValue).toFixed(2);
|
||||
return <span className="font-mono font-medium">{etbValue} ETB</span>;
|
||||
return <span className="font-mono font-medium">{fareValue} ETB</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -449,12 +475,14 @@ export default function PricingPage() {
|
||||
onClick: tab === 'schedule' ? handleEditFare : handleEditSegmentFare,
|
||||
variant: 'secondary' as const,
|
||||
icon: Edit,
|
||||
disabled: tab === 'schedule', // Schedule fares are computed, not stored
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: (fare: any) => setDeleteConfirm({ isOpen: true, id: fare.id }),
|
||||
variant: 'danger' as const,
|
||||
icon: Trash2,
|
||||
disabled: tab === 'schedule', // Schedule fares are computed, not stored
|
||||
},
|
||||
];
|
||||
|
||||
@@ -463,7 +491,7 @@ export default function PricingPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Pricing & Fares</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage fares by schedule and route segments</p>
|
||||
<p className="text-muted-foreground mt-1">Manage fares by schedule and route segments with passenger type pricing</p>
|
||||
</div>
|
||||
<ActionButton
|
||||
icon={Plus}
|
||||
@@ -475,6 +503,7 @@ export default function PricingPage() {
|
||||
seatClassId: '',
|
||||
baseFare: '',
|
||||
nationality: '',
|
||||
passengerCategory: '',
|
||||
route: '',
|
||||
validFrom: new Date().toISOString().split('T')[0],
|
||||
validUntil: '',
|
||||
@@ -486,6 +515,7 @@ export default function PricingPage() {
|
||||
destinationStationId: '',
|
||||
baseFare: '',
|
||||
nationality: '',
|
||||
passengerCategory: '',
|
||||
validFrom: new Date().toISOString().split('T')[0],
|
||||
validUntil: '',
|
||||
});
|
||||
@@ -547,26 +577,29 @@ export default function PricingPage() {
|
||||
|
||||
{selectedSchedule && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Fare Rules</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">Calculated Fares</h3>
|
||||
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded text-sm text-blue-800 dark:text-blue-200">
|
||||
These are <strong>dynamically calculated</strong> fares based on the fare engine. To create custom override fares, click "Add Fare Rule" above.
|
||||
</div>
|
||||
{faresLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : faresArray.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{`No fares defined. Click "Add Fare Rule" to create one.`}
|
||||
No fares available for this schedule.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4 p-3 bg-muted/50 rounded text-sm text-muted-foreground">
|
||||
{faresArray.length} fare rule(s) found
|
||||
{faresArray.length} seat class(es) available
|
||||
</div>
|
||||
<DataTable
|
||||
data={faresArray}
|
||||
columns={fareColumns}
|
||||
actions={fareActions}
|
||||
loading={false}
|
||||
emptyMessage="No fares found."
|
||||
emptyMessage="No fares available."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -632,16 +665,16 @@ export default function PricingPage() {
|
||||
<h3 className="font-semibold text-blue-900 dark:text-blue-200 mb-3">Pricing Structure</h3>
|
||||
<ul className="text-sm text-blue-800 dark:text-blue-300 space-y-2">
|
||||
<li>
|
||||
• <strong>Schedule Fares:</strong> Set custom pricing for each schedule by seat class
|
||||
• <strong>Schedule Fares:</strong> Set custom pricing for each schedule by seat class and passenger type
|
||||
</li>
|
||||
<li>
|
||||
• <strong>Segment Fares:</strong> Set fares for specific stop-to-stop segments (e.g., Addis → Dire Dawa)
|
||||
</li>
|
||||
<li>
|
||||
• <strong>Nationality-based:</strong> Override fares for specific nationalities
|
||||
• <strong>Passenger Type:</strong> ADULT (5+ years) or CHILD (<5) — first child travels free, subsequent children pay full fare
|
||||
</li>
|
||||
<li>
|
||||
• <strong>Age-Based Pricing:</strong> ADULT (5+ years) pays 100%, CHILD (<5) first child FREE, subsequent children 100%
|
||||
• <strong>Nationality-based:</strong> Override fares for specific nationalities (Ethiopian, Djiboutian, Other)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -732,28 +765,44 @@ export default function PricingPage() {
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
step="1"
|
||||
value={fareForm.baseFare}
|
||||
onChange={(e) => setFareForm({ ...fareForm, baseFare: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="e.g., 350.00"
|
||||
placeholder="e.g., 350"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Nationality (Optional)</label>
|
||||
<select
|
||||
value={fareForm.nationality}
|
||||
onChange={(e) => setFareForm({ ...fareForm, nationality: e.target.value })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="">All Nationalities</option>
|
||||
<option value="Ethiopian">Ethiopian</option>
|
||||
<option value="Djiboutian">Djiboutian</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="label">Passenger Type (Optional)</label>
|
||||
<select
|
||||
value={fareForm.passengerCategory}
|
||||
onChange={(e) => setFareForm({ ...fareForm, passengerCategory: e.target.value })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="">All Passenger Types</option>
|
||||
<option value="ADULT">Adult (5+ years)</option>
|
||||
<option value="CHILD">Child (Less than 5 years)</option>
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground mt-1">Scope pricing to specific passenger type</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Nationality (Optional)</label>
|
||||
<select
|
||||
value={fareForm.nationality}
|
||||
onChange={(e) => setFareForm({ ...fareForm, nationality: e.target.value })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="">All Nationalities</option>
|
||||
<option value="Ethiopian">Ethiopian</option>
|
||||
<option value="Djiboutian">Djiboutian</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -846,28 +895,44 @@ export default function PricingPage() {
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
step="1"
|
||||
value={segmentForm.baseFare}
|
||||
onChange={(e) => setSegmentForm({ ...segmentForm, baseFare: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="e.g., 150.00"
|
||||
placeholder="e.g., 150"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Nationality (Optional)</label>
|
||||
<select
|
||||
value={segmentForm.nationality}
|
||||
onChange={(e) => setSegmentForm({ ...segmentForm, nationality: e.target.value })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="">All Nationalities</option>
|
||||
<option value="Ethiopian">Ethiopian</option>
|
||||
<option value="Djiboutian">Djiboutian</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="label">Passenger Type (Optional)</label>
|
||||
<select
|
||||
value={segmentForm.passengerCategory}
|
||||
onChange={(e) => setSegmentForm({ ...segmentForm, passengerCategory: e.target.value })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="">All Passenger Types</option>
|
||||
<option value="ADULT">Adult (5+ years)</option>
|
||||
<option value="CHILD">Child (Less than 5 years)</option>
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground mt-1">Scope pricing to specific passenger type</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Nationality (Optional)</label>
|
||||
<select
|
||||
value={segmentForm.nationality}
|
||||
onChange={(e) => setSegmentForm({ ...segmentForm, nationality: e.target.value })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="">All Nationalities</option>
|
||||
<option value="Ethiopian">Ethiopian</option>
|
||||
<option value="Djiboutian">Djiboutian</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -900,7 +965,8 @@ export default function PricingPage() {
|
||||
<ul className="space-y-1">
|
||||
<li>• <strong>Schedule:</strong> Apply to specific schedule only</li>
|
||||
<li>• <strong>Route Code:</strong> Apply to all schedules on that route</li>
|
||||
<li>• <strong>Nationality:</strong> Override for specific passenger nationalities</li>
|
||||
<li>• <strong>Passenger Type:</strong> ADULT or CHILD pricing</li>
|
||||
<li>• <strong>Nationality:</strong> Override for specific nationalities</li>
|
||||
<li>• <strong>All empty:</strong> Apply globally to all schedules</li>
|
||||
</ul>
|
||||
)}
|
||||
@@ -908,6 +974,7 @@ export default function PricingPage() {
|
||||
<ul className="space-y-1">
|
||||
<li>• <strong>Segments:</strong> Define pricing for specific stop-to-stop segments</li>
|
||||
<li>• <strong>Stops:</strong> Use sequence numbers from the route</li>
|
||||
<li>• <strong>Passenger Type:</strong> ADULT or CHILD pricing</li>
|
||||
<li>• <strong>Nationality:</strong> Optional scope to specific nationalities</li>
|
||||
</ul>
|
||||
)}
|
||||
|
||||
@@ -220,28 +220,15 @@ export default function RoutesPage() {
|
||||
setOriginStationId(routeStops[0].stationId);
|
||||
setDestinationStationId(routeStops[routeStops.length - 1].stationId);
|
||||
|
||||
// Calculate cumulative distance for destination
|
||||
let cumulativeDistance = 0;
|
||||
routeStops.forEach((stop: any, idx: number) => {
|
||||
if (idx > 0) {
|
||||
cumulativeDistance += stop.distanceKm || 0;
|
||||
}
|
||||
});
|
||||
setDestinationDistance(cumulativeDistance);
|
||||
|
||||
// Calculate distance from origin for middle stops
|
||||
const middleStops = routeStops.slice(1, -1).map((stop: any, idx: number) => {
|
||||
let distFromOrigin = 0;
|
||||
for (let i = 1; i <= idx + 1; i++) {
|
||||
distFromOrigin += routeStops[i].distanceKm || 0;
|
||||
}
|
||||
return {
|
||||
stationId: stop.stationId,
|
||||
sequence: stop.sequence,
|
||||
distanceKm: stop.distanceKm,
|
||||
distanceFromOrigin: distFromOrigin,
|
||||
};
|
||||
});
|
||||
// Last stop's distanceKm is already cumulative from origin
|
||||
setDestinationDistance(routeStops[routeStops.length - 1].distanceKm || 0);
|
||||
|
||||
const middleStops = routeStops.slice(1, -1).map((stop: any) => ({
|
||||
stationId: stop.stationId,
|
||||
sequence: stop.sequence,
|
||||
distanceKm: stop.distanceKm,
|
||||
distanceFromOrigin: stop.distanceKm || 0,
|
||||
}));
|
||||
setStops(middleStops);
|
||||
}
|
||||
setShowModal(true);
|
||||
|
||||
@@ -14,6 +14,11 @@ export default function SeatsPage() {
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||
const [selectedSeat, setSelectedSeat] = useState<any>(null);
|
||||
const [blockReason, setBlockReason] = useState('');
|
||||
const [showBlockCoachModal, setShowBlockCoachModal] = useState(false);
|
||||
const [selectedCoach, setSelectedCoach] = useState<any>(null);
|
||||
const [blockCoachReason, setBlockCoachReason] = useState('');
|
||||
const [showUnblockCoachModal, setShowUnblockCoachModal] = useState(false);
|
||||
const [coachToUnblock, setCoachToUnblock] = useState<any>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: schedulesData } = useQuery({
|
||||
@@ -68,6 +73,33 @@ export default function SeatsPage() {
|
||||
const schedules = schedulesData?.items || schedulesData?.data || [];
|
||||
const coaches = seatMapData?.coaches || [];
|
||||
|
||||
const blockCoachMutation = useMutation({
|
||||
mutationFn: async ({ coachId, reason }: any) => {
|
||||
const coachSeats = coaches.find(c => c.id === coachId)?.seats || [];
|
||||
const seatIds = coachSeats.map((s: any) => s.id).filter((id: any) => id);
|
||||
return Promise.all(seatIds.map((seatId: string) => seatsApi.block(seatId, { reason })));
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
|
||||
setShowBlockCoachModal(false);
|
||||
setSelectedCoach(null);
|
||||
setBlockCoachReason('');
|
||||
},
|
||||
});
|
||||
|
||||
const unblockCoachMutation = useMutation({
|
||||
mutationFn: async ({ coachId }: any) => {
|
||||
const coachSeats = coaches.find(c => c.id === coachId)?.seats || [];
|
||||
const seatIds = coachSeats.map((s: any) => s.id).filter((id: any) => id);
|
||||
return Promise.all(seatIds.map((seatId: string) => seatsApi.unblock(seatId)));
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['seatmap'] });
|
||||
setShowUnblockCoachModal(false);
|
||||
setCoachToUnblock(null);
|
||||
},
|
||||
});
|
||||
|
||||
const toggleCoach = (coachId: string) => {
|
||||
const newExpanded = new Set(expandedCoaches);
|
||||
if (newExpanded.has(coachId)) {
|
||||
@@ -100,6 +132,37 @@ export default function SeatsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlockCoach = (coach: any) => {
|
||||
setSelectedCoach(coach);
|
||||
setShowBlockCoachModal(true);
|
||||
};
|
||||
|
||||
const handleUnblockCoach = (coach: any) => {
|
||||
const isBlocked = coach.seats?.some((s: any) => s.status === 'BLOCKED' || s.isBlocked);
|
||||
if (isBlocked) {
|
||||
setCoachToUnblock(coach);
|
||||
setShowUnblockCoachModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmUnblockCoach = async () => {
|
||||
if (coachToUnblock) {
|
||||
await unblockCoachMutation.mutateAsync({ coachId: coachToUnblock.id });
|
||||
}
|
||||
};
|
||||
|
||||
const isCoachBlocked = (coach: any) => {
|
||||
return coach.seats?.some((s: any) => s.status === 'BLOCKED' || s.isBlocked);
|
||||
};
|
||||
|
||||
const submitBlockCoach = async () => {
|
||||
if (!blockCoachReason.trim()) {
|
||||
alert('Please provide a reason for blocking');
|
||||
return;
|
||||
}
|
||||
await blockCoachMutation.mutateAsync({ coachId: selectedCoach.id, reason: blockCoachReason });
|
||||
};
|
||||
|
||||
const submitBlock = async () => {
|
||||
if (!blockReason.trim()) {
|
||||
alert('Please provide a reason for blocking');
|
||||
@@ -346,10 +409,12 @@ export default function SeatsPage() {
|
||||
);
|
||||
};
|
||||
|
||||
const coachesWithSeats = coaches.filter((coach: any) => {
|
||||
const seats = (coach.seats || []).filter((s: any) => s.seatNumber);
|
||||
return seats.length > 0;
|
||||
});
|
||||
const coachesWithSeats = coaches
|
||||
.filter((coach: any) => {
|
||||
const seats = (coach.seats || []).filter((s: any) => s.seatNumber);
|
||||
return seats.length > 0;
|
||||
})
|
||||
.sort((a: any, b: any) => (a.sequence || 0) - (b.sequence || 0));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -394,9 +459,7 @@ export default function SeatsPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left Column: Schedule Selector & Legends */}
|
||||
<div className="card h-fit sticky top-6 space-y-6">
|
||||
{/* Schedule Selector */}
|
||||
<div>
|
||||
<label className="label">Select Schedule</label>
|
||||
<select
|
||||
@@ -418,7 +481,6 @@ export default function SeatsPage() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Seat Legends - Vertical */}
|
||||
<div className="space-y-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<h3 className="font-semibold text-sm text-foreground">Seat Status</h3>
|
||||
<div className="space-y-2">
|
||||
@@ -446,14 +508,11 @@ export default function SeatsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Coaches with Locomotive - Single Column */}
|
||||
<div className="space-y-4 w-80">
|
||||
{/* Locomotive Icon Card */}
|
||||
<div className="bg-gradient-to-r from-[rgb(20,113,76)] to-[rgb(15,85,57)] rounded-lg border-2 border-[rgb(20,113,76)] flex items-center justify-center shadow-lg p-6 h-24">
|
||||
<Train className="w-14 h-14 text-white" />
|
||||
</div>
|
||||
|
||||
{/* Coaches List - Single Column */}
|
||||
{coachesWithSeats.map((coach: any, index: number) => {
|
||||
const coachData = coachTypesData?.items?.find((c: any) => c.id === coach.id) || coach;
|
||||
const coachTypeName = coachData?.coachType?.type || 'Coach';
|
||||
@@ -464,12 +523,11 @@ export default function SeatsPage() {
|
||||
|
||||
return (
|
||||
<div key={coach.id} className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800/50 shadow-md hover:shadow-lg transition-shadow">
|
||||
{/* Coach Header */}
|
||||
<button
|
||||
onClick={() => toggleCoach(coach.id)}
|
||||
className="w-full px-4 py-3 flex items-center justify-between bg-gradient-to-r from-[rgb(20,113,76)]/10 to-[rgb(20,113,76)]/5 dark:from-[rgb(20,113,76)]/20 dark:to-[rgb(20,113,76)]/10 hover:from-[rgb(20,113,76)]/20 hover:to-[rgb(20,113,76)]/15 dark:hover:from-[rgb(20,113,76)]/30 dark:hover:to-[rgb(20,113,76)]/20 transition-all border-b border-[rgb(20,113,76)]/20 dark:border-[rgb(20,113,76)]/30"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="px-4 py-3 bg-gradient-to-r from-[rgb(20,113,76)]/10 to-[rgb(20,113,76)]/5 dark:from-[rgb(20,113,76)]/20 dark:to-[rgb(20,113,76)]/10 border-b border-[rgb(20,113,76)]/20 dark:border-[rgb(20,113,76)]/30 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => toggleCoach(coach.id)}
|
||||
className="flex-1 flex items-center gap-3 hover:opacity-75 transition-opacity"
|
||||
>
|
||||
<div className={`transform transition-transform ${isExpanded ? 'rotate-180' : ''}`}>
|
||||
<ChevronDown className="w-5 h-5 text-[rgb(20,113,76)]" />
|
||||
</div>
|
||||
@@ -477,10 +535,17 @@ export default function SeatsPage() {
|
||||
<p className="font-semibold text-foreground">Coach {coach.coachNumber}</p>
|
||||
<p className="text-xs text-muted-foreground">{coachTypeName} • {seats.length} {seatOrBedLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
<ActionButton
|
||||
variant={isCoachBlocked(coach) ? 'danger' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => isCoachBlocked(coach) ? handleUnblockCoach(coach) : handleBlockCoach(coach)}
|
||||
className="ml-2"
|
||||
>
|
||||
{isCoachBlocked(coach) ? 'Unblock' : 'Block'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
{/* Coach Content - Seat Map */}
|
||||
{isExpanded && (
|
||||
<div className="px-4 py-4 bg-white dark:bg-gray-900/50 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-3 inline-block">
|
||||
@@ -580,6 +645,96 @@ export default function SeatsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={showBlockCoachModal}
|
||||
onClose={() => {
|
||||
setShowBlockCoachModal(false);
|
||||
setSelectedCoach(null);
|
||||
setBlockCoachReason('');
|
||||
}}
|
||||
title="Block Coach"
|
||||
size="md"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Block all seats in Coach <strong>{selectedCoach?.coachNumber}</strong>
|
||||
</p>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<p className="text-sm text-red-800">
|
||||
This will block all {selectedCoach?.seats?.length || 0} seats in this coach.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Reason for Blocking *</label>
|
||||
<textarea
|
||||
className="input"
|
||||
rows={3}
|
||||
value={blockCoachReason}
|
||||
onChange={(e) => setBlockCoachReason(e.target.value)}
|
||||
placeholder="e.g., Major maintenance, Safety inspection, Temporary withdrawal"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<ActionButton
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowBlockCoachModal(false);
|
||||
setSelectedCoach(null);
|
||||
setBlockCoachReason('');
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
variant="danger"
|
||||
onClick={submitBlockCoach}
|
||||
loading={blockCoachMutation.isPending}
|
||||
disabled={!blockCoachReason.trim()}
|
||||
>
|
||||
Block Coach
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={showUnblockCoachModal}
|
||||
onClose={() => {
|
||||
setShowUnblockCoachModal(false);
|
||||
setCoachToUnblock(null);
|
||||
}}
|
||||
title="Unblock Coach"
|
||||
size="md"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Unblock all seats in Coach <strong>{coachToUnblock?.coachNumber}</strong>
|
||||
</p>
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<p className="text-sm text-green-800">
|
||||
This will unblock all {coachToUnblock?.seats?.filter((s: any) => s.status === 'BLOCKED' || s.isBlocked).length || 0} blocked seats in this coach.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<ActionButton
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowUnblockCoachModal(false);
|
||||
setCoachToUnblock(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={confirmUnblockCoach}
|
||||
loading={unblockCoachMutation.isPending}
|
||||
>
|
||||
Unblock Coach
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { MapPin, Globe, Plus, Edit, Trash2 } from 'lucide-react';
|
||||
import { Globe, Plus, Edit, Trash2 } from 'lucide-react';
|
||||
import DataTable from '@/components/ui/DataTable';
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import ActionButton from '@/components/ui/ActionButton';
|
||||
@@ -11,6 +11,15 @@ import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import { stationsApi } from '@/lib/api';
|
||||
import { Station } from '@/types';
|
||||
|
||||
const TIMEZONES = [
|
||||
'Africa/Addis_Ababa',
|
||||
'Africa/Johannesburg',
|
||||
'Africa/Cairo',
|
||||
'Africa/Lagos',
|
||||
'Asia/Kolkata',
|
||||
'UTC',
|
||||
];
|
||||
|
||||
export default function StationsPage() {
|
||||
const [filters, setFilters] = useState({ search: '', country: '', operational: '' });
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@@ -51,6 +60,13 @@ export default function StationsPage() {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const sequence = parseInt(formData.get('sequence') as string);
|
||||
|
||||
if (isNaN(sequence)) {
|
||||
alert('Sequence Number is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const stationData = {
|
||||
code: formData.get('code') as string,
|
||||
name: formData.get('name') as string,
|
||||
@@ -59,6 +75,8 @@ export default function StationsPage() {
|
||||
lat: parseFloat(formData.get('lat') as string) || null,
|
||||
lng: parseFloat(formData.get('lng') as string) || null,
|
||||
timezone: formData.get('timezone') as string,
|
||||
distance: parseFloat(formData.get('distance') as string) || 0,
|
||||
sequence,
|
||||
isOperational: formData.get('isOperational') === 'true',
|
||||
};
|
||||
|
||||
@@ -81,6 +99,14 @@ export default function StationsPage() {
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'sequence',
|
||||
label: 'Sequence',
|
||||
sortable: true,
|
||||
render: (station: any) => (
|
||||
<span className="font-mono font-semibold text-sm">{station.sequence || 0}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'code',
|
||||
label: 'Code',
|
||||
@@ -111,24 +137,11 @@ export default function StationsPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'coordinates',
|
||||
label: 'Coordinates',
|
||||
render: (station: any) => {
|
||||
const lat = station.lat ? parseFloat(station.lat) : null;
|
||||
const lng = station.lng ? parseFloat(station.lng) : null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-mono">
|
||||
{lat && lng && !isNaN(lat) && !isNaN(lng)
|
||||
? `${lat.toFixed(4)}, ${lng.toFixed(4)}`
|
||||
: 'N/A'
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
key: 'distance',
|
||||
label: 'Distance (km)',
|
||||
render: (station: any) => (
|
||||
<span className="font-mono text-sm">{station.distance ? `${station.distance}` : '0'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'isOperational',
|
||||
@@ -139,13 +152,6 @@ export default function StationsPage() {
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'timezone',
|
||||
label: 'Timezone',
|
||||
render: (station: any) => (
|
||||
<span className="text-sm">{station.timezone || 'N/A'}</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const actions = [
|
||||
@@ -328,15 +334,43 @@ export default function StationsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Timezone</label>
|
||||
<input
|
||||
type="text"
|
||||
<label className="label">Timezone *</label>
|
||||
<select
|
||||
name="timezone"
|
||||
className="input"
|
||||
defaultValue={editingStation?.timezone || 'Africa/Addis_Ababa'}
|
||||
placeholder="e.g., Africa/Addis_Ababa"
|
||||
required
|
||||
>
|
||||
{TIMEZONES.map((tz) => (
|
||||
<option key={tz} value={tz}>{tz}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Distance from Previous (km)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="distance"
|
||||
className="input"
|
||||
defaultValue={editingStation?.distance || 0}
|
||||
min="0"
|
||||
step="0.1"
|
||||
placeholder="e.g., 150.5"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Sequence Number *</label>
|
||||
<input
|
||||
type="number"
|
||||
name="sequence"
|
||||
className="input"
|
||||
defaultValue={editingStation?.sequence || 0}
|
||||
min="0"
|
||||
required
|
||||
placeholder="e.g., 1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">Used for ordering stations in routes</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Status</label>
|
||||
<select
|
||||
|
||||
@@ -2,19 +2,35 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Download, RefreshCw, CheckCircle, Trash2 } from 'lucide-react';
|
||||
import { LogIn, Trash2 } from 'lucide-react';
|
||||
import { Download } 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 { ticketsApi, apiClient } from '@/lib/api';
|
||||
import { formatDateTime } from '@/lib/utils';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import { ticketsApi, apiClient, schedulesApi, stationsApi } from '@/lib/api';
|
||||
import { formatDateTime, formatCurrency } from '@/lib/utils';
|
||||
|
||||
export default function TicketsPage() {
|
||||
const [filters, setFilters] = useState({ search: '', status: '' });
|
||||
const [filters, setFilters] = useState({ search: '', status: '', originStationId: '', destinationStationId: '', tripDate: '' });
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [ticketToDelete, setTicketToDelete] = useState<any>(null);
|
||||
const [boardConfirmOpen, setBoardConfirmOpen] = useState(false);
|
||||
const [ticketToBoard, setTicketToBoard] = useState<any>(null);
|
||||
const [successMessage, setSuccessMessage] = useState('');
|
||||
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
|
||||
const [selectedTicket, setSelectedTicket] = useState<any>(null);
|
||||
const [exportModalOpen, setExportModalOpen] = useState(false);
|
||||
const [selectedColumns, setSelectedColumns] = useState<Record<string, boolean>>({
|
||||
ticketNumber: true,
|
||||
booking: true,
|
||||
trip: true,
|
||||
seat: true,
|
||||
seatClass: true,
|
||||
amount: true,
|
||||
status: true,
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
@@ -22,27 +38,22 @@ export default function TicketsPage() {
|
||||
queryFn: () => ticketsApi.getAll({ ...filters, skip: 0, take: 50 }),
|
||||
});
|
||||
|
||||
const regenerateMutation = useMutation({
|
||||
mutationFn: ticketsApi.regenerate,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tickets'] });
|
||||
setSuccessMessage('Ticket regenerated successfully');
|
||||
setTimeout(() => setSuccessMessage(''), 3000);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert(`Error: ${error.message || 'Failed to regenerate ticket'}`);
|
||||
},
|
||||
const { data: stationsData } = useQuery({
|
||||
queryKey: ['stations'],
|
||||
queryFn: () => stationsApi.getAll(),
|
||||
});
|
||||
|
||||
const validateMutation = useMutation({
|
||||
mutationFn: ({ ticketId, data }: any) => ticketsApi.validate(ticketId, data),
|
||||
const boardMutation = useMutation({
|
||||
mutationFn: ({ ticketId }: any) => ticketsApi.validate(ticketId, { status: 'USED', boardedAt: new Date().toISOString() }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tickets'] });
|
||||
setSuccessMessage('Ticket validated successfully');
|
||||
setBoardConfirmOpen(false);
|
||||
setTicketToBoard(null);
|
||||
setSuccessMessage('Ticket boarded successfully');
|
||||
setTimeout(() => setSuccessMessage(''), 3000);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert(`Error: ${error.message || 'Failed to validate ticket'}`);
|
||||
alert(`Error: ${error.message || 'Failed to board ticket'}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,17 +72,15 @@ export default function TicketsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleRegenerate = async (ticket: any) => {
|
||||
if (window.confirm(`Regenerate QR code for ticket ${ticket.ticketNumber}?`)) {
|
||||
await regenerateMutation.mutateAsync(ticket.id);
|
||||
}
|
||||
const handleBoard = (ticket: any) => {
|
||||
setTicketToBoard(ticket);
|
||||
setBoardConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleValidate = async (ticket: any) => {
|
||||
await validateMutation.mutateAsync({
|
||||
ticketId: ticket.id,
|
||||
data: { validatedAt: new Date().toISOString() },
|
||||
});
|
||||
const handleConfirmBoard = async () => {
|
||||
if (ticketToBoard) {
|
||||
await boardMutation.mutateAsync({ ticketId: ticketToBoard.id });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (ticket: any) => {
|
||||
@@ -85,6 +94,50 @@ export default function TicketsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportTickets = async () => {
|
||||
setExportModalOpen(true);
|
||||
};
|
||||
|
||||
const confirmExport = () => {
|
||||
const cols = Object.entries(selectedColumns)
|
||||
.filter(([, selected]) => selected)
|
||||
.map(([col]) => col);
|
||||
|
||||
if (cols.length === 0) {
|
||||
alert('Please select at least one column');
|
||||
return;
|
||||
}
|
||||
|
||||
const csv = [
|
||||
cols.join(','),
|
||||
...data?.items?.map((ticket: any) => {
|
||||
const values = cols.map(col => {
|
||||
switch(col) {
|
||||
case 'ticketNumber': return ticket.ticketNumber || '';
|
||||
case 'booking': return ticket.booking?.bookingRef || '';
|
||||
case 'trip': return `${ticket.schedule?.originStation?.name || ''}-${ticket.schedule?.destinationStation?.name || ''}`;
|
||||
case 'coach': return ticket.seat?.coach?.number || '';
|
||||
case 'seat': return ticket.seat?.seatNumber || '';
|
||||
case 'seatClass': return ticket.seat?.coach?.coachType?.name || '';
|
||||
case 'amount': return formatCurrency((ticket.booking?.totalMinor || 0), ticket.booking?.currency || 'ETB');
|
||||
case 'status': return ticket.status || '';
|
||||
case 'boarded': return ticket.boardedAt ? 'Yes' : 'No';
|
||||
default: return '';
|
||||
}
|
||||
});
|
||||
return values.map(v => `"${v}"`).join(',');
|
||||
}) || []
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `tickets-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
setExportModalOpen(false);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'ticketNumber',
|
||||
@@ -123,54 +176,60 @@ export default function TicketsPage() {
|
||||
{
|
||||
key: 'seat',
|
||||
label: 'Seat',
|
||||
sortable: true,
|
||||
render: (ticket: any) => (
|
||||
<span className="font-mono">{ticket.seat?.seatNumber || 'N/A'}</span>
|
||||
<div>
|
||||
<div className="font-mono font-semibold">Coach {ticket.seat?.coach?.number || 'N/A'} - Seat {ticket.seat?.seatNumber || 'N/A'}</div>
|
||||
<div className="text-xs text-muted-foreground">{ticket.seat?.coach?.coachType?.name || 'N/A'}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
label: 'Amount',
|
||||
sortable: true,
|
||||
render: (ticket: any) => formatCurrency(ticket.booking?.totalMinor || 0, ticket.booking?.currency || 'ETB'),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'Status',
|
||||
sortable: true,
|
||||
render: (ticket: any) => (
|
||||
<Badge variant="status" status={ticket.status || 'PENDING'}>
|
||||
{ticket.status || 'PENDING'}
|
||||
<Badge variant="status" status={ticket.status || 'ACTIVE'}>
|
||||
{ticket.status || 'ACTIVE'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'validated',
|
||||
label: 'Validated',
|
||||
key: 'boarded',
|
||||
label: 'Boarded',
|
||||
render: (ticket: any) => (
|
||||
ticket.validatedAt ? (
|
||||
ticket.boardedAt ? (
|
||||
<div className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<span className="text-sm">{formatDateTime(ticket.validatedAt)}</span>
|
||||
<span className="text-sm">{formatDateTime(ticket.boardedAt)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">Not validated</span>
|
||||
<span className="text-sm text-muted-foreground">Not boarded</span>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: 'Created',
|
||||
sortable: true,
|
||||
render: (ticket: any) => formatDateTime(ticket.createdAt),
|
||||
},
|
||||
];
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Check-in',
|
||||
onClick: handleValidate,
|
||||
label: 'Board',
|
||||
onClick: handleBoard,
|
||||
variant: 'primary' as const,
|
||||
icon: CheckCircle,
|
||||
show: (ticket: any) => !ticket.validatedAt,
|
||||
icon: LogIn,
|
||||
show: (ticket: any) => ticket.status !== 'USED' && !ticket.boardedAt,
|
||||
},
|
||||
{
|
||||
label: 'Regenerate',
|
||||
onClick: handleRegenerate,
|
||||
label: 'Details',
|
||||
onClick: (ticket: any) => {
|
||||
setSelectedTicket(ticket);
|
||||
setDetailsModalOpen(true);
|
||||
},
|
||||
variant: 'secondary' as const,
|
||||
icon: RefreshCw,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
@@ -180,6 +239,8 @@ export default function TicketsPage() {
|
||||
},
|
||||
];
|
||||
|
||||
const stations = stationsData?.items || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -187,7 +248,7 @@ export default function TicketsPage() {
|
||||
<h1 className="text-2xl font-bold text-foreground">Tickets</h1>
|
||||
<p className="text-muted-foreground">Manage tickets and validations</p>
|
||||
</div>
|
||||
<ActionButton icon={Download} variant="secondary">Export</ActionButton>
|
||||
<ActionButton icon={Download} variant="secondary" onClick={handleExportTickets}>Export</ActionButton>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
@@ -202,17 +263,52 @@ export default function TicketsPage() {
|
||||
Error loading tickets: {error instanceof Error ? error.message : 'Unknown error'}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<div>
|
||||
<label className="label">Search</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by ticket number or booking ref..."
|
||||
placeholder="Search by ticket number..."
|
||||
className="input"
|
||||
value={filters.search}
|
||||
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Origin</label>
|
||||
<select
|
||||
className="input"
|
||||
value={filters.originStationId}
|
||||
onChange={(e) => setFilters({ ...filters, originStationId: e.target.value })}
|
||||
>
|
||||
<option value="">All Origins</option>
|
||||
{stations.map((station: any) => (
|
||||
<option key={station.id} value={station.id}>{station.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Destination</label>
|
||||
<select
|
||||
className="input"
|
||||
value={filters.destinationStationId}
|
||||
onChange={(e) => setFilters({ ...filters, destinationStationId: e.target.value })}
|
||||
>
|
||||
<option value="">All Destinations</option>
|
||||
{stations.map((station: any) => (
|
||||
<option key={station.id} value={station.id}>{station.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Trip Date</label>
|
||||
<input
|
||||
type="date"
|
||||
className="input"
|
||||
value={filters.tripDate}
|
||||
onChange={(e) => setFilters({ ...filters, tripDate: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Status</label>
|
||||
<select
|
||||
@@ -224,7 +320,6 @@ export default function TicketsPage() {
|
||||
<option value="ACTIVE">Active</option>
|
||||
<option value="USED">Used</option>
|
||||
<option value="CANCELLED">Cancelled</option>
|
||||
<option value="EXPIRED">Expired</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,6 +334,21 @@ export default function TicketsPage() {
|
||||
emptyMessage="No tickets found"
|
||||
/>
|
||||
|
||||
{/* Board Confirmation Dialog */}
|
||||
<ConfirmDialog
|
||||
isOpen={boardConfirmOpen}
|
||||
onClose={() => {
|
||||
setBoardConfirmOpen(false);
|
||||
setTicketToBoard(null);
|
||||
}}
|
||||
onConfirm={handleConfirmBoard}
|
||||
title="Board Ticket"
|
||||
message={`Are you sure you want to board ticket ${ticketToBoard?.ticketNumber}? This will mark the ticket as USED.`}
|
||||
confirmText="Board"
|
||||
cancelText="Cancel"
|
||||
isLoading={boardMutation.isPending}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<ConfirmDialog
|
||||
isOpen={deleteConfirmOpen}
|
||||
@@ -254,6 +364,154 @@ export default function TicketsPage() {
|
||||
isLoading={deleteMutation.isPending}
|
||||
isDanger={true}
|
||||
/>
|
||||
|
||||
{/* Ticket Details Modal */}
|
||||
<Modal
|
||||
isOpen={detailsModalOpen}
|
||||
onClose={() => {
|
||||
setDetailsModalOpen(false);
|
||||
setSelectedTicket(null);
|
||||
}}
|
||||
title="Ticket Details"
|
||||
size="lg"
|
||||
>
|
||||
{selectedTicket && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Ticket Number</p>
|
||||
<p className="font-mono font-semibold text-lg">{selectedTicket.ticketNumber}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Status</p>
|
||||
<div className="mt-1">
|
||||
<Badge variant="status" status={selectedTicket.status || 'ACTIVE'}>
|
||||
{selectedTicket.status || 'ACTIVE'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="font-semibold mb-3">Booking Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Booking Reference</p>
|
||||
<p className="font-medium">{selectedTicket.booking?.bookingRef || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Passenger</p>
|
||||
<p className="font-medium">{selectedTicket.booking?.passenger?.fullName || selectedTicket.booking?.contactEmail || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Amount</p>
|
||||
<p className="font-medium">{formatCurrency(selectedTicket.booking?.totalMinor || 0, selectedTicket.booking?.currency || 'ETB')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="font-semibold mb-3">Trip Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Route</p>
|
||||
<p className="font-medium">
|
||||
{selectedTicket.schedule?.originStation?.name || 'N/A'} → {selectedTicket.schedule?.destinationStation?.name || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Departure</p>
|
||||
<p className="font-medium">{selectedTicket.schedule?.departureAt ? formatDateTime(selectedTicket.schedule.departureAt) : 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="font-semibold mb-3">Seat Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Coach</p>
|
||||
<p className="font-mono font-semibold">{selectedTicket.seat?.coach?.number || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Seat Number</p>
|
||||
<p className="font-mono font-semibold">{selectedTicket.seat?.seatNumber || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Class</p>
|
||||
<p className="font-medium">{selectedTicket.seat?.coach?.coachType?.name || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedTicket.boardedAt && (
|
||||
<div className="border-t pt-4 bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
|
||||
<p className="text-sm text-muted-foreground">Boarded At</p>
|
||||
<p className="font-medium text-green-700 dark:text-green-400">{formatDateTime(selectedTicket.boardedAt)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<ActionButton
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setDetailsModalOpen(false);
|
||||
setSelectedTicket(null);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Export Columns Modal */}
|
||||
<Modal
|
||||
isOpen={exportModalOpen}
|
||||
onClose={() => setExportModalOpen(false)}
|
||||
title="Export Tickets - Select Columns"
|
||||
size="md"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">Select which columns to include in the export</p>
|
||||
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{[
|
||||
{ key: 'ticketNumber', label: 'Ticket Number' },
|
||||
{ key: 'booking', label: 'Booking Reference & Passenger' },
|
||||
{ key: 'trip', label: 'Trip (Origin → Destination)' },
|
||||
{ key: 'coach', label: 'Coach Number' },
|
||||
{ key: 'seat', label: 'Seat Number' },
|
||||
{ key: 'seatClass', label: 'Seat Class' },
|
||||
{ key: 'amount', label: 'Amount' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'boarded', label: 'Boarded Status' },
|
||||
].map((col) => (
|
||||
<label key={col.key} className="flex items-center gap-3 p-2 hover:bg-gray-50 dark:hover:bg-gray-900/50 rounded cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedColumns[col.key] || false}
|
||||
onChange={(e) =>
|
||||
setSelectedColumns({ ...selectedColumns, [col.key]: e.target.checked })
|
||||
}
|
||||
className="w-4 h-4 rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm font-medium">{col.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<ActionButton variant="secondary" onClick={() => setExportModalOpen(false)}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
<ActionButton onClick={confirmExport}>
|
||||
Export CSV
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Bell, LogOut, Moon, Sun, ChevronDown } from 'lucide-react';
|
||||
import { Bell, LogOut, Moon, Sun, ChevronDown, HelpCircle } from 'lucide-react';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { useTheme } from '@/lib/theme-store';
|
||||
import { useState } from 'react';
|
||||
@@ -19,6 +19,16 @@ export default function Header() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Help Documentation */}
|
||||
<Link
|
||||
href="/docs"
|
||||
className="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors"
|
||||
title="View Documentation"
|
||||
target="_blank"
|
||||
>
|
||||
<HelpCircle className="h-5 w-5 text-[rgb(20,113,76)] dark:text-slate-400" />
|
||||
</Link>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="relative">
|
||||
<button
|
||||
|
||||
@@ -32,7 +32,8 @@ import {
|
||||
Moon,
|
||||
Sun,
|
||||
Armchair,
|
||||
Grid3x3
|
||||
Grid3x3,
|
||||
Banknote
|
||||
} from 'lucide-react';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -69,6 +70,7 @@ const navigationSections = [
|
||||
title: 'Financial',
|
||||
items: [
|
||||
{ name: 'Pricing & Fares', href: '/pricing', icon: DollarSign },
|
||||
{ name: 'Currencies', href: '/currencies', icon: Banknote },
|
||||
{ name: 'Payments', href: '/payments', icon: CreditCard },
|
||||
{ name: 'Promo Codes', href: '/promos', icon: Gift },
|
||||
]
|
||||
|
||||
@@ -10,6 +10,7 @@ interface Column<T> {
|
||||
key: string;
|
||||
label: string;
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
render?: (item: T) => ReactNode;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user