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;
|
statusFilter: string;
|
||||||
selectedYear: string;
|
selectedYear: string;
|
||||||
selectedMonth: string;
|
selectedMonth: string;
|
||||||
|
recipientFilter: string;
|
||||||
|
entityFilter: string;
|
||||||
|
ventilationFilter: string;
|
||||||
sortField: "invoiceDate" | "createdAt";
|
sortField: "invoiceDate" | "createdAt";
|
||||||
sortDir: "asc" | "desc";
|
sortDir: "asc" | "desc";
|
||||||
};
|
};
|
||||||
@@ -34,6 +37,10 @@ function isYear(value: string | null): value is string {
|
|||||||
return value === "all" || (value !== null && /^\d{4}$/.test(value));
|
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. */
|
/** Lit et valide les filtres BAP transmis dans l’URL. */
|
||||||
export function parseBapFilters(search: string, defaultYear: string): BapFilters {
|
export function parseBapFilters(search: string, defaultYear: string): BapFilters {
|
||||||
const params = asSearchParams(search);
|
const params = asSearchParams(search);
|
||||||
@@ -48,6 +55,9 @@ export function parseBapFilters(search: string, defaultYear: string): BapFilters
|
|||||||
statusFilter: status && BAP_STATUSES.has(status) ? status : "all",
|
statusFilter: status && BAP_STATUSES.has(status) ? status : "all",
|
||||||
selectedYear: isYear(year) ? year : defaultYear,
|
selectedYear: isYear(year) ? year : defaultYear,
|
||||||
selectedMonth: month === "all" || isMonth(month) ? month : "all",
|
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",
|
sortField: sort === "invoiceDate" ? "invoiceDate" : "createdAt",
|
||||||
sortDir: direction === "asc" ? "asc" : "desc",
|
sortDir: direction === "asc" ? "asc" : "desc",
|
||||||
};
|
};
|
||||||
@@ -61,6 +71,9 @@ export function buildBapDetailLocation(invoiceId: number, filters: BapFilters):
|
|||||||
status: filters.statusFilter,
|
status: filters.statusFilter,
|
||||||
year: filters.selectedYear,
|
year: filters.selectedYear,
|
||||||
month: filters.selectedMonth,
|
month: filters.selectedMonth,
|
||||||
|
recipient: filters.recipientFilter,
|
||||||
|
entity: filters.entityFilter,
|
||||||
|
ventilation: filters.ventilationFilter,
|
||||||
sort: filters.sortField,
|
sort: filters.sortField,
|
||||||
dir: filters.sortDir,
|
dir: filters.sortDir,
|
||||||
});
|
});
|
||||||
@@ -74,7 +87,7 @@ export function getBapReturnLocation(search: string): string {
|
|||||||
if (params.get("returnTo") !== "bap") return "/invoices";
|
if (params.get("returnTo") !== "bap") return "/invoices";
|
||||||
|
|
||||||
const allowed = new URLSearchParams();
|
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);
|
const value = params.get(key);
|
||||||
if (value) allowed.set(key, value);
|
if (value) allowed.set(key, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ export default function InvoicesBAP() {
|
|||||||
const [searchQuery, setSearchQuery] = useState(initialFilters.searchQuery);
|
const [searchQuery, setSearchQuery] = useState(initialFilters.searchQuery);
|
||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
const [statusFilter, setStatusFilter] = useState<string>(initialFilters.statusFilter);
|
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 [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);
|
||||||
@@ -143,6 +146,9 @@ export default function InvoicesBAP() {
|
|||||||
statusFilter,
|
statusFilter,
|
||||||
selectedYear,
|
selectedYear,
|
||||||
selectedMonth,
|
selectedMonth,
|
||||||
|
recipientFilter,
|
||||||
|
entityFilter,
|
||||||
|
ventilationFilter,
|
||||||
sortField,
|
sortField,
|
||||||
sortDir,
|
sortDir,
|
||||||
}));
|
}));
|
||||||
@@ -169,6 +175,14 @@ export default function InvoicesBAP() {
|
|||||||
const { data: allInvoices, isLoading } = trpc.invoices.list.useQuery();
|
const { data: allInvoices, isLoading } = trpc.invoices.list.useQuery();
|
||||||
// Filter for BAP invoices only (Abonnement = NON, isSubscription = 0)
|
// Filter for BAP invoices only (Abonnement = NON, isSubscription = 0)
|
||||||
const invoices = allInvoices?.filter(inv => inv.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)
|
// 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)
|
// Stabilisé avec useMemo pour éviter les re-renders infinis (anti-pattern tRPC)
|
||||||
const validatedInvoiceIds = useMemo(
|
const validatedInvoiceIds = useMemo(
|
||||||
@@ -441,6 +455,27 @@ export default function InvoicesBAP() {
|
|||||||
if (statusFilter === "to_complete" && !isToComplete(inv)) 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;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -778,6 +813,40 @@ export default function InvoicesBAP() {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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") && (
|
{(selectedYear !== "all" || selectedMonth !== "all") && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setSelectedYear(String(currentYear)); setSelectedMonth("all"); }}
|
onClick={() => { setSelectedYear(String(currentYear)); setSelectedMonth("all"); }}
|
||||||
|
|||||||
@@ -12,27 +12,33 @@ describe("navigation Factures BAP", () => {
|
|||||||
statusFilter: "bap_pending",
|
statusFilter: "bap_pending",
|
||||||
selectedYear: "2026",
|
selectedYear: "2026",
|
||||||
selectedMonth: "05",
|
selectedMonth: "05",
|
||||||
|
recipientFilter: "Direction générale",
|
||||||
|
entityFilter: "santinova",
|
||||||
|
ventilationFilter: "615200",
|
||||||
sortField: "invoiceDate",
|
sortField: "invoiceDate",
|
||||||
sortDir: "asc",
|
sortDir: "asc",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(location).toBe(
|
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", () => {
|
it("restaure la liste BAP et les filtres de façon sûre", () => {
|
||||||
const detailSearch =
|
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(
|
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({
|
expect(parseBapFilters(detailSearch, "2026")).toEqual({
|
||||||
searchQuery: "microsoft europe",
|
searchQuery: "microsoft europe",
|
||||||
statusFilter: "bap_pending",
|
statusFilter: "bap_pending",
|
||||||
selectedYear: "2026",
|
selectedYear: "2026",
|
||||||
selectedMonth: "05",
|
selectedMonth: "05",
|
||||||
|
recipientFilter: "Direction générale",
|
||||||
|
entityFilter: "santinova",
|
||||||
|
ventilationFilter: "615200",
|
||||||
sortField: "invoiceDate",
|
sortField: "invoiceDate",
|
||||||
sortDir: "asc",
|
sortDir: "asc",
|
||||||
});
|
});
|
||||||
@@ -44,6 +50,9 @@ describe("navigation Factures BAP", () => {
|
|||||||
statusFilter: "all",
|
statusFilter: "all",
|
||||||
selectedYear: "2026",
|
selectedYear: "2026",
|
||||||
selectedMonth: "all",
|
selectedMonth: "all",
|
||||||
|
recipientFilter: "all",
|
||||||
|
entityFilter: "all",
|
||||||
|
ventilationFilter: "all",
|
||||||
sortField: "createdAt",
|
sortField: "createdAt",
|
||||||
sortDir: "desc",
|
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] 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] 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
|
- [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