Checkpoint: Factures BAP inclut désormais, après Année et Mois, les filtres Destinataires, Entités et Ventilations comptables de la page Factures. Leur logique est identique, avec valeurs absentes et règle SANTINOVA/ITINOVA fondée sur DSI SANTINOVA. Les filtres sont conservés au retour depuis le détail. Tests, TypeScript, build et rendu visuel validés en sandbox.
This commit is contained in:
@@ -8,6 +8,9 @@ export type BapFilters = {
|
||||
statusFilter: string;
|
||||
selectedYear: string;
|
||||
selectedMonth: string;
|
||||
recipientFilter: string;
|
||||
entityFilter: string;
|
||||
ventilationFilter: string;
|
||||
sortField: "invoiceDate" | "createdAt";
|
||||
sortDir: "asc" | "desc";
|
||||
};
|
||||
@@ -34,6 +37,10 @@ function isYear(value: string | null): value is string {
|
||||
return value === "all" || (value !== null && /^\d{4}$/.test(value));
|
||||
}
|
||||
|
||||
function isSafeFilterValue(value: string | null): value is string {
|
||||
return value !== null && value.length > 0 && value.length <= 200;
|
||||
}
|
||||
|
||||
/** Lit et valide les filtres BAP transmis dans l’URL. */
|
||||
export function parseBapFilters(search: string, defaultYear: string): BapFilters {
|
||||
const params = asSearchParams(search);
|
||||
@@ -48,6 +55,9 @@ export function parseBapFilters(search: string, defaultYear: string): BapFilters
|
||||
statusFilter: status && BAP_STATUSES.has(status) ? status : "all",
|
||||
selectedYear: isYear(year) ? year : defaultYear,
|
||||
selectedMonth: month === "all" || isMonth(month) ? month : "all",
|
||||
recipientFilter: isSafeFilterValue(params.get("recipient")) ? params.get("recipient")! : "all",
|
||||
entityFilter: ["all", "santinova", "itinova"].includes(params.get("entity") || "") ? params.get("entity")! : "all",
|
||||
ventilationFilter: isSafeFilterValue(params.get("ventilation")) ? params.get("ventilation")! : "all",
|
||||
sortField: sort === "invoiceDate" ? "invoiceDate" : "createdAt",
|
||||
sortDir: direction === "asc" ? "asc" : "desc",
|
||||
};
|
||||
@@ -61,6 +71,9 @@ export function buildBapDetailLocation(invoiceId: number, filters: BapFilters):
|
||||
status: filters.statusFilter,
|
||||
year: filters.selectedYear,
|
||||
month: filters.selectedMonth,
|
||||
recipient: filters.recipientFilter,
|
||||
entity: filters.entityFilter,
|
||||
ventilation: filters.ventilationFilter,
|
||||
sort: filters.sortField,
|
||||
dir: filters.sortDir,
|
||||
});
|
||||
@@ -74,7 +87,7 @@ export function getBapReturnLocation(search: string): string {
|
||||
if (params.get("returnTo") !== "bap") return "/invoices";
|
||||
|
||||
const allowed = new URLSearchParams();
|
||||
for (const key of ["q", "status", "year", "month", "sort", "dir"]) {
|
||||
for (const key of ["q", "status", "year", "month", "recipient", "entity", "ventilation", "sort", "dir"]) {
|
||||
const value = params.get(key);
|
||||
if (value) allowed.set(key, value);
|
||||
}
|
||||
|
||||
@@ -116,6 +116,9 @@ export default function InvoicesBAP() {
|
||||
const [searchQuery, setSearchQuery] = useState(initialFilters.searchQuery);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [statusFilter, setStatusFilter] = useState<string>(initialFilters.statusFilter);
|
||||
const [recipientFilter, setRecipientFilter] = useState<string>(initialFilters.recipientFilter);
|
||||
const [entityFilter, setEntityFilter] = useState<string>(initialFilters.entityFilter);
|
||||
const [ventilationFilter, setVentilationFilter] = useState<string>(initialFilters.ventilationFilter);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [invoiceToDelete, setInvoiceToDelete] = useState<number | null>(null);
|
||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||
@@ -143,6 +146,9 @@ export default function InvoicesBAP() {
|
||||
statusFilter,
|
||||
selectedYear,
|
||||
selectedMonth,
|
||||
recipientFilter,
|
||||
entityFilter,
|
||||
ventilationFilter,
|
||||
sortField,
|
||||
sortDir,
|
||||
}));
|
||||
@@ -169,6 +175,14 @@ export default function InvoicesBAP() {
|
||||
const { data: allInvoices, isLoading } = trpc.invoices.list.useQuery();
|
||||
// Filter for BAP invoices only (Abonnement = NON, isSubscription = 0)
|
||||
const invoices = allInvoices?.filter(inv => inv.isSubscription === 0);
|
||||
const uniqueRecipients = useMemo(
|
||||
() => Array.from(new Set((invoices || []).map(inv => (inv as any).recipientName).filter(Boolean))).sort(),
|
||||
[invoices],
|
||||
);
|
||||
const uniqueVentilations = useMemo(
|
||||
() => Array.from(new Set((invoices || []).map(inv => (inv as any).ventilationComptable).filter(Boolean))).sort(),
|
||||
[invoices],
|
||||
);
|
||||
// IDs des factures déjà validées BAP (pour charger leurs pdfUrl depuis bapHistory)
|
||||
// Stabilisé avec useMemo pour éviter les re-renders infinis (anti-pattern tRPC)
|
||||
const validatedInvoiceIds = useMemo(
|
||||
@@ -440,6 +454,27 @@ export default function InvoicesBAP() {
|
||||
if (statusFilter === "bap_pending" && inv.bapValidated === 1) return false;
|
||||
if (statusFilter === "to_complete" && !isToComplete(inv)) return false;
|
||||
}
|
||||
|
||||
// Même convention que la page Factures : valeur vide et entités déduites du service.
|
||||
if (recipientFilter !== "all") {
|
||||
if (recipientFilter === "__empty__") {
|
||||
if ((inv as any).recipientName) return false;
|
||||
} else if ((inv as any).recipientName !== recipientFilter) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (entityFilter !== "all") {
|
||||
const service = ((inv as any).serviceConcerne || "").trim().toUpperCase();
|
||||
if (entityFilter === "santinova" && service !== "DSI SANTINOVA") return false;
|
||||
if (entityFilter === "itinova" && service === "DSI SANTINOVA") return false;
|
||||
}
|
||||
if (ventilationFilter !== "all") {
|
||||
if (ventilationFilter === "__empty__") {
|
||||
if ((inv as any).ventilationComptable) return false;
|
||||
} else if ((inv as any).ventilationComptable !== ventilationFilter) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -778,6 +813,40 @@ export default function InvoicesBAP() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={recipientFilter} onValueChange={setRecipientFilter}>
|
||||
<SelectTrigger className="w-[180px] h-8 text-sm bg-white border-emerald-200">
|
||||
<SelectValue placeholder="Destinataire" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tous destinataires</SelectItem>
|
||||
<SelectItem value="__empty__">Sans destinataire</SelectItem>
|
||||
{uniqueRecipients.map((recipient) => (
|
||||
<SelectItem key={recipient} value={recipient}>{recipient}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={entityFilter} onValueChange={setEntityFilter}>
|
||||
<SelectTrigger className="w-[150px] h-8 text-sm bg-white border-emerald-200">
|
||||
<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] h-8 text-sm bg-white border-emerald-200">
|
||||
<SelectValue placeholder="Ventilation" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toutes ventilations</SelectItem>
|
||||
<SelectItem value="__empty__">Sans ventilation</SelectItem>
|
||||
{uniqueVentilations.map((ventilation) => (
|
||||
<SelectItem key={ventilation} value={ventilation}>{ventilation}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(selectedYear !== "all" || selectedMonth !== "all") && (
|
||||
<button
|
||||
onClick={() => { setSelectedYear(String(currentYear)); setSelectedMonth("all"); }}
|
||||
|
||||
@@ -12,27 +12,33 @@ describe("navigation Factures BAP", () => {
|
||||
statusFilter: "bap_pending",
|
||||
selectedYear: "2026",
|
||||
selectedMonth: "05",
|
||||
recipientFilter: "Direction générale",
|
||||
entityFilter: "santinova",
|
||||
ventilationFilter: "615200",
|
||||
sortField: "invoiceDate",
|
||||
sortDir: "asc",
|
||||
});
|
||||
|
||||
expect(location).toBe(
|
||||
"/invoices/42?returnTo=bap&q=microsoft+europe&status=bap_pending&year=2026&month=05&sort=invoiceDate&dir=asc"
|
||||
"/invoices/42?returnTo=bap&q=microsoft+europe&status=bap_pending&year=2026&month=05&recipient=Direction+g%C3%A9n%C3%A9rale&entity=santinova&ventilation=615200&sort=invoiceDate&dir=asc"
|
||||
);
|
||||
});
|
||||
|
||||
it("restaure la liste BAP et les filtres de façon sûre", () => {
|
||||
const detailSearch =
|
||||
"?returnTo=bap&q=microsoft+europe&status=bap_pending&year=2026&month=05&sort=invoiceDate&dir=asc";
|
||||
"?returnTo=bap&q=microsoft+europe&status=bap_pending&year=2026&month=05&recipient=Direction+g%C3%A9n%C3%A9rale&entity=santinova&ventilation=615200&sort=invoiceDate&dir=asc";
|
||||
|
||||
expect(getBapReturnLocation(detailSearch)).toBe(
|
||||
"/invoices-bap?q=microsoft+europe&status=bap_pending&year=2026&month=05&sort=invoiceDate&dir=asc"
|
||||
"/invoices-bap?q=microsoft+europe&status=bap_pending&year=2026&month=05&recipient=Direction+g%C3%A9n%C3%A9rale&entity=santinova&ventilation=615200&sort=invoiceDate&dir=asc"
|
||||
);
|
||||
expect(parseBapFilters(detailSearch, "2026")).toEqual({
|
||||
searchQuery: "microsoft europe",
|
||||
statusFilter: "bap_pending",
|
||||
selectedYear: "2026",
|
||||
selectedMonth: "05",
|
||||
recipientFilter: "Direction générale",
|
||||
entityFilter: "santinova",
|
||||
ventilationFilter: "615200",
|
||||
sortField: "invoiceDate",
|
||||
sortDir: "asc",
|
||||
});
|
||||
@@ -44,6 +50,9 @@ describe("navigation Factures BAP", () => {
|
||||
statusFilter: "all",
|
||||
selectedYear: "2026",
|
||||
selectedMonth: "all",
|
||||
recipientFilter: "all",
|
||||
entityFilter: "all",
|
||||
ventilationFilter: "all",
|
||||
sortField: "createdAt",
|
||||
sortDir: "desc",
|
||||
});
|
||||
|
||||
5
todo.md
5
todo.md
@@ -853,3 +853,8 @@
|
||||
- [x] Ajouter une action administrateur d’import e-mail manuel par compte configuré
|
||||
- [x] Garantir que le déclenchement manuel ne modifie jamais la planification automatique
|
||||
- [x] Ajouter les tests de non-régression et valider le parcours en sandbox
|
||||
|
||||
## Alignement des filtres Factures BAP
|
||||
- [x] Ajouter les filtres Destinataires, Entités et Ventilations après le filtre Mois
|
||||
- [x] Appliquer ces filtres à la liste BAP en préservant les filtres et le tri existants
|
||||
- [x] Ajouter les tests et valider l’affichage de Factures BAP en sandbox
|
||||
|
||||
Reference in New Issue
Block a user