Round trip journeys, feedback items, backoffice documentation

This commit is contained in:
Stephanos A
2026-06-16 13:33:03 +03:00
parent 28e183b086
commit 75c8423f50
46 changed files with 7222 additions and 570 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);
}

View 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>
);
}

View 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;

View 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;

View File

@@ -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>

View File

@@ -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 (&lt;5) first child travels free, subsequent children pay full fare
</li>
<li>
<strong>Age-Based Pricing:</strong> ADULT (5+ years) pays 100%, CHILD (&lt;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>
)}

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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 },
]

View File

@@ -10,6 +10,7 @@ interface Column<T> {
key: string;
label: string;
sortable?: boolean;
filterable?: boolean;
render?: (item: T) => ReactNode;
width?: string;
}