diff --git a/client/src/lib/bapNavigation.ts b/client/src/lib/bapNavigation.ts
new file mode 100644
index 0000000..4ed7852
--- /dev/null
+++ b/client/src/lib/bapNavigation.ts
@@ -0,0 +1,83 @@
+/**
+ * Paramètres d’affichage qui doivent survivre au passage par le détail d’une
+ * facture BAP. Seules ces clés sont sérialisées afin d’éviter de propager des
+ * paramètres de navigation arbitraires.
+ */
+export type BapFilters = {
+ searchQuery: string;
+ statusFilter: string;
+ selectedYear: string;
+ selectedMonth: string;
+ sortField: "invoiceDate" | "createdAt";
+ sortDir: "asc" | "desc";
+};
+
+const BAP_STATUSES = new Set([
+ "all",
+ "exported",
+ "not_exported",
+ "export_error",
+ "bap_validated",
+ "bap_pending",
+ "to_complete",
+]);
+
+function asSearchParams(search: string): URLSearchParams {
+ return new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
+}
+
+function isMonth(value: string | null): value is string {
+ return value !== null && /^(0[1-9]|1[0-2])$/.test(value);
+}
+
+function isYear(value: string | null): value is string {
+ return value === "all" || (value !== null && /^\d{4}$/.test(value));
+}
+
+/** Lit et valide les filtres BAP transmis dans l’URL. */
+export function parseBapFilters(search: string, defaultYear: string): BapFilters {
+ const params = asSearchParams(search);
+ const status = params.get("status");
+ const year = params.get("year");
+ const month = params.get("month");
+ const sort = params.get("sort");
+ const direction = params.get("dir");
+
+ return {
+ searchQuery: params.get("q") || "",
+ statusFilter: status && BAP_STATUSES.has(status) ? status : "all",
+ selectedYear: isYear(year) ? year : defaultYear,
+ selectedMonth: month === "all" || isMonth(month) ? month : "all",
+ sortField: sort === "invoiceDate" ? "invoiceDate" : "createdAt",
+ sortDir: direction === "asc" ? "asc" : "desc",
+ };
+}
+
+/** Construit une URL de détail qui conserve explicitement le contexte BAP. */
+export function buildBapDetailLocation(invoiceId: number, filters: BapFilters): string {
+ const params = new URLSearchParams({
+ returnTo: "bap",
+ q: filters.searchQuery,
+ status: filters.statusFilter,
+ year: filters.selectedYear,
+ month: filters.selectedMonth,
+ sort: filters.sortField,
+ dir: filters.sortDir,
+ });
+
+ return `/invoices/${invoiceId}?${params.toString()}`;
+}
+
+/** Retourne la liste d’origine. Sans contexte BAP, le comportement historique est conservé. */
+export function getBapReturnLocation(search: string): string {
+ const params = asSearchParams(search);
+ if (params.get("returnTo") !== "bap") return "/invoices";
+
+ const allowed = new URLSearchParams();
+ for (const key of ["q", "status", "year", "month", "sort", "dir"]) {
+ const value = params.get(key);
+ if (value) allowed.set(key, value);
+ }
+ const query = allowed.toString();
+ return query ? `/invoices-bap?${query}` : "/invoices-bap";
+}
diff --git a/client/src/pages/AutomationRules.tsx b/client/src/pages/AutomationRules.tsx
index 1f52697..f8bd13c 100644
--- a/client/src/pages/AutomationRules.tsx
+++ b/client/src/pages/AutomationRules.tsx
@@ -46,6 +46,7 @@ interface Actions {
typeAchat?: string;
serviceConcerne?: string;
ventilationComptable?: string;
+ isSubscription?: 0 | 1;
}
export default function AutomationRules() {
@@ -84,6 +85,12 @@ export default function AutomationRules() {
return val;
};
+ const normalizeSubscriptionAction = (value: unknown): 0 | 1 | undefined => {
+ if (value === 1 || value === "1" || value === "OUI") return 1;
+ if (value === 0 || value === "0" || value === "NON") return 0;
+ return undefined;
+ };
+
const createMutation = trpc.automationRules.create.useMutation({
onSuccess: (rule) => {
if (rule.isActive === 1) {
@@ -204,7 +211,7 @@ export default function AutomationRules() {
} else {
setCustomVentilation("");
}
- setActions(parsedActions);
+ setActions({ ...parsedActions, isSubscription: normalizeSubscriptionAction(parsedActions.isSubscription) });
} catch {
setActions({});
setCustomTypeAchat("");
@@ -231,11 +238,13 @@ export default function AutomationRules() {
typeAchat: resolveAction(actions.typeAchat, customTypeAchat),
serviceConcerne: resolveAction(actions.serviceConcerne, customServiceConcerne),
ventilationComptable: resolveAction(actions.ventilationComptable, customVentilation),
+ isSubscription: actions.isSubscription,
};
// Supprimer les clés undefined
if (!resolvedActions.typeAchat) delete resolvedActions.typeAchat;
if (!resolvedActions.serviceConcerne) delete resolvedActions.serviceConcerne;
if (!resolvedActions.ventilationComptable) delete resolvedActions.ventilationComptable;
+ if (resolvedActions.isSubscription === undefined) delete resolvedActions.isSubscription;
const actionsJSON = JSON.stringify(resolvedActions);
if (editingRule) {
@@ -372,6 +381,7 @@ export default function AutomationRules() {
if (acts.typeAchat) actionsArr.push(`Type: ${acts.typeAchat}`);
if (acts.serviceConcerne) actionsArr.push(`Service: ${acts.serviceConcerne}`);
if (acts.ventilationComptable) actionsArr.push(`Ventilation: ${acts.ventilationComptable}`);
+ if (acts.isSubscription !== undefined) actionsArr.push(`Abonnement: ${normalizeSubscriptionAction(acts.isSubscription) === 1 ? "Oui" : "Non"}`);
actionsDisplay = actionsArr.join(", ");
} catch {}
@@ -610,6 +620,23 @@ export default function AutomationRules() {
/>
)}
+
+
+
+
+
)}
@@ -642,7 +669,8 @@ export default function AutomationRules() {
{actions.typeAchat && • Type d'achat : {actions.typeAchat}
}
{actions.serviceConcerne && • Service : {actions.serviceConcerne}
}
{actions.ventilationComptable && • Ventilation : {actions.ventilationComptable}
}
- {!actions.typeAchat && !actions.serviceConcerne && !actions.ventilationComptable && (
+ {actions.isSubscription !== undefined && • Abonnement : {actions.isSubscription === 1 ? "Oui" : "Non"}
}
+ {!actions.typeAchat && !actions.serviceConcerne && !actions.ventilationComptable && actions.isSubscription === undefined && (
Aucune action définie
)}
@@ -829,6 +857,23 @@ export default function AutomationRules() {
/>
)}
+
+
+
+
+
diff --git a/client/src/pages/InvoiceDetail.tsx b/client/src/pages/InvoiceDetail.tsx
index a69ea4e..bdda05c 100644
--- a/client/src/pages/InvoiceDetail.tsx
+++ b/client/src/pages/InvoiceDetail.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
-import { useParams, useLocation } from "wouter";
+import { useParams, useLocation, useSearch } from "wouter";
import DashboardLayout from "@/components/DashboardLayout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -9,10 +9,13 @@ import { Badge } from "@/components/ui/badge";
import { trpc } from "@/lib/trpc";
import { ArrowLeft, Save, FileText } from "lucide-react";
import { toast } from "sonner";
+import { getBapReturnLocation } from "@/lib/bapNavigation";
export default function InvoiceDetail() {
const { id } = useParams<{ id: string }>();
const [, setLocation] = useLocation();
+ const detailSearch = useSearch();
+ const returnLocation = getBapReturnLocation(detailSearch);
const invoiceId = parseInt(id || "0");
const { data: invoice, isLoading } = trpc.invoices.getById.useQuery({ id: invoiceId });
@@ -172,7 +175,7 @@ export default function InvoiceDetail() {
Facture introuvable
Cette facture n'existe pas ou a été supprimée
-