mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
Booking and pricing related updates
This commit is contained in:
@@ -2,14 +2,13 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Plus, Trash2, Loader2, Edit, RefreshCw } from 'lucide-react';
|
||||
import { Edit, Loader2, 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 {
|
||||
interface CurrencyRate {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
@@ -18,203 +17,104 @@ interface Currency {
|
||||
exchangeRate: number;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const CURRENCY_META: Record<string, { name: string; symbol: string }> = {
|
||||
ETB: { name: 'Ethiopian Birr', symbol: 'Br' },
|
||||
DJF: { name: 'Djiboutian Franc', symbol: 'Fdj' },
|
||||
USD: { name: 'US Dollar', symbol: '$' },
|
||||
};
|
||||
|
||||
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 [editingRate, setEditingRate] = useState<CurrencyRate | null>(null);
|
||||
const [rateInput, setRateInput] = useState('');
|
||||
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({
|
||||
const { data: currencies = [], isLoading } = useQuery<CurrencyRate[]>({
|
||||
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),
|
||||
mutationFn: ({ id, exchangeRate }: { id: string; exchangeRate: number }) =>
|
||||
apiClient.patch(`/currencies/${id}`, { exchangeRate }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['currencies'] });
|
||||
setEditingCurrency(null);
|
||||
resetForm();
|
||||
setEditingRate(null);
|
||||
setError(null);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setError(err.response?.data?.message || 'Failed to update currency');
|
||||
setError(err.response?.data?.message || 'Failed to update exchange rate');
|
||||
},
|
||||
});
|
||||
|
||||
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({
|
||||
const syncMutation = 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');
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['currencies'] }),
|
||||
onError: (err: any) => setError(err.response?.data?.message || 'Failed to sync rates'),
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
setCurrencyForm({
|
||||
code: '',
|
||||
name: '',
|
||||
symbol: '',
|
||||
baseCurrencyCode: 'ETB',
|
||||
exchangeRate: '',
|
||||
});
|
||||
setEditingCurrency(null);
|
||||
setShowModal(false);
|
||||
const handleEdit = (currency: CurrencyRate) => {
|
||||
setEditingRate(currency);
|
||||
setRateInput(currency.exchangeRate.toString());
|
||||
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);
|
||||
const handleSave = async () => {
|
||||
const rate = parseFloat(rateInput);
|
||||
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);
|
||||
}
|
||||
await updateMutation.mutateAsync({ id: editingRate!.id, exchangeRate: rate });
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (deleteConfirm.id) {
|
||||
await deleteMutation.mutateAsync(deleteConfirm.id);
|
||||
}
|
||||
};
|
||||
|
||||
const currenciesArray = Array.isArray(currencies) ? currencies : (currencies as any)?.items || [];
|
||||
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}
|
||||
label: 'Currency',
|
||||
render: (c: CurrencyRate) => (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl font-bold text-muted-foreground w-10 text-center">
|
||||
{CURRENCY_META[c.code]?.symbol ?? c.symbol}
|
||||
</span>
|
||||
<div>
|
||||
<div className="font-semibold">{c.code}</div>
|
||||
<div className="text-xs text-muted-foreground">{CURRENCY_META[c.code]?.name ?? c.name}</div>
|
||||
</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: 'baseCurrencyCode',
|
||||
label: 'Base',
|
||||
render: (c: CurrencyRate) => (
|
||||
<span className="font-mono text-sm text-muted-foreground">{c.baseCurrencyCode}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'updatedAt',
|
||||
key: 'exchangeRate',
|
||||
label: 'Exchange Rate',
|
||||
render: (c: CurrencyRate) => (
|
||||
<div>
|
||||
<div className="font-mono font-semibold">
|
||||
1 {c.baseCurrencyCode} = {c.exchangeRate} {c.code}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
1 {c.code} = {(1 / c.exchangeRate).toFixed(6)} {c.baseCurrencyCode}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: 'Last Updated',
|
||||
render: (currency: Currency) => (
|
||||
render: (c: CurrencyRate) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{new Date(currency.updatedAt).toLocaleDateString()}
|
||||
{new Date(c.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
@@ -222,140 +122,93 @@ export default function CurrenciesPage() {
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Edit',
|
||||
onClick: handleEditCurrency,
|
||||
label: 'Edit Rate',
|
||||
onClick: handleEdit,
|
||||
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>
|
||||
<h1 className="text-3xl font-bold text-foreground">Exchange Rates</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage ETB exchange rates for display currencies (DJF, USD)
|
||||
</p>
|
||||
</div>
|
||||
<ActionButton
|
||||
icon={RefreshCw}
|
||||
variant="secondary"
|
||||
onClick={() => syncMutation.mutate()}
|
||||
loading={syncMutation.isPending}
|
||||
>
|
||||
Sync Rates
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
{error && !editingRate && (
|
||||
<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="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 className="grid grid-cols-3 gap-4 mb-6">
|
||||
{(['ETB', 'DJF', 'USD'] as const).map((code) => {
|
||||
const entry = currenciesArray.find((c: CurrencyRate) => c.code === code);
|
||||
return (
|
||||
<div
|
||||
key={code}
|
||||
className="p-4 rounded-lg border bg-muted/30 flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground font-medium">{CURRENCY_META[code].name}</div>
|
||||
<div className="text-2xl font-bold mt-1">{code}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{entry ? (
|
||||
<>
|
||||
<div className="font-mono font-semibold text-lg">{entry.exchangeRate}</div>
|
||||
<div className="text-xs text-muted-foreground">per ETB</div>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">Not configured</span>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
data={currenciesArray}
|
||||
columns={columns}
|
||||
actions={actions}
|
||||
loading={false}
|
||||
emptyMessage="No exchange rates configured."
|
||||
/>
|
||||
)}
|
||||
</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 className="card bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 text-sm text-blue-800 dark:text-blue-300 space-y-1">
|
||||
<p className="font-semibold text-blue-900 dark:text-blue-200 mb-2">How it works</p>
|
||||
<p>• ETB is the transaction currency — all fares are stored in ETB minor units (1 ETB = 100 minor)</p>
|
||||
<p>• DJF and USD rates are used to display prices to passengers in their preferred currency</p>
|
||||
<p>• Rates apply globally; changes take effect immediately on the next booking or fare quote</p>
|
||||
</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"
|
||||
isOpen={!!editingRate}
|
||||
onClose={() => { setEditingRate(null); setError(null); }}
|
||||
title={`Update Rate — ${editingRate?.code}`}
|
||||
size="sm"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
@@ -364,108 +217,38 @@ export default function CurrenciesPage() {
|
||||
</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 className="p-3 bg-muted/40 rounded-lg text-sm">
|
||||
<span className="text-muted-foreground">Currency: </span>
|
||||
<span className="font-semibold">{editingRate?.code} — {CURRENCY_META[editingRate?.code ?? '']?.name}</span>
|
||||
</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}
|
||||
<label className="label">
|
||||
1 {editingRate?.baseCurrencyCode} = ? {editingRate?.code}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0.0001"
|
||||
step="0.0001"
|
||||
value={rateInput}
|
||||
onChange={(e) => setRateInput(e.target.value)}
|
||||
className="input w-full"
|
||||
placeholder="e.g., 3.25"
|
||||
autoFocus
|
||||
/>
|
||||
{rateInput && parseFloat(rateInput) > 0 && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
≈ 1 {editingRate?.code} = {(1 / parseFloat(rateInput)).toFixed(6)} {editingRate?.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"
|
||||
>
|
||||
<div className="flex gap-2 justify-end pt-2">
|
||||
<ActionButton variant="secondary" onClick={() => { setEditingRate(null); setError(null); }}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={handleSaveCurrency}
|
||||
loading={createMutation.isPending || updateMutation.isPending}
|
||||
>
|
||||
{editingCurrency ? 'Update Currency' : 'Add Currency'}
|
||||
<ActionButton onClick={handleSave} loading={updateMutation.isPending}>
|
||||
Save Rate
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -731,14 +731,19 @@ export default function PricingPage() {
|
||||
|
||||
<div>
|
||||
<label className="label">Route Code (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
<select
|
||||
value={fareForm.route}
|
||||
onChange={(e) => setFareForm({ ...fareForm, route: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="e.g., ADD-DJI"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">e.g., ADD-DJI for full route</p>
|
||||
>
|
||||
<option value="">All routes</option>
|
||||
{routesArray.map((route: Route) => (
|
||||
<option key={route.id} value={route.code}>
|
||||
{route.code} — {route.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground mt-1">Scope this fare to a specific route</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user