mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 00:08:18 +00:00
207 lines
9.5 KiB
TypeScript
207 lines
9.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Download, Eye, Wallet, Trash2 } from 'lucide-react';
|
|
import DataTable from '@/components/ui/DataTable';
|
|
import Badge from '@/components/ui/Badge';
|
|
import ActionButton from '@/components/ui/ActionButton';
|
|
import Modal from '@/components/ui/Modal';
|
|
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
|
import { walletApi } from '@/lib/api';
|
|
import { formatDateTime, formatCurrency } from '@/lib/utils';
|
|
|
|
const Field = ({ label, value, mono = false, truncate = false }: { label: string; value: string; mono?: boolean; truncate?: boolean }) => (
|
|
<div className="bg-muted/40 rounded-lg p-3">
|
|
<p className="text-xs text-muted-foreground mb-1">{label}</p>
|
|
<p className={`text-sm font-semibold text-foreground${mono ? ' font-mono' : ''}${truncate ? ' truncate' : ''}`} title={value}>{value || '—'}</p>
|
|
</div>
|
|
);
|
|
|
|
const SectionHeader = ({ title }: { title: string }) => (
|
|
<h3 className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-3 flex items-center gap-2">
|
|
<span className="w-4 h-px bg-muted-foreground/40 inline-block" />{title}
|
|
</h3>
|
|
);
|
|
|
|
export default function WalletAccountsPage() {
|
|
const [filters, setFilters] = useState({ search: '' });
|
|
const [selected, setSelected] = useState<any>(null);
|
|
const [toDelete, setToDelete] = useState<any>(null);
|
|
const [deleteError, setDeleteError] = useState('');
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['wallet-accounts', filters],
|
|
queryFn: () => walletApi.getAccounts(filters),
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => walletApi.delete(id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['wallet-accounts'] });
|
|
setToDelete(null);
|
|
setDeleteError('');
|
|
},
|
|
onError: (err: any) => setDeleteError(err?.message || 'Failed to delete wallet account'),
|
|
});
|
|
|
|
const columns = [
|
|
{ key: 'passenger', label: 'Passenger', render: (w: any) => (
|
|
<div>
|
|
<div className="font-medium">{w.passenger?.fullName || 'N/A'}</div>
|
|
<div className="text-xs text-muted-foreground">{w.passenger?.email || ''}</div>
|
|
</div>
|
|
)},
|
|
{ key: 'balanceMinor', label: 'Balance', render: (w: any) => (
|
|
<span className="font-semibold">{formatCurrency(w.balanceMinor ?? 0, w.currency || 'ETB')}</span>
|
|
)},
|
|
{ key: 'currency', label: 'Currency', render: (w: any) => w.currency || 'ETB' },
|
|
{ key: 'status', label: 'Status', render: (w: any) => (
|
|
<Badge variant="status" status={w.isActive !== false ? 'CONFIRMED' : 'CANCELLED'}>
|
|
{w.isActive !== false ? 'Active' : 'Inactive'}
|
|
</Badge>
|
|
)},
|
|
];
|
|
|
|
const actions = [
|
|
{ label: 'View Details', onClick: (w: any) => setSelected(w), variant: 'secondary' as const, icon: Eye },
|
|
{ label: 'Delete', onClick: (w: any) => { setDeleteError(''); setToDelete(w); }, variant: 'danger' as const, icon: Trash2 },
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-foreground">Wallet Accounts</h1>
|
|
<p className="text-muted-foreground">Manage passenger wallet accounts</p>
|
|
</div>
|
|
<ActionButton icon={Download} variant="secondary">Export</ActionButton>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="label">Search</label>
|
|
<input type="text" placeholder="Search by name or email..." className="input" value={filters.search} onChange={(e) => setFilters({ ...filters, search: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
data={Array.isArray(data) ? data : (data?.items || [])}
|
|
columns={columns}
|
|
actions={actions}
|
|
loading={isLoading}
|
|
emptyMessage="No wallet accounts found"
|
|
/>
|
|
|
|
{/* Delete Confirm Dialog */}
|
|
<ConfirmDialog
|
|
isOpen={!!toDelete}
|
|
onClose={() => { setToDelete(null); setDeleteError(''); }}
|
|
onConfirm={() => deleteMutation.mutate(toDelete.id)}
|
|
title="Delete Wallet Account"
|
|
message={`Are you sure you want to delete the wallet account for ${toDelete?.passenger?.fullName || 'this passenger'}? This will permanently remove the account and all ledger entries.`}
|
|
confirmText="Delete"
|
|
isDanger
|
|
isLoading={deleteMutation.isPending}
|
|
error={deleteError}
|
|
/>
|
|
|
|
{/* Wallet Details Modal */}
|
|
<Modal isOpen={!!selected} onClose={() => setSelected(null)} title="Wallet Account Details" size="xl">
|
|
{selected && (() => {
|
|
const w = selected;
|
|
const balance = w.balanceMinor ?? 0;
|
|
const passengerName = w.passenger?.fullName || 'N/A';
|
|
return (
|
|
<div>
|
|
<div className="from-blue-600 to-blue-700 -mx-6 -mt-4 mb-6 px-6 py-5 bg-gradient-to-r rounded-t-lg">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center shrink-0">
|
|
<Wallet className="w-7 h-7 text-white" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-white text-xl font-bold truncate">{passengerName}</p>
|
|
<p className="text-blue-200 text-sm">{w.passenger?.email || ''}</p>
|
|
</div>
|
|
<div className="text-right shrink-0">
|
|
<Badge variant="status" status={w.isActive !== false ? 'CONFIRMED' : 'CANCELLED'}>
|
|
{w.isActive !== false ? 'Active' : 'Inactive'}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 grid grid-cols-3 gap-3">
|
|
{[
|
|
{ label: 'Current Balance', value: formatCurrency(balance, w.currency || 'ETB') },
|
|
{ label: 'Currency', value: w.currency || 'ETB' },
|
|
{ label: 'Total Topped Up', value: formatCurrency(w.totalTopUp ?? 0, w.currency || 'ETB') },
|
|
].map(({ label, value }) => (
|
|
<div key={label} className="bg-white/10 rounded-lg px-3 py-2">
|
|
<p className="text-blue-200 text-xs">{label}</p>
|
|
<p className="text-white text-sm font-bold truncate">{value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<section>
|
|
<SectionHeader title="Balance" />
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-lg p-3 col-span-2">
|
|
<p className="text-xs text-blue-700 dark:text-blue-400 mb-1">Current Balance</p>
|
|
<p className="text-xl font-bold text-blue-800 dark:text-blue-300">{formatCurrency(balance, w.currency || 'ETB')}</p>
|
|
</div>
|
|
<Field label="Total Topped Up" value={formatCurrency(w.totalTopUp ?? 0, w.currency || 'ETB')} />
|
|
<Field label="Total Spent" value={formatCurrency(w.totalSpent ?? 0, w.currency || 'ETB')} />
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<SectionHeader title="Account Details" />
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<Field label="Currency" value={w.currency || 'ETB'} />
|
|
<div className="bg-muted/40 rounded-lg p-3">
|
|
<p className="text-xs text-muted-foreground mb-2">Status</p>
|
|
<Badge variant="status" status={w.isActive !== false ? 'CONFIRMED' : 'CANCELLED'}>
|
|
{w.isActive !== false ? 'Active' : 'Inactive'}
|
|
</Badge>
|
|
</div>
|
|
<Field label="Locked" value={w.isLocked ? 'Yes' : 'No'} />
|
|
<Field label="Lock Reason" value={w.lockReason || 'N/A'} truncate />
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<SectionHeader title="Passenger" />
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
<Field label="Full Name" value={w.passenger?.fullName} />
|
|
<Field label="Email" value={w.passenger?.email} truncate />
|
|
<Field label="Phone" value={w.passenger?.phone} />
|
|
<Field label="Passenger ID" value={w.passengerId || w.passenger?.id} mono truncate />
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<SectionHeader title="Timestamps & IDs" />
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
<Field label="Created" value={formatDateTime(w.createdAt)} />
|
|
<Field label="Last Updated" value={formatDateTime(w.updatedAt)} />
|
|
<Field label="Account ID" value={w.id} mono truncate />
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-6 mt-2 border-t border-muted">
|
|
<ActionButton variant="secondary" onClick={() => setSelected(null)}>Close</ActionButton>
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|