Files
edr-platform/apps/edr-passenger-web/backoffice/src/app/wallet-accounts/page.tsx

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