Compare commits

...

5 Commits

5 changed files with 138 additions and 4 deletions

View File

@@ -8,7 +8,7 @@
}, },
"containerName": "demat-facturation-app", "containerName": "demat-facturation-app",
"image": "images/demat-facturation-dsi.jpg", "image": "images/demat-facturation-dsi.jpg",
"giteaRepo": "demat-facturation", "giteaRepo": "demat-facturation-dsi",
"giteaOwner": "manus-admin", "giteaOwner": "manus-admin",
"ci": { "ci": {
"required": true "required": true

View File

@@ -1,4 +1,4 @@
import { useState } from "react"; import { useMemo, useState } from "react";
import DashboardLayout from "@/components/DashboardLayout"; import DashboardLayout from "@/components/DashboardLayout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -33,6 +33,11 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { trpc } from "@/lib/trpc"; import { trpc } from "@/lib/trpc";
import {
AUTOMATION_ACTION_FILTERS,
type AutomationActionFilter,
matchesAutomationActionFilter,
} from "@shared/automationActions";
import { toast } from "sonner"; import { toast } from "sonner";
import { Plus, Edit, Trash2, Power, PowerOff, Copy } from "lucide-react"; import { Plus, Edit, Trash2, Power, PowerOff, Copy } from "lucide-react";
@@ -63,6 +68,7 @@ export default function AutomationRules() {
const [wizardStep, setWizardStep] = useState(1); const [wizardStep, setWizardStep] = useState(1);
const [testResultsOpen, setTestResultsOpen] = useState(false); const [testResultsOpen, setTestResultsOpen] = useState(false);
const [testResults, setTestResults] = useState<any>(null); const [testResults, setTestResults] = useState<any>(null);
const [actionFilter, setActionFilter] = useState<AutomationActionFilter>("all");
// Form state // Form state
const [ruleName, setRuleName] = useState(""); const [ruleName, setRuleName] = useState("");
@@ -320,6 +326,11 @@ export default function AutomationRules() {
{ value: "<=", label: "inférieur ou égal à" }, { value: "<=", label: "inférieur ou égal à" },
]; ];
const filteredRules = useMemo(
() => rules.filter((rule) => matchesAutomationActionFilter(rule.actions, actionFilter)),
[rules, actionFilter],
);
return ( return (
<DashboardLayout> <DashboardLayout>
<div className="space-y-6"> <div className="space-y-6">
@@ -348,12 +359,36 @@ export default function AutomationRules() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <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 ? ( {isLoading ? (
<div className="text-center py-8 text-gray-500">Chargement...</div> <div className="text-center py-8 text-gray-500">Chargement...</div>
) : rules.length === 0 ? ( ) : rules.length === 0 ? (
<div className="text-center py-8 text-gray-500"> <div className="text-center py-8 text-gray-500">
Aucune règle d'automatisme configurée Aucune règle d'automatisme configurée
</div> </div>
) : filteredRules.length === 0 ? (
<div className="text-center py-8 text-gray-500">
Aucune règle ne correspond à cette action
</div>
) : ( ) : (
<Table> <Table>
<TableHeader> <TableHeader>
@@ -366,7 +401,7 @@ export default function AutomationRules() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{rules.map((rule) => { {filteredRules.map((rule) => {
let conditionsDisplay = ""; let conditionsDisplay = "";
let actionsDisplay = ""; let actionsDisplay = "";
try { try {

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { matchesAutomationActionFilter } from "@shared/automationActions";
describe("matchesAutomationActionFilter", () => {
it("filtre chaque type daction 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);
});
});

View File

@@ -0,0 +1,51 @@
/** Les catégories dactions proposées dans les règles dautomatisme. */
export const AUTOMATION_ACTION_FILTERS = [
{ value: "all", label: "Toutes les actions" },
{ value: "typeAchat", label: "Type dachat" },
{ 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 laffichage 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 dune 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;
}

20
todo.md
View File

@@ -763,4 +763,22 @@
## Correctif recette — dialogue dautomatisme ## Correctif recette — dialogue dautomatisme
- [x] Vérifier le commit et localiser lemplacement absent de loption Abonnement - [x] Vérifier le commit et localiser lemplacement absent de loption Abonnement
- [ ] Corriger le dialogue dédition et le valider en recette - [x] Corriger le dialogue dédition et le valider en recette
- [x] Arrêter le processus non applicatif autorisé qui bloque le build Docker
## Déploiement production — dialogue Abonnement
- [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 daction disponibles et leurs règles de détection
- [x] Ajouter un filtre daction dans la liste des automatismes
- [x] Permettre disoler les règles Abonnement, Oui ou Non
- [x] Ajouter les tests et valider TypeScript, tests et build
## Déploiement — filtre par action des automatismes
- [ ] Pousser le filtre par action vers Gitea recette
- [ ] Déployer et vérifier le filtre par action en recette
- [ ] Pousser la version validée vers Gitea production
- [ ] Déployer et vérifier le filtre par action en production