From 269b9e0e13b7dc43f079348c1d06a0ae6f880af0 Mon Sep 17 00:00:00 2001 From: Domi31tls Date: Mon, 15 Jun 2026 09:19:43 +0200 Subject: [PATCH] fix(gui): complete V6 admin configuration mockup --- gui_v6/tabs/tab_config.py | 331 +++++++++++++++--- .../test_gui_v6_config_mockup_sections.py | 26 ++ 2 files changed, 316 insertions(+), 41 deletions(-) create mode 100644 tests/unit/test_gui_v6_config_mockup_sections.py diff --git a/gui_v6/tabs/tab_config.py b/gui_v6/tabs/tab_config.py index aa35a67..e53c093 100644 --- a/gui_v6/tabs/tab_config.py +++ b/gui_v6/tabs/tab_config.py @@ -17,7 +17,44 @@ from gui_v6 import theme as theme_mod from gui_v6 import ui_kit from gui_v6.config_state import ConfigState, default_profile_key, list_profile_keys -_SUBTABS = [("reg", "⚙️ Réglages"), ("msk", "🎭 Masquage"), ("shr", "🔄 Partage"), ("rul", "🛡️ Règles")] +_SUBTABS = [ + ("reg", "⚙️ Réglages"), + ("msk", "🎭 Masquage"), + ("shr", "🔄 Partage"), + ("rul", "🛡️ Règles 2"), +] + +_DETECTION_OPTIONS = [ + ("Noms et prénoms", "Gazetteers INSEE · CamemBERT"), + ("Dates de naissance", "Uniquement la date de naissance"), + ("Établissements", "Répertoire FINESS + contexte"), + ("Adresses et codes postaux", ""), + ("N° sécurité sociale", ""), + ("Téléphones et e-mails", ""), + ("N° adhérent mutuelle", ""), +] + +_PRESERVE_TERMS = ["FUROSEMIDE", "rééducation fonctionnelle", "classification internationale"] +_MASK_TERMS = ["CHUXX"] + +CONFIG_MOCKUP_SECTIONS = { + "reglages": [ + "Profil métier", + "Moteurs NER", + "Données à détecter", + "Termes à toujours conserver", + "Termes à toujours masquer", + ], + "masquage": [ + "Couleur de masquage (PDF)", + "Style des marqueurs (texte)", + "Épaisseur du masque", + "Codes de remplacement", + "Masques de zones fixes", + ], + "partage": ["Exporter la configuration", "Importer une configuration"], + "regles": ["Règles actives", "Testeur de règle"], +} class ConfigTab(ctk.CTkFrame): @@ -68,9 +105,11 @@ class ConfigTab(ctk.CTkFrame): def _build_reglages(self) -> None: p = self._p - # Profil + sortie - prof = ui_kit.Card(self._body, p, title="🗂️ Profil métier") - prof.pack(fill="x", padx=14, pady=7) + cols = self._two_columns(self._body) + left, right = cols + + prof = ui_kit.Card(left, p, title="🗂️ Profil métier") + prof.pack(fill="x", pady=7) row = ctk.CTkFrame(prof, fg_color="transparent") row.pack(fill="x", padx=16, pady=(0, 10)) profiles = list_profile_keys() @@ -89,65 +128,182 @@ class ConfigTab(ctk.CTkFrame): text_color=p["text_muted"], font=ui_kit.font(12)) self._out_label.pack(side="left") - # Moteurs NER (câblé) - ner = ui_kit.Card(self._body, p, title="🧠 Moteurs NER") - ner.pack(fill="x", padx=14, pady=7) + ner = ui_kit.Card(left, p, title="🧠 Moteurs NER") + ner.pack(fill="x", pady=7) self._tog_ner = ui_kit.ToggleRow(ner, p, "CamemBERT-bio ⚡", "~10 ms/doc · F1 = 0.963", value=self._state.use_local_ner, command=self._on_ner) self._tog_ner.pack(fill="x", padx=16, pady=2) - self._tog_eds = ui_kit.ToggleRow(ner, p, "EDS-Pseudo", "médical français (optionnel)", value=self._state.enable_eds, command=self._on_eds) + self._tog_eds = ui_kit.ToggleRow(ner, p, "EDS-Pseudo PRÉCIS", "~200 ms/doc · médical français", value=self._state.enable_eds, command=self._on_eds) self._tog_eds.pack(fill="x", padx=16, pady=2) - self._tog_gli = ui_kit.ToggleRow(ner, p, "GLiNER", "vote croisé (optionnel)", value=self._state.enable_gliner, command=self._on_gliner) + self._tog_gli = ui_kit.ToggleRow(ner, p, "GLiNER OPTIONNEL", "~95 ms/doc · vote croisé", value=self._state.enable_gliner, command=self._on_gliner) self._tog_gli.pack(fill="x", padx=16, pady=(2, 14)) - # Données à détecter (visuel) - det = ui_kit.Card(self._body, p, title="🔍 Données à détecter") - det.pack(fill="x", padx=14, pady=7) - for label, hint in [ - ("Noms et prénoms", "Gazetteers INSEE · CamemBERT"), - ("Dates de naissance", "Uniquement la date de naissance"), - ("Établissements", "Répertoire FINESS + contexte"), - ("Adresses et codes postaux", ""), - ("N° sécurité sociale", ""), - ("Téléphones et e-mails", ""), - ]: + det = ui_kit.Card(right, p, title="🔍 Données à détecter") + det.pack(fill="x", pady=7) + for label, hint in _DETECTION_OPTIONS: ui_kit.ToggleRow(det, p, label, hint, value=True).pack(fill="x", padx=16, pady=2) ctk.CTkFrame(det, fg_color="transparent", height=8).pack() + keep = ui_kit.Card(right, p, title="✅ Termes à toujours conserver") + keep.pack(fill="x", pady=7) + self._note(keep, "Ces termes ne seront jamais masqués, même s'ils ressemblent à un nom propre.") + self._tag_editor(keep, "Ex : FUROSEMIDE…", _PRESERVE_TERMS, "keep") + + mask = ui_kit.Card(right, p, title="🚫 Termes à toujours masquer") + mask.pack(fill="x", pady=7) + self._note(mask, "Ces termes seront toujours masqués, même sans contexte médical autour.") + self._tag_editor(mask, "Ex : CHUXX, Dr Dupont…", _MASK_TERMS, "mask") + def _build_masquage(self) -> None: p = self._p - card = ui_kit.Card(self._body, p, title="🎭 Masquage") - card.pack(fill="x", padx=14, pady=7) - ctk.CTkLabel(card, text="Couleur, style des marqueurs et éditeur de masques PDF.", - text_color=p["text_dim"], font=ui_kit.font(12), anchor="w", justify="left").pack(anchor="w", padx=16) - codes = ui_kit.Card(self._body, p, title="🔒 Codes de remplacement") - codes.pack(fill="x", padx=14, pady=7) - for k, v in [("Nom/Prénom", "[NOM]"), ("Date naissance", "[DATE_NAISSANCE]"), - ("Établissement", "[ETABLISSEMENT]"), ("Adresse", "[ADRESSE]"), ("N° sécu", "[NIR]")]: + cols = self._two_columns(self._body) + left, right = cols + + color = ui_kit.Card(left, p, title="⬛ Couleur de masquage (PDF)") + color.pack(fill="x", pady=7) + self._note(color, "Couleur des rectangles dans le PDF final.") + swatches = ctk.CTkFrame(color, fg_color="transparent") + swatches.pack(fill="x", padx=16, pady=(0, 14)) + for color_value in ["#000000", "#1a1a2e", "#374151", "#92400e", "#1e3a5f"]: + ctk.CTkButton( + swatches, + text="", + width=30, + height=30, + fg_color=color_value, + hover_color=color_value, + border_color=p["primary"] if color_value == "#000000" else p["card_border"], + border_width=3 if color_value == "#000000" else 1, + corner_radius=6, + command=lambda: None, + ).pack(side="left", padx=(0, 8)) + + style = ui_kit.Card(left, p, title="🏷️ Style des marqueurs (texte)") + style.pack(fill="x", pady=7) + self._mask_style_var = ctk.StringVar(value="brackets") + for label, value, preview in [ + ("Crochets", "brackets", "[NOM]"), + ("Étoiles", "stars", "***"), + ("Noirci", "blackout", "████"), + ]: + row = ctk.CTkFrame(style, fg_color="transparent") + row.pack(fill="x", padx=16, pady=2) + ctk.CTkRadioButton( + row, + text=f"{label} — {preview}", + variable=self._mask_style_var, + value=value, + command=self._update_mask_preview, + text_color=p["text"], + fg_color=p["primary"], + hover_color=p["primary_dim"], + font=ui_kit.font(13), + ).pack(anchor="w") + self._mask_preview = ctk.CTkLabel( + style, + text="Patient [NOM], né le [DATE_NAISSANCE]", + text_color=p["text_dim"], + fg_color=p["divider"], + corner_radius=6, + font=ui_kit.font(13), + anchor="w", + ) + self._mask_preview.pack(fill="x", padx=16, pady=(8, 14), ipady=8) + + thick = ui_kit.Card(right, p, title="📐 Épaisseur du masque") + thick.pack(fill="x", pady=7) + self._note(thick, "Marge autour du texte masqué, en points.") + self._slider_row(thick, "Marge horizontale", 2) + self._slider_row(thick, "Marge verticale", 1) + ui_kit.ToggleRow(thick, p, "Coins arrondis", "", value=False).pack(fill="x", padx=16, pady=(2, 14)) + + codes = ui_kit.Card(right, p, title="🔒 Codes de remplacement") + codes.pack(fill="x", pady=7) + for k, v in [ + ("Nom/Prénom", "[NOM]"), + ("Date naissance", "[DATE_NAISSANCE]"), + ("Établissement", "[ETABLISSEMENT]"), + ("Adresse", "[ADRESSE]"), + ("Téléphone", "[TEL]"), + ("N° sécu", "[NIR]"), + ("IPP", "[IPP]"), + ("Email", "[EMAIL]"), + ]: r = ctk.CTkFrame(codes, fg_color="transparent") r.pack(fill="x", padx=16, pady=1) ctk.CTkLabel(r, text=k, text_color=p["text_muted"], font=ui_kit.font(12)).pack(side="left") ctk.CTkLabel(r, text=v, text_color=p["primary"], font=ui_kit.font(12, "bold")).pack(side="right") ctk.CTkFrame(codes, fg_color="transparent", height=8).pack() - ctk.CTkLabel(self._body, text="Éditeur de masques PDF (zones fixes) : intégré au lot suivant.", - text_color=p["text_muted"], font=ui_kit.font(11)).pack(anchor="w", padx=18, pady=6) + + editor = ui_kit.Card(self._body, p, title="🏠 Masques de zones fixes (logos, en-têtes)") + editor.pack(fill="x", padx=14, pady=7) + top = ctk.CTkFrame(editor, fg_color="transparent") + top.pack(fill="x", padx=16, pady=(0, 8)) + ctk.CTkLabel( + top, + text="Dessinez des rectangles sur un PDF modèle pour masquer systématiquement les logos, en-têtes ou zones fixes.", + text_color=p["text_dim"], + font=ui_kit.font(12), + justify="left", + wraplength=610, + ).pack(side="left", fill="x", expand=True) + ui_kit.primary_button(top, p, "✏ Ouvrir l'éditeur de masques", command=lambda: None).pack(side="right", padx=(8, 0)) + toolbar = ctk.CTkFrame(editor, fg_color=p["divider"], border_color=p["card_border"], border_width=1, corner_radius=8) + toolbar.pack(fill="x", padx=16, pady=(0, 10)) + for label in ["📄 Ouvrir PDF…", "−", "100%", "+", "💾 Sauver", "📁 Charger", "🗑 Effacer page"]: + ui_kit.secondary_button(toolbar, p, label, command=lambda: None).pack(side="left", padx=4, pady=8) + canvas = ctk.CTkFrame(editor, fg_color=p["divider"], border_color=p["card_border"], border_width=1, corner_radius=8, height=150) + canvas.pack(fill="x", padx=16, pady=(0, 12)) + canvas.pack_propagate(False) + ctk.CTkLabel( + canvas, + text="📄\nOuvrez un PDF pour commencer à dessiner des zones de masquage.\nCliquez-glissez pour tracer un rectangle.", + text_color=p["text_muted"], + font=ui_kit.font(12), + justify="center", + ).pack(expand=True) def _build_partage(self) -> None: p = self._p - card = ui_kit.Card(self._body, p, title="🔄 Partage de configuration") - card.pack(fill="x", padx=14, pady=7) - ctk.CTkLabel(card, text="Exporter / importer la configuration (whitelist, blacklist, profils) par JSON.", - text_color=p["text_dim"], font=ui_kit.font(12), anchor="w", justify="left").pack(anchor="w", padx=16, pady=(0, 8)) - actrow = ctk.CTkFrame(card, fg_color="transparent") - actrow.pack(fill="x", padx=16, pady=(0, 14)) - ui_kit.secondary_button(actrow, p, "⬇ Exporter", command=lambda: None).pack(side="left", padx=(0, 8)) - ui_kit.secondary_button(actrow, p, "⬆ Importer", command=lambda: None).pack(side="left") + export = ui_kit.Card(self._body, p, title="📤 Exporter la configuration") + export.pack(fill="x", padx=14, pady=7) + self._note(export, "Génère un fichier .json avec vos listes, à envoyer par e-mail à d'autres établissements.") + ui_kit.secondary_button(export, p, "⬇ Exporter (.json)", command=lambda: None).pack(anchor="w", padx=16, pady=(0, 14)) + + import_card = ui_kit.Card(self._body, p, title="📥 Importer une configuration") + import_card.pack(fill="x", padx=14, pady=7) + self._note(import_card, "Importez un fichier reçu. Vos réglages locaux ne seront pas supprimés.") + ui_kit.secondary_button(import_card, p, "⬆ Importer (.json)", command=lambda: None).pack(anchor="w", padx=16, pady=(0, 14)) def _build_regles(self) -> None: p = self._p - card = ui_kit.Card(self._body, p, title="🛡️ Règles personnalisées") + card = ui_kit.Card(self._body, p, title="🛡️ Règles actives") card.pack(fill="x", padx=14, pady=7) - ctk.CTkLabel(card, text="Règles système (lecture seule) et règles personnalisées de l'établissement.", - text_color=p["text_dim"], font=ui_kit.font(12), anchor="w", justify="left").pack(anchor="w", padx=16, pady=(0, 14)) + self._note(card, "Ces règles adaptent le moteur à votre établissement. Chaque règle est validée avant activation.") + headers = ctk.CTkFrame(card, fg_color="transparent") + headers.pack(fill="x", padx=16, pady=(0, 4)) + for text, width in [("Label", 190), ("Type", 80), ("Cible → Résultat", 210), ("Statut", 70), ("", 70)]: + ctk.CTkLabel(headers, text=text.upper(), width=width, anchor="w", text_color=p["text_muted"], font=ui_kit.font(10, "bold")).pack(side="left") + for row in [ + ("Masquer le sigle CHUXX", "exact", "CHUXX → [MASK]", "Actif"), + ("Préserver “classification internationale”", "preserve", "conservé tel quel", "Actif"), + ("Identifier N° 1234567", "norm-id", "N° 1234567 → [NDA]", "Candidat"), + ]: + self._rule_row(card, row) + actions = ctk.CTkFrame(card, fg_color="transparent") + actions.pack(fill="x", padx=16, pady=(10, 14)) + ui_kit.primary_button(actions, p, "+ Nouvelle règle", command=lambda: None).pack(side="left", padx=(0, 8)) + ui_kit.secondary_button(actions, p, "🔄 Recharger", command=lambda: None).pack(side="left") + + sim = ui_kit.Card(self._body, p, title="🧪 Testeur de règle") + sim.pack(fill="x", padx=14, pady=7) + ctk.CTkLabel(sim, text="Texte de test", text_color=p["text_muted"], font=ui_kit.font(12)).pack(anchor="w", padx=16) + txt = ctk.CTkTextbox(sim, height=78, fg_color=p["divider"], text_color=p["text"], border_color=p["card_border"], border_width=1) + txt.pack(fill="x", padx=16, pady=(5, 10)) + txt.insert("1.0", "Compte rendu CHUXX, patient N° 1234567.") + btns = ctk.CTkFrame(sim, fg_color="transparent") + btns.pack(fill="x", padx=16, pady=(0, 14)) + ui_kit.primary_button(btns, p, "▶ Tester", command=lambda: None).pack(side="left", padx=(0, 8)) + ui_kit.secondary_button(btns, p, "✖ Fermer", command=lambda: None).pack(side="left") # -- callbacks -------------------------------------------------------- @@ -168,3 +324,96 @@ class ConfigTab(ctk.CTkFrame): if path: self._state.output_dir = Path(path) self._out_label.configure(text=str(self._state.output_dir)) + + # -- helpers UI ------------------------------------------------------- + + def _two_columns(self, parent) -> tuple[ctk.CTkFrame, ctk.CTkFrame]: + row = ctk.CTkFrame(parent, fg_color="transparent") + row.pack(fill="both", expand=True, padx=14) + left = ctk.CTkFrame(row, fg_color="transparent") + right = ctk.CTkFrame(row, fg_color="transparent") + left.pack(side="left", fill="both", expand=True, padx=(0, 7)) + right.pack(side="left", fill="both", expand=True, padx=(7, 0)) + return left, right + + def _note(self, parent, text: str) -> None: + p = self._p + ctk.CTkLabel( + parent, + text=text, + text_color=p["text_muted"], + fg_color=p["divider"], + corner_radius=4, + font=ui_kit.font(11), + anchor="w", + justify="left", + wraplength=330, + ).pack(fill="x", padx=16, pady=(0, 10), ipady=5) + + def _tag_editor(self, parent, placeholder: str, terms: list[str], kind: str) -> None: + p = self._p + row = ctk.CTkFrame(parent, fg_color="transparent") + row.pack(fill="x", padx=16, pady=(0, 8)) + entry = ctk.CTkEntry( + row, + placeholder_text=placeholder, + fg_color=p["btn_sec_bg"], + border_color=p["btn_sec_border"], + text_color=p["text"], + height=32, + ) + entry.pack(side="left", fill="x", expand=True, padx=(0, 8)) + ui_kit.secondary_button(row, p, "+ Ajouter", command=lambda: None).pack(side="right") + cloud = ctk.CTkFrame(parent, fg_color="transparent") + cloud.pack(fill="x", padx=16, pady=(0, 14)) + tag_color = p["success"] if kind == "keep" else p["primary"] + line = ctk.CTkFrame(cloud, fg_color="transparent") + line.pack(fill="x", anchor="w") + used_width = 0 + for term in terms: + estimated_width = min(230, max(88, len(term) * 8 + 34)) + if used_width and used_width + estimated_width > 360: + line = ctk.CTkFrame(cloud, fg_color="transparent") + line.pack(fill="x", anchor="w") + used_width = 0 + ctk.CTkLabel( + line, + text=f"{term[:30]}{'…' if len(term) > 30 else ''} ×", + width=estimated_width, + text_color=tag_color, + fg_color=p["btn_sec_bg"], + corner_radius=99, + font=ui_kit.font(12), + ).pack(side="left", padx=(0, 6), pady=3, ipadx=4, ipady=3) + used_width += estimated_width + 6 + + def _slider_row(self, parent, label: str, value: int) -> None: + p = self._p + row = ctk.CTkFrame(parent, fg_color="transparent") + row.pack(fill="x", padx=16, pady=3) + ctk.CTkLabel(row, text=label, text_color=p["text"], font=ui_kit.font(13)).pack(side="left") + slider = ctk.CTkSlider(row, from_=0, to=6, number_of_steps=6, progress_color=p["primary"], width=125) + slider.set(value) + slider.pack(side="right") + + def _update_mask_preview(self) -> None: + value = self._mask_style_var.get() + if value == "stars": + text = "Patient ***, né le ***" + elif value == "blackout": + text = "Patient ████, né le ████" + else: + text = "Patient [NOM], né le [DATE_NAISSANCE]" + self._mask_preview.configure(text=text) + + def _rule_row(self, parent, values: tuple[str, str, str, str]) -> None: + p = self._p + label, rule_type, target, status = values + row = ctk.CTkFrame(parent, fg_color="transparent") + row.pack(fill="x", padx=16, pady=1) + ctk.CTkLabel(row, text=label, width=190, anchor="w", text_color=p["text"], font=ui_kit.font(12)).pack(side="left") + ctk.CTkLabel(row, text=rule_type, width=80, anchor="w", text_color=p["text_dim"], font=ui_kit.font(11)).pack(side="left") + ctk.CTkLabel(row, text=target, width=210, anchor="w", text_color=p["primary"], font=ui_kit.font(12, "bold")).pack(side="left") + color = p["success"] if status == "Actif" else p["warning"] + ctk.CTkLabel(row, text=status, width=70, anchor="w", text_color=color, font=ui_kit.font(11, "bold")).pack(side="left") + ui_kit.secondary_button(row, p, "▶ Tester", command=lambda: None).pack(side="left") diff --git a/tests/unit/test_gui_v6_config_mockup_sections.py b/tests/unit/test_gui_v6_config_mockup_sections.py new file mode 100644 index 0000000..ae49326 --- /dev/null +++ b/tests/unit/test_gui_v6_config_mockup_sections.py @@ -0,0 +1,26 @@ +"""Garde-fou : l'onglet Configuration doit couvrir les sections de la maquette V6.""" + +from __future__ import annotations + +from gui_v6.tabs.tab_config import CONFIG_MOCKUP_SECTIONS + + +def test_config_mockup_sections_cover_admin_surface(): + assert CONFIG_MOCKUP_SECTIONS == { + "reglages": [ + "Profil métier", + "Moteurs NER", + "Données à détecter", + "Termes à toujours conserver", + "Termes à toujours masquer", + ], + "masquage": [ + "Couleur de masquage (PDF)", + "Style des marqueurs (texte)", + "Épaisseur du masque", + "Codes de remplacement", + "Masques de zones fixes", + ], + "partage": ["Exporter la configuration", "Importer une configuration"], + "regles": ["Règles actives", "Testeur de règle"], + }