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

391 lines
16 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Plus, Edit, Eye, Trash2 } from 'lucide-react';
import DataTable from '@/components/ui/DataTable';
import ActionButton from '@/components/ui/ActionButton';
import Badge from '@/components/ui/Badge';
import Modal from '@/components/ui/Modal';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import { agentsApi, apiClient } from '@/lib/api';
import { formatCurrency, formatDateTime } from '@/lib/utils';
import { useAuthStore } from '@/lib/auth-store';
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 AgentsPage() {
const { user } = useAuthStore();
const queryClient = useQueryClient();
const [filters, setFilters] = useState({ search: '', active: '' });
const [selected, setSelected] = useState<any>(null);
const [createModal, setCreateModal] = useState(false);
const [createForm, setCreateForm] = useState({ iamUserId: '', agentCode: '', commissionRate: '5' });
const [createError, setCreateError] = useState<string | null>(null);
const createMutation = useMutation({
mutationFn: (data: any) => apiClient.post('/agents', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['agents'] });
setCreateModal(false);
setCreateError(null);
},
onError: (e: any) => setCreateError(e?.response?.data?.message || e?.message || 'Failed to create agent'),
});
const [editModal, setEditModal] = useState(false);
const [editForm, setEditForm] = useState({ agentCode: '', commissionRate: '5', active: true });
const [editingAgent, setEditingAgent] = useState<any>(null);
const [editError, setEditError] = useState<string | null>(null);
const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; agent: any | null }>({ isOpen: false, agent: null });
const [deleteError, setDeleteError] = useState<string | null>(null);
const deleteMutation = useMutation({
mutationFn: (id: string) => agentsApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['agents'] });
setDeleteConfirm({ isOpen: false, agent: null });
setDeleteError(null);
},
onError: (e: any) => setDeleteError(e?.response?.data?.message || e?.message || 'Failed to delete agent'),
});
const editMutation = useMutation({
mutationFn: ({ id, ...data }: any) => apiClient.patch(`/agents/${id}`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['agents'] });
setEditModal(false);
setEditError(null);
},
onError: (e: any) => setEditError(e?.response?.data?.message || e?.message || 'Failed to update agent'),
});
const openCreateModal = () => {
setCreateForm({ iamUserId: '', agentCode: '', commissionRate: '5' });
setCreateError(null);
setCreateModal(true);
};
const openEditModal = (agent: any) => {
setEditingAgent(agent);
setEditForm({ agentCode: agent.agentCode, commissionRate: String(agent.commissionRate ?? 5), active: agent.active });
setEditError(null);
setEditModal(true);
};
const { data, isLoading } = useQuery({
queryKey: ['agents', filters],
queryFn: () => agentsApi.getAll(filters),
});
const columns = [
{
key: 'agentCode',
label: 'Agent Code',
sortable: true,
render: (agent: any) => <span className="font-mono font-semibold">{agent.agentCode}</span>,
},
{
key: 'user',
label: 'Name',
render: (agent: any) => (
<div>
<div className="font-medium">{agent.user?.fullName || 'N/A'}</div>
<div className="text-sm text-muted-foreground">{agent.user?.email}</div>
</div>
),
},
{
key: 'commissionRate',
label: 'Commission',
render: (agent: any) => <span>{agent.commissionRate}%</span>,
},
{
key: 'active',
label: 'Status',
render: (agent: any) => (
<Badge variant="status" status={agent.active ? 'CONFIRMED' : 'CANCELLED'}>
{agent.active ? 'Active' : 'Inactive'}
</Badge>
),
},
];
const actions = [
{
label: 'Edit',
onClick: (agent: any) => openEditModal(agent),
variant: 'secondary' as const,
icon: Edit,
},
{
label: 'Details',
onClick: (agent: any) => setSelected(agent),
variant: 'secondary' as const,
icon: Eye,
},
{
label: 'Delete',
onClick: (agent: any) => { setDeleteError(null); setDeleteConfirm({ isOpen: true, agent }); },
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">Agents</h1>
<p className="text-muted-foreground">Manage agents and their operations</p>
</div>
<ActionButton icon={Plus} onClick={openCreateModal}>Add Agent</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 agents..."
className="input"
value={filters.search}
onChange={(e) => setFilters({ ...filters, search: e.target.value })}
/>
</div>
<div>
<label className="label">Status</label>
<select
className="input"
value={filters.active}
onChange={(e) => setFilters({ ...filters, active: e.target.value })}
>
<option value="">All Status</option>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
</div>
</div>
</div>
<DataTable
data={data?.items || []}
columns={columns}
actions={actions}
loading={isLoading}
emptyMessage="No agents found"
/>
<ConfirmDialog
isOpen={deleteConfirm.isOpen}
onClose={() => { setDeleteConfirm({ isOpen: false, agent: null }); setDeleteError(null); }}
onConfirm={async () => { if (deleteConfirm.agent) await deleteMutation.mutateAsync(deleteConfirm.agent.id); }}
title="Delete Agent"
message={`Are you sure you want to delete agent ${deleteConfirm.agent?.agentCode}? This action cannot be undone.`}
confirmText="Delete"
isDanger
isLoading={deleteMutation.isPending}
error={deleteError ?? undefined}
/>
{/* Agent Details Modal */}
<Modal isOpen={!!selected} onClose={() => setSelected(null)} title="Agent Details" size="xl">
{selected && (() => {
const a = selected;
const initials = (a.user?.fullName || a.agentCode || '?').split(' ').map((w: string) => w[0]).join('').slice(0, 2).toUpperCase();
return (
<div>
<div className="from-emerald-600 to-emerald-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">
<span className="text-white text-xl font-bold">{initials}</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-white text-xl font-bold truncate">{a.user?.fullName || 'N/A'}</p>
<p className="text-emerald-200 text-sm font-mono">{a.agentCode}</p>
</div>
<div className="text-right shrink-0">
<Badge variant="status" status={a.active ? 'CONFIRMED' : 'CANCELLED'}>
{a.active ? 'Active' : 'Inactive'}
</Badge>
</div>
</div>
<div className="mt-4 grid grid-cols-3 gap-3">
{[
{ label: 'Agent Code', value: a.agentCode || '—' },
{ label: 'Commission Rate', value: `${a.commissionRate ?? 0}%` },
{ label: 'Total Bookings', value: (a.totalBookings ?? 0).toLocaleString() },
].map(({ label, value }) => (
<div key={label} className="bg-white/10 rounded-lg px-3 py-2">
<p className="text-emerald-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="Agent Information" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Agent Code" value={a.agentCode} mono />
<Field label="Commission Rate" value={`${a.commissionRate ?? 0}%`} />
<Field label="Counter Location" value={a.counterLocation || a.location || 'N/A'} />
<div className="bg-muted/40 rounded-lg p-3">
<p className="text-xs text-muted-foreground mb-2">Status</p>
<Badge variant="status" status={a.active ? 'CONFIRMED' : 'CANCELLED'}>
{a.active ? 'Active' : 'Inactive'}
</Badge>
</div>
</div>
</section>
<section>
<SectionHeader title="User Account" />
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<Field label="Full Name" value={a.user?.fullName} />
<Field label="Email" value={a.user?.email} truncate />
<Field label="Phone" value={a.user?.phone} />
<Field label="Role" value={a.user?.role || 'AGENT'} />
<Field label="User ID" value={a.userId || a.user?.id} mono truncate />
</div>
</section>
<section>
<SectionHeader title="Performance" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Field label="Total Bookings" value={(a.totalBookings ?? 0).toLocaleString()} />
<Field label="Total Revenue" value={a.totalRevenue ? formatCurrency(a.totalRevenue, 'ETB') : 'N/A'} />
<Field label="Total Commission" value={a.totalCommission ? formatCurrency(a.totalCommission, 'ETB') : 'N/A'} />
<Field label="Pending Commission" value={a.pendingCommission ? formatCurrency(a.pendingCommission, 'ETB') : 'N/A'} />
</div>
</section>
<section>
<SectionHeader title="Timestamps & IDs" />
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<Field label="Agent Since" value={formatDateTime(a.createdAt)} />
<Field label="Last Updated" value={formatDateTime(a.updatedAt)} />
<Field label="Agent ID" value={a.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>
{/* Create Agent Modal */}
<Modal isOpen={createModal} onClose={() => setCreateModal(false)} title="Add Agent Profile" size="md">
<div className="space-y-4">
{createError && (
<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">
{createError}
</div>
)}
<div>
<label className="label">IAM User ID *</label>
<input
className="input"
value={createForm.iamUserId}
onChange={(e) => setCreateForm({ ...createForm, iamUserId: e.target.value })}
placeholder="IAM user UUID"
/>
{user?.id && createForm.iamUserId === user.id && (
<p className="text-xs text-emerald-600 dark:text-emerald-400 mt-1"> Pre-filled with your logged-in user ID</p>
)}
<p className="text-xs text-muted-foreground mt-1">Links this agent profile to an IAM back-office user</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">Agent Code (optional)</label>
<input
className="input"
value={createForm.agentCode}
onChange={(e) => setCreateForm({ ...createForm, agentCode: e.target.value })}
placeholder="e.g. AG0002 (auto-generated if empty)"
/>
</div>
<div>
<label className="label">Commission Rate (%)</label>
<input
type="number" min="0" max="100" className="input"
value={createForm.commissionRate}
onChange={(e) => setCreateForm({ ...createForm, commissionRate: e.target.value })}
/>
</div>
</div>
<div className="flex justify-end gap-2 pt-2">
<ActionButton variant="secondary" onClick={() => setCreateModal(false)}>Cancel</ActionButton>
<ActionButton
loading={createMutation.isPending}
onClick={() => createMutation.mutate({
iamUserId: createForm.iamUserId,
agentCode: createForm.agentCode || undefined,
commissionRate: parseInt(createForm.commissionRate) || 5,
})}
>
Create Agent
</ActionButton>
</div>
</div>
</Modal>
{/* Edit Agent Modal */}
<Modal isOpen={editModal} onClose={() => setEditModal(false)} title="Edit Agent" size="md">
<div className="space-y-4">
{editError && (
<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">
{editError}
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="label">Agent Code</label>
<input className="input" value={editForm.agentCode}
onChange={(e) => setEditForm({ ...editForm, agentCode: e.target.value })} />
</div>
<div>
<label className="label">Commission Rate (%)</label>
<input type="number" min="0" max="100" className="input" value={editForm.commissionRate}
onChange={(e) => setEditForm({ ...editForm, commissionRate: e.target.value })} />
</div>
</div>
<div>
<label className="label">Status</label>
<select className="input" value={editForm.active ? 'true' : 'false'}
onChange={(e) => setEditForm({ ...editForm, active: e.target.value === 'true' })}>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
</div>
<div className="flex justify-end gap-2 pt-2">
<ActionButton variant="secondary" onClick={() => setEditModal(false)}>Cancel</ActionButton>
<ActionButton loading={editMutation.isPending} onClick={() => editMutation.mutate({
id: editingAgent.id,
agentCode: editForm.agentCode,
commissionRate: parseInt(editForm.commissionRate) || 5,
active: editForm.active,
})}>Save Changes</ActionButton>
</div>
</div>
</Modal>
</div>
);
}