Checkpoint: Ajout d’un filtre par action dans la liste des automatismes, avec catégories Type d’achat, Service, Ventilation, Abonnement, Abonnement Oui et Abonnement Non. Tests, TypeScript, build et rendu visuel validés en sandbox.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import DashboardLayout from "@/components/DashboardLayout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -33,6 +33,11 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import {
|
||||
AUTOMATION_ACTION_FILTERS,
|
||||
type AutomationActionFilter,
|
||||
matchesAutomationActionFilter,
|
||||
} from "@shared/automationActions";
|
||||
import { toast } from "sonner";
|
||||
import { Plus, Edit, Trash2, Power, PowerOff, Copy } from "lucide-react";
|
||||
|
||||
@@ -63,6 +68,7 @@ export default function AutomationRules() {
|
||||
const [wizardStep, setWizardStep] = useState(1);
|
||||
const [testResultsOpen, setTestResultsOpen] = useState(false);
|
||||
const [testResults, setTestResults] = useState<any>(null);
|
||||
const [actionFilter, setActionFilter] = useState<AutomationActionFilter>("all");
|
||||
|
||||
// Form state
|
||||
const [ruleName, setRuleName] = useState("");
|
||||
@@ -320,6 +326,11 @@ export default function AutomationRules() {
|
||||
{ value: "<=", label: "inférieur ou égal à" },
|
||||
];
|
||||
|
||||
const filteredRules = useMemo(
|
||||
() => rules.filter((rule) => matchesAutomationActionFilter(rule.actions, actionFilter)),
|
||||
[rules, actionFilter],
|
||||
);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
@@ -348,12 +359,36 @@ export default function AutomationRules() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||||
<Label htmlFor="automation-action-filter" className="font-medium text-slate-700">
|
||||
Filtrer par action
|
||||
</Label>
|
||||
<Select value={actionFilter} onValueChange={(value) => setActionFilter(value as AutomationActionFilter)}>
|
||||
<SelectTrigger id="automation-action-filter" className="w-[260px] bg-white">
|
||||
<SelectValue placeholder="Toutes les actions" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AUTOMATION_ACTION_FILTERS.map((filter) => (
|
||||
<SelectItem key={filter.value} value={filter.value}>
|
||||
{filter.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-slate-500">
|
||||
{filteredRules.length} règle{filteredRules.length > 1 ? "s" : ""} affichée{filteredRules.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">Chargement...</div>
|
||||
) : rules.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Aucune règle d'automatisme configurée
|
||||
</div>
|
||||
) : filteredRules.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Aucune règle ne correspond à cette action
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -366,7 +401,7 @@ export default function AutomationRules() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rules.map((rule) => {
|
||||
{filteredRules.map((rule) => {
|
||||
let conditionsDisplay = "";
|
||||
let actionsDisplay = "";
|
||||
try {
|
||||
|
||||
30
server/automationActions.test.ts
Normal file
30
server/automationActions.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { matchesAutomationActionFilter } from "@shared/automationActions";
|
||||
|
||||
describe("matchesAutomationActionFilter", () => {
|
||||
it("filtre chaque type d’action métier", () => {
|
||||
const actions = JSON.stringify({
|
||||
typeAchat: "OPEX",
|
||||
serviceConcerne: "DSI SANTINOVA",
|
||||
ventilationComptable: "SANTINOVA",
|
||||
});
|
||||
|
||||
expect(matchesAutomationActionFilter(actions, "typeAchat")).toBe(true);
|
||||
expect(matchesAutomationActionFilter(actions, "serviceConcerne")).toBe(true);
|
||||
expect(matchesAutomationActionFilter(actions, "ventilationComptable")).toBe(true);
|
||||
expect(matchesAutomationActionFilter(actions, "subscription")).toBe(false);
|
||||
});
|
||||
|
||||
it("distingue les règles Abonnement Oui et Non", () => {
|
||||
expect(matchesAutomationActionFilter('{"isSubscription":1}', "subscription")).toBe(true);
|
||||
expect(matchesAutomationActionFilter('{"isSubscription":1}', "subscriptionYes")).toBe(true);
|
||||
expect(matchesAutomationActionFilter('{"isSubscription":1}', "subscriptionNo")).toBe(false);
|
||||
expect(matchesAutomationActionFilter('{"isSubscription":0}', "subscriptionYes")).toBe(false);
|
||||
expect(matchesAutomationActionFilter('{"isSubscription":0}', "subscriptionNo")).toBe(true);
|
||||
});
|
||||
|
||||
it("tolère les règles historiques invalides sans masquer le filtre Toutes les actions", () => {
|
||||
expect(matchesAutomationActionFilter("json-invalide", "all")).toBe(true);
|
||||
expect(matchesAutomationActionFilter("json-invalide", "subscription")).toBe(false);
|
||||
});
|
||||
});
|
||||
51
shared/automationActions.ts
Normal file
51
shared/automationActions.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/** Les catégories d’actions proposées dans les règles d’automatisme. */
|
||||
export const AUTOMATION_ACTION_FILTERS = [
|
||||
{ value: "all", label: "Toutes les actions" },
|
||||
{ value: "typeAchat", label: "Type d’achat" },
|
||||
{ value: "serviceConcerne", label: "Service concerné" },
|
||||
{ value: "ventilationComptable", label: "Ventilation comptable" },
|
||||
{ value: "subscription", label: "Abonnement (Oui ou Non)" },
|
||||
{ value: "subscriptionYes", label: "Abonnement : Oui" },
|
||||
{ value: "subscriptionNo", label: "Abonnement : Non" },
|
||||
] as const;
|
||||
|
||||
export type AutomationActionFilter = (typeof AUTOMATION_ACTION_FILTERS)[number]["value"];
|
||||
|
||||
type RuleActions = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Lit défensivement le JSON stocké en base : une règle historique invalide ne
|
||||
* doit jamais empêcher l’affichage ni le filtrage de la liste complète.
|
||||
*/
|
||||
function parseRuleActions(actionsJson: string): RuleActions {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(actionsJson);
|
||||
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
||||
? parsed as RuleActions
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getSubscriptionValue(actions: RuleActions): 0 | 1 | undefined {
|
||||
const value = actions.isSubscription;
|
||||
if (value === 1 || value === "1" || value === "OUI") return 1;
|
||||
if (value === 0 || value === "0" || value === "NON") return 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Retourne si les actions JSON d’une règle correspondent au filtre choisi. */
|
||||
export function matchesAutomationActionFilter(
|
||||
actionsJson: string,
|
||||
filter: AutomationActionFilter,
|
||||
): boolean {
|
||||
if (filter === "all") return true;
|
||||
|
||||
const actions = parseRuleActions(actionsJson);
|
||||
if (filter === "subscription") return Object.hasOwn(actions, "isSubscription");
|
||||
if (filter === "subscriptionYes") return getSubscriptionValue(actions) === 1;
|
||||
if (filter === "subscriptionNo") return getSubscriptionValue(actions) === 0;
|
||||
|
||||
return typeof actions[filter] === "string" && actions[filter].trim().length > 0;
|
||||
}
|
||||
6
todo.md
6
todo.md
@@ -770,3 +770,9 @@
|
||||
- [x] Pousser le correctif vers Gitea production
|
||||
- [x] Construire et redémarrer uniquement le conteneur applicatif
|
||||
- [x] Vérifier le commit, la santé, HTTP et le bundle de production
|
||||
|
||||
## Filtre par action des automatismes
|
||||
- [x] Identifier les types d’action disponibles et leurs règles de détection
|
||||
- [x] Ajouter un filtre d’action dans la liste des automatismes
|
||||
- [x] Permettre d’isoler les règles Abonnement, Oui ou Non
|
||||
- [x] Ajouter les tests et valider TypeScript, tests et build
|
||||
|
||||
Reference in New Issue
Block a user