Heaven K
fix: remove "ICC " prefix from all branch/succursale names
1ad3123
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
MapPin,
Search,
Plus,
Pencil,
Trash2,
Check,
X,
ChevronLeft,
ChevronRight,
} from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { cn } from '@/lib/utils';
import { api } from '@/services/api';
import { toast } from 'sonner';
interface BranchData {
id: number;
email: string;
branch: string;
active: boolean;
updatedAt: string;
}
const BRANCH_COLORS = [
'bg-blue-100 text-blue-600',
'bg-indigo-100 text-indigo-600',
'bg-purple-100 text-purple-600',
'bg-teal-100 text-teal-600',
'bg-amber-100 text-amber-600',
'bg-pink-100 text-pink-600',
'bg-emerald-100 text-emerald-600',
'bg-cyan-100 text-cyan-600',
];
function getBranchColor(name: string): string {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return BRANCH_COLORS[Math.abs(hash) % BRANCH_COLORS.length];
}
const PER_PAGE = 100;
export default function BranchTable() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [currentPage, setCurrentPage] = useState(1);
// Add form state
const [showAddForm, setShowAddForm] = useState(false);
const [addEmail, setAddEmail] = useState('');
const [addBranch, setAddBranch] = useState('');
// Edit state
const [editingId, setEditingId] = useState<number | null>(null);
const [editEmail, setEditEmail] = useState('');
const [editBranch, setEditBranch] = useState('');
// Delete confirmation state
const [deletingId, setDeletingId] = useState<number | null>(null);
// Fetch branches
const { data, isLoading, isError } = useQuery({
queryKey: ['branches'],
queryFn: () => api.get<{ branches: BranchData[] }>('/settings/branches'),
retry: false,
});
const allBranches = data?.branches ?? [];
// Filter branches
const filtered = allBranches.filter((b) => {
const matchesSearch =
!search ||
b.email.toLowerCase().includes(search.toLowerCase()) ||
b.branch.toLowerCase().includes(search.toLowerCase());
const matchesStatus =
statusFilter === 'all' ||
(statusFilter === 'active' && b.active) ||
(statusFilter === 'inactive' && !b.active);
return matchesSearch && matchesStatus;
});
const totalPages = Math.ceil(filtered.length / PER_PAGE);
const paginated = filtered.slice(
(currentPage - 1) * PER_PAGE,
currentPage * PER_PAGE
);
const startItem = filtered.length > 0 ? (currentPage - 1) * PER_PAGE + 1 : 0;
const endItem = Math.min(currentPage * PER_PAGE, filtered.length);
// Mutations
const addMutation = useMutation({
mutationFn: (data: { email: string; branch: string }) =>
api.post('/settings/branches', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['branches'] });
setShowAddForm(false);
setAddEmail('');
setAddBranch('');
toast.success('Succursale ajoutée');
},
onError: (err: Error) => {
toast.error(err.message);
},
});
const updateMutation = useMutation({
mutationFn: ({ id, ...data }: { id: number; email?: string; branch?: string; active?: boolean }) =>
api.put(`/settings/branches/${id}`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['branches'] });
setEditingId(null);
toast.success('Succursale mise à jour');
},
onError: (err: Error) => {
toast.error(err.message);
},
});
const deleteMutation = useMutation({
mutationFn: (id: number) => api.delete(`/settings/branches/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['branches'] });
setDeletingId(null);
toast.success('Succursale supprimée');
},
onError: (err: Error) => {
toast.error(err.message);
},
});
const handleAdd = () => {
if (!addEmail || !addBranch) return;
addMutation.mutate({ email: addEmail, branch: addBranch });
};
const startEdit = (b: BranchData) => {
setEditingId(b.id);
setEditEmail(b.email);
setEditBranch(b.branch);
};
const handleEdit = () => {
if (editingId === null || !editEmail || !editBranch) return;
updateMutation.mutate({ id: editingId, email: editEmail, branch: editBranch });
};
const toggleActive = (b: BranchData) => {
updateMutation.mutate({ id: b.id, active: !b.active });
};
return (
<div className="flex flex-col gap-6">
{/* Search & Filters */}
<div className="flex flex-col gap-4 rounded-xl bg-card p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between border border-border">
<div className="relative w-full max-w-md">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Input
className="pl-10 bg-slate-50 border-slate-300"
placeholder={t('branches.searchPlaceholder')}
value={search}
onChange={(e) => {
setSearch(e.target.value);
setCurrentPage(1);
}}
/>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-slate-700" htmlFor="status-filter">
{t('branches.statusLabel')}:
</label>
<Select value={statusFilter} onValueChange={(v) => { setStatusFilter(v); setCurrentPage(1); }}>
<SelectTrigger className="w-[130px] bg-slate-50 border-slate-300">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t('branches.statusAll')}</SelectItem>
<SelectItem value="active">{t('branches.statusActive')}</SelectItem>
<SelectItem value="inactive">{t('branches.statusInactive')}</SelectItem>
</SelectContent>
</Select>
</div>
<Button
size="sm"
className="gap-2"
onClick={() => setShowAddForm(!showAddForm)}
>
<Plus className="h-4 w-4" />
{t('branches.addBranch')}
</Button>
</div>
</div>
{/* Add form */}
{showAddForm && (
<div className="flex items-end gap-3 rounded-xl bg-blue-50 border border-blue-200 p-4">
<div className="flex-1 space-y-1">
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wide">
{t('branches.colEmail')}
</label>
<Input
placeholder="email@iccameriques.org"
value={addEmail}
onChange={(e) => setAddEmail(e.target.value)}
className="bg-white"
/>
</div>
<div className="flex-1 space-y-1">
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wide">
{t('branches.colBranch')}
</label>
<Input
placeholder="Ville"
value={addBranch}
onChange={(e) => setAddBranch(e.target.value)}
className="bg-white"
/>
</div>
<Button onClick={handleAdd} disabled={addMutation.isPending} className="gap-2">
<Check className="h-4 w-4" />
{t('common.save')}
</Button>
<Button
variant="outline"
onClick={() => { setShowAddForm(false); setAddEmail(''); setAddBranch(''); }}
>
{t('common.cancel')}
</Button>
</div>
)}
{/* Table */}
<div className="overflow-hidden rounded-xl border border-border bg-card shadow-sm">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-slate-50">
<TableHead className="font-semibold text-xs uppercase text-slate-700">
{t('branches.colEmail')}
</TableHead>
<TableHead className="font-semibold text-xs uppercase text-slate-700">
{t('branches.colBranch')}
</TableHead>
<TableHead className="font-semibold text-xs uppercase text-slate-700">
{t('branches.colStatus')}
</TableHead>
<TableHead className="font-semibold text-xs uppercase text-slate-700 text-right">
{t('branches.colActions')}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
Array.from({ length: 5 }).map((_, i) => (
<TableRow key={i} className="animate-pulse">
{Array.from({ length: 4 }).map((_, j) => (
<TableCell key={j} className="py-4">
<div className="h-4 bg-slate-100 rounded w-32" />
</TableCell>
))}
</TableRow>
))
) : isError ? (
<TableRow>
<TableCell colSpan={4} className="py-12 text-center text-red-500">
Veuillez vous connecter pour voir les succursales.
</TableCell>
</TableRow>
) : paginated.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="py-16 text-center">
<div className="flex flex-col items-center gap-3">
<div className="h-12 w-12 rounded-xl bg-slate-100 flex items-center justify-center">
<MapPin className="h-6 w-6 text-slate-400" />
</div>
<div>
<p className="text-sm font-medium text-slate-700">
{search ? t('transactions.noResults') : t('branches.emptyTitle', 'No branches configured yet')}
</p>
<p className="text-xs text-slate-500 mt-1">
{search ? '' : t('branches.emptyDesc', 'Add a branch to map recipient emails to branch names')}
</p>
</div>
{!search && !showAddForm && (
<Button size="sm" className="gap-2 mt-2" onClick={() => setShowAddForm(true)}>
<Plus className="h-4 w-4" />
{t('branches.addBranch')}
</Button>
)}
</div>
</TableCell>
</TableRow>
) : (
paginated.map((branch) => (
<TableRow
key={branch.id}
className="bg-white hover:bg-slate-50 transition-colors"
>
{editingId === branch.id ? (
<>
<TableCell>
<Input
value={editEmail}
onChange={(e) => setEditEmail(e.target.value)}
className="h-8 text-sm"
/>
</TableCell>
<TableCell>
<Input
value={editBranch}
onChange={(e) => setEditBranch(e.target.value)}
className="h-8 text-sm"
/>
</TableCell>
<TableCell>
<button
onClick={() => toggleActive(branch)}
className="cursor-pointer"
>
{branch.active ? (
<span className="inline-flex items-center rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800">
<span className="mr-1.5 h-1.5 w-1.5 rounded-full bg-emerald-500" />
{t('branches.statusActive')}
</span>
) : (
<span className="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-medium text-slate-600">
<span className="mr-1.5 h-1.5 w-1.5 rounded-full bg-slate-400" />
{t('branches.statusInactive')}
</span>
)}
</button>
</TableCell>
<TableCell className="text-right">
<button
onClick={handleEdit}
className="rounded-lg p-2 text-emerald-600 hover:bg-emerald-50 transition-colors"
>
<Check className="h-4 w-4" />
</button>
<button
onClick={() => setEditingId(null)}
className="rounded-lg p-2 text-slate-500 hover:bg-slate-100 transition-colors"
>
<X className="h-4 w-4" />
</button>
</TableCell>
</>
) : (
<>
<TableCell className="font-medium text-slate-900 whitespace-nowrap">
{branch.email}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div
className={cn(
'flex h-8 w-8 items-center justify-center rounded-full',
getBranchColor(branch.branch)
)}
>
<MapPin className="h-3.5 w-3.5" />
</div>
<span className="text-slate-600">{branch.branch}</span>
</div>
</TableCell>
<TableCell>
<button
onClick={() => toggleActive(branch)}
className="cursor-pointer"
>
{branch.active ? (
<span className="inline-flex items-center rounded-full bg-emerald-100 px-2.5 py-0.5 text-xs font-medium text-emerald-800 hover:bg-emerald-200 transition-colors">
<span className="mr-1.5 h-1.5 w-1.5 rounded-full bg-emerald-500" />
{t('branches.statusActive')}
</span>
) : (
<span className="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-medium text-slate-600 hover:bg-slate-200 transition-colors">
<span className="mr-1.5 h-1.5 w-1.5 rounded-full bg-slate-400" />
{t('branches.statusInactive')}
</span>
)}
</button>
</TableCell>
<TableCell className="text-right">
{deletingId === branch.id ? (
<>
<Button
size="sm"
variant="outline"
className="h-8 text-xs border-red-300 text-red-700 hover:bg-red-50 mr-1"
onClick={() => deleteMutation.mutate(branch.id)}
disabled={deleteMutation.isPending}
>
Confirmer
</Button>
<Button
size="sm"
variant="outline"
className="h-8 text-xs"
onClick={() => setDeletingId(null)}
>
{t('common.cancel')}
</Button>
</>
) : (
<>
<button
onClick={() => startEdit(branch)}
className="rounded-lg p-2 text-slate-500 hover:bg-slate-100 hover:text-primary transition-colors"
>
<Pencil className="h-4 w-4" />
</button>
<button
onClick={() => setDeletingId(branch.id)}
className="rounded-lg p-2 text-slate-500 hover:bg-slate-100 hover:text-red-600 transition-colors"
>
<Trash2 className="h-4 w-4" />
</button>
</>
)}
</TableCell>
</>
)}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
{filtered.length > 0 && (
<div className="flex items-center justify-between border-t border-border bg-white px-4 py-3 sm:px-6">
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<p className="text-sm text-slate-700">
{t('transactions.showing')}{' '}
<span className="font-medium text-slate-900">{startItem}</span>{' '}
{t('transactions.to')}{' '}
<span className="font-medium text-slate-900">{endItem}</span>{' '}
{t('transactions.of')}{' '}
<span className="font-medium text-slate-900">{filtered.length}</span>{' '}
{t('transactions.results')}
</p>
{totalPages > 1 && (
<nav className="isolate inline-flex -space-x-px rounded-md shadow-sm">
<button
className="relative inline-flex items-center rounded-l-md px-2 py-2 text-slate-400 ring-1 ring-inset ring-slate-300 hover:bg-slate-50 disabled:opacity-50"
disabled={currentPage === 1}
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
>
<ChevronLeft className="h-4 w-4" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
className={cn(
'relative inline-flex items-center px-4 py-2 text-sm font-semibold',
page === currentPage
? 'z-10 bg-primary text-white'
: 'text-slate-900 ring-1 ring-inset ring-slate-300 hover:bg-slate-50'
)}
onClick={() => setCurrentPage(page)}
>
{page}
</button>
))}
<button
className="relative inline-flex items-center rounded-r-md px-2 py-2 text-slate-400 ring-1 ring-inset ring-slate-300 hover:bg-slate-50 disabled:opacity-50"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
>
<ChevronRight className="h-4 w-4" />
</button>
</nav>
)}
</div>
</div>
)}
</div>
</div>
);
}