Checkpoint: Zone de filtres Factures refactorisée en 3 lignes dans un cartouche bleu pastel : ligne 1 (recherche + compact/détail), ligne 2 (destinataire, abonnement, entité, ventilation comptable), ligne 3 (boutons statut export + tri par date).
This commit is contained in:
@@ -54,6 +54,7 @@ export default function Invoices() {
|
|||||||
const [recipientFilter, setRecipientFilter] = useState<string>("all");
|
const [recipientFilter, setRecipientFilter] = useState<string>("all");
|
||||||
const [subscriptionFilter, setSubscriptionFilter] = useState<string>("all"); // all | yes | no
|
const [subscriptionFilter, setSubscriptionFilter] = useState<string>("all"); // all | yes | no
|
||||||
const [entityFilter, setEntityFilter] = useState<string>("all"); // all | santinova | itinova
|
const [entityFilter, setEntityFilter] = useState<string>("all"); // all | santinova | itinova
|
||||||
|
const [ventilationFilter, setVentilationFilter] = useState<string>("all");
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [invoiceToDelete, setInvoiceToDelete] = useState<number | null>(null);
|
const [invoiceToDelete, setInvoiceToDelete] = useState<number | null>(null);
|
||||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||||
@@ -199,6 +200,12 @@ export default function Invoices() {
|
|||||||
)
|
)
|
||||||
).sort();
|
).sort();
|
||||||
|
|
||||||
|
const uniqueVentilations = Array.from(
|
||||||
|
new Set(
|
||||||
|
(invoices || []).map(inv => (inv as any).ventilationComptable).filter(Boolean)
|
||||||
|
)
|
||||||
|
).sort();
|
||||||
|
|
||||||
const handleSort = (field: SortField) => {
|
const handleSort = (field: SortField) => {
|
||||||
if (sortField === field) {
|
if (sortField === field) {
|
||||||
setSortDir(d => d === "asc" ? "desc" : "asc");
|
setSortDir(d => d === "asc" ? "desc" : "asc");
|
||||||
@@ -250,6 +257,14 @@ export default function Invoices() {
|
|||||||
if (entityFilter === "santinova" && service !== "DSI SANTINOVA") return false;
|
if (entityFilter === "santinova" && service !== "DSI SANTINOVA") return false;
|
||||||
if (entityFilter === "itinova" && service === "DSI SANTINOVA") return false;
|
if (entityFilter === "itinova" && service === "DSI SANTINOVA") return false;
|
||||||
}
|
}
|
||||||
|
// Filter by ventilation comptable
|
||||||
|
if (ventilationFilter !== "all") {
|
||||||
|
if (ventilationFilter === "__empty__") {
|
||||||
|
if ((inv as any).ventilationComptable) return false;
|
||||||
|
} else {
|
||||||
|
if ((inv as any).ventilationComptable !== ventilationFilter) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -405,135 +420,150 @@ export default function Invoices() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
{/* ===== CARTOUCHE FILTRES ===== */}
|
||||||
<CardContent className="pt-6">
|
<div className="rounded-xl border border-blue-100 bg-blue-50/60 px-4 py-3 mb-4 shadow-sm">
|
||||||
{/* Search + Recipient Filter */}
|
|
||||||
<div className="mb-4 flex gap-3 flex-wrap">
|
|
||||||
<div className="relative flex-1 min-w-[200px]">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
||||||
<Input
|
|
||||||
placeholder="Rechercher par fournisseur, destinataire ou numéro..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 min-w-[220px]">
|
|
||||||
<Filter className="w-4 h-4 text-gray-400 shrink-0" />
|
|
||||||
<Select value={recipientFilter} onValueChange={setRecipientFilter}>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Filtrer par destinataire" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les destinataires</SelectItem>
|
|
||||||
<SelectItem value="__empty__">Sans destinataire</SelectItem>
|
|
||||||
{uniqueRecipients.map((r) => (
|
|
||||||
<SelectItem key={r} value={r}>{r}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{/* Filtre Abonnement */}
|
|
||||||
<div className="flex items-center gap-2 min-w-[180px]">
|
|
||||||
<Select value={subscriptionFilter} onValueChange={setSubscriptionFilter}>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Abonnement" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous (abonnement)</SelectItem>
|
|
||||||
<SelectItem value="yes">Abonnement : OUI</SelectItem>
|
|
||||||
<SelectItem value="no">Abonnement : NON</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{/* Filtre Entité */}
|
|
||||||
<div className="flex items-center gap-2 min-w-[180px]">
|
|
||||||
<Select value={entityFilter} onValueChange={setEntityFilter}>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Entité" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Toutes entités</SelectItem>
|
|
||||||
<SelectItem value="santinova">SANTINOVA</SelectItem>
|
|
||||||
<SelectItem value="itinova">ITINOVA</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mode compact/détail + Tri */}
|
|
||||||
<div className="flex gap-2 mb-4 flex-wrap items-center justify-between">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant={compactMode ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCompactMode(true)}
|
|
||||||
title="Mode compact"
|
|
||||||
>
|
|
||||||
<LayoutList className="w-4 h-4 mr-1" /> Compact
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={!compactMode ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setCompactMode(false)}
|
|
||||||
title="Mode détail"
|
|
||||||
>
|
|
||||||
<LayoutGrid className="w-4 h-4 mr-1" /> Détail
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<span className="text-sm text-gray-500">Trier par :</span>
|
|
||||||
<Button
|
|
||||||
variant={sortField === "createdAt" ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleSort("createdAt")}
|
|
||||||
>
|
|
||||||
Date réception <SortIcon field="createdAt" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={sortField === "invoiceDate" ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleSort("invoiceDate")}
|
|
||||||
>
|
|
||||||
Date facture <SortIcon field="invoiceDate" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Filters */}
|
{/* Ligne 1 : Recherche + affichage compact/détail */}
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-3 items-center flex-wrap mb-3">
|
||||||
|
<div className="relative flex-1 min-w-[220px]">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-blue-400 w-4 h-4" />
|
||||||
|
<Input
|
||||||
|
placeholder="Rechercher par fournisseur, destinataire ou numéro..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 bg-white border-blue-200 focus:border-blue-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 shrink-0">
|
||||||
<Button
|
<Button
|
||||||
variant={statusFilter === "all" ? "default" : "outline"}
|
variant={compactMode ? "default" : "outline"}
|
||||||
onClick={() => setStatusFilter("all")}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
onClick={() => setCompactMode(true)}
|
||||||
|
title="Mode compact"
|
||||||
|
className={compactMode ? "bg-blue-600 hover:bg-blue-700" : "bg-white border-blue-200 text-blue-700 hover:bg-blue-50"}
|
||||||
>
|
>
|
||||||
Tous ({statusCounts.all})
|
<LayoutList className="w-4 h-4 mr-1" /> Compact
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={statusFilter === "exported" ? "default" : "outline"}
|
variant={!compactMode ? "default" : "outline"}
|
||||||
onClick={() => setStatusFilter("exported")}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
className={statusFilter === "exported" ? "bg-green-600 hover:bg-green-700" : ""}
|
onClick={() => setCompactMode(false)}
|
||||||
|
title="Mode détail"
|
||||||
|
className={!compactMode ? "bg-blue-600 hover:bg-blue-700" : "bg-white border-blue-200 text-blue-700 hover:bg-blue-50"}
|
||||||
>
|
>
|
||||||
Exportés ({statusCounts.exported})
|
<LayoutGrid className="w-4 h-4 mr-1" /> Détail
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={statusFilter === "not_exported" ? "default" : "outline"}
|
|
||||||
onClick={() => setStatusFilter("not_exported")}
|
|
||||||
size="sm"
|
|
||||||
className={statusFilter === "not_exported" ? "bg-blue-600 hover:bg-blue-700" : ""}
|
|
||||||
>
|
|
||||||
Non exportés ({statusCounts.not_exported})
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={statusFilter === "export_error" ? "default" : "outline"}
|
|
||||||
onClick={() => setStatusFilter("export_error")}
|
|
||||||
size="sm"
|
|
||||||
className={statusFilter === "export_error" ? "bg-red-600 hover:bg-red-700" : ""}
|
|
||||||
>
|
|
||||||
Erreurs ({statusCounts.export_error})
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ligne 2 : Filtres destinataire, abonnement, entité, ventilation */}
|
||||||
|
<div className="flex gap-2 items-center flex-wrap mb-3">
|
||||||
|
<Filter className="w-4 h-4 text-blue-400 shrink-0" />
|
||||||
|
<Select value={recipientFilter} onValueChange={setRecipientFilter}>
|
||||||
|
<SelectTrigger className="w-[180px] bg-white border-blue-200 text-sm">
|
||||||
|
<SelectValue placeholder="Destinataire" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Tous destinataires</SelectItem>
|
||||||
|
<SelectItem value="__empty__">Sans destinataire</SelectItem>
|
||||||
|
{uniqueRecipients.map((r) => (
|
||||||
|
<SelectItem key={r} value={r}>{r}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={subscriptionFilter} onValueChange={setSubscriptionFilter}>
|
||||||
|
<SelectTrigger className="w-[170px] bg-white border-blue-200 text-sm">
|
||||||
|
<SelectValue placeholder="Abonnement" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Abonnement : tous</SelectItem>
|
||||||
|
<SelectItem value="yes">Abonnement : OUI</SelectItem>
|
||||||
|
<SelectItem value="no">Abonnement : NON</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={entityFilter} onValueChange={setEntityFilter}>
|
||||||
|
<SelectTrigger className="w-[150px] bg-white border-blue-200 text-sm">
|
||||||
|
<SelectValue placeholder="Entité" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Toutes entités</SelectItem>
|
||||||
|
<SelectItem value="santinova">SANTINOVA</SelectItem>
|
||||||
|
<SelectItem value="itinova">ITINOVA</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={ventilationFilter} onValueChange={setVentilationFilter}>
|
||||||
|
<SelectTrigger className="w-[170px] bg-white border-blue-200 text-sm">
|
||||||
|
<SelectValue placeholder="Ventilation" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Toutes ventilations</SelectItem>
|
||||||
|
<SelectItem value="__empty__">Sans ventilation</SelectItem>
|
||||||
|
{uniqueVentilations.map((v) => (
|
||||||
|
<SelectItem key={v} value={v}>{v}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ligne 3 : Boutons statut export + tri */}
|
||||||
|
<div className="flex gap-2 items-center flex-wrap">
|
||||||
|
<Button
|
||||||
|
variant={statusFilter === "all" ? "default" : "outline"}
|
||||||
|
onClick={() => setStatusFilter("all")}
|
||||||
|
size="sm"
|
||||||
|
className={statusFilter === "all" ? "bg-blue-600 hover:bg-blue-700" : "bg-white border-blue-200 text-blue-700 hover:bg-blue-50"}
|
||||||
|
>
|
||||||
|
Tous ({statusCounts.all})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={statusFilter === "exported" ? "default" : "outline"}
|
||||||
|
onClick={() => setStatusFilter("exported")}
|
||||||
|
size="sm"
|
||||||
|
className={statusFilter === "exported" ? "bg-green-600 hover:bg-green-700 text-white" : "bg-white border-green-200 text-green-700 hover:bg-green-50"}
|
||||||
|
>
|
||||||
|
Exportés ({statusCounts.exported})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={statusFilter === "not_exported" ? "default" : "outline"}
|
||||||
|
onClick={() => setStatusFilter("not_exported")}
|
||||||
|
size="sm"
|
||||||
|
className={statusFilter === "not_exported" ? "bg-indigo-600 hover:bg-indigo-700 text-white" : "bg-white border-indigo-200 text-indigo-700 hover:bg-indigo-50"}
|
||||||
|
>
|
||||||
|
Non exportés ({statusCounts.not_exported})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={statusFilter === "export_error" ? "default" : "outline"}
|
||||||
|
onClick={() => setStatusFilter("export_error")}
|
||||||
|
size="sm"
|
||||||
|
className={statusFilter === "export_error" ? "bg-red-600 hover:bg-red-700 text-white" : "bg-white border-red-200 text-red-700 hover:bg-red-50"}
|
||||||
|
>
|
||||||
|
Erreurs ({statusCounts.export_error})
|
||||||
|
</Button>
|
||||||
|
<div className="ml-auto flex gap-1 items-center">
|
||||||
|
<span className="text-xs text-blue-500 font-medium mr-1">Trier :</span>
|
||||||
|
<Button
|
||||||
|
variant={sortField === "createdAt" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSort("createdAt")}
|
||||||
|
className={sortField === "createdAt" ? "bg-blue-600 hover:bg-blue-700" : "bg-white border-blue-200 text-blue-700 hover:bg-blue-50"}
|
||||||
|
>
|
||||||
|
Réception <SortIcon field="createdAt" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={sortField === "invoiceDate" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSort("invoiceDate")}
|
||||||
|
className={sortField === "invoiceDate" ? "bg-blue-600 hover:bg-blue-700" : "bg-white border-blue-200 text-blue-700 hover:bg-blue-50"}
|
||||||
|
>
|
||||||
|
Facture <SortIcon field="invoiceDate" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* ===== FIN CARTOUCHE ===== */}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-4">
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user