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

- @@ -187,7 +190,7 @@ export default function InvoiceDetail() { {/* Header */}
- diff --git a/client/src/pages/InvoicesBAP.tsx b/client/src/pages/InvoicesBAP.tsx index e6b7385..a8f1abd 100644 --- a/client/src/pages/InvoicesBAP.tsx +++ b/client/src/pages/InvoicesBAP.tsx @@ -87,7 +87,8 @@ function downloadBapPdf( } import * as XLSX from 'xlsx'; import { toast } from "sonner"; -import { useLocation } from "wouter"; +import { useLocation, useSearch } from "wouter"; +import { buildBapDetailLocation, parseBapFilters } from "@/lib/bapNavigation"; // Helper function to determine field color based on origin const getFieldColor = (invoice: any, fieldName: string): string => { @@ -108,9 +109,12 @@ const getFieldColor = (invoice: any, fieldName: string): string => { export default function InvoicesBAP() { const [, setLocation] = useLocation(); - const [searchQuery, setSearchQuery] = useState(""); + const queryString = useSearch(); + const currentYear = new Date().getFullYear(); + const initialFilters = parseBapFilters(queryString, String(currentYear)); + const [searchQuery, setSearchQuery] = useState(initialFilters.searchQuery); const [selectedIds, setSelectedIds] = useState([]); - const [statusFilter, setStatusFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState(initialFilters.statusFilter); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [invoiceToDelete, setInvoiceToDelete] = useState(null); const [addDialogOpen, setAddDialogOpen] = useState(false); @@ -126,12 +130,22 @@ export default function InvoicesBAP() { const [expandedIds, setExpandedIds] = useState>(new Set()); const [allExpanded, setAllExpanded] = useState(false); // Filtre par période - const currentYear = new Date().getFullYear(); - const [selectedYear, setSelectedYear] = useState(String(currentYear)); - const [selectedMonth, setSelectedMonth] = useState("all"); // "all" ou "01".."12" + const [selectedYear, setSelectedYear] = useState(initialFilters.selectedYear); + const [selectedMonth, setSelectedMonth] = useState(initialFilters.selectedMonth); // "all" ou "01".."12" // Tri - const [sortField, setSortField] = useState<"invoiceDate" | "createdAt">("createdAt"); - const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); + const [sortField, setSortField] = useState<"invoiceDate" | "createdAt">(initialFilters.sortField); + const [sortDir, setSortDir] = useState<"asc" | "desc">(initialFilters.sortDir); + + const openInvoiceDetail = (invoiceId: number) => { + setLocation(buildBapDetailLocation(invoiceId, { + searchQuery, + statusFilter, + selectedYear, + selectedMonth, + sortField, + sortDir, + })); + }; const toggleRow = (id: number) => { setExpandedIds(prev => { @@ -926,7 +940,7 @@ export default function InvoicesBAP() {