fix(gui): complete V6 admin configuration mockup

This commit is contained in:
2026-06-15 09:19:43 +02:00
parent 873fd5622a
commit 269b9e0e13
2 changed files with 316 additions and 41 deletions

View File

@@ -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")

View File

@@ -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"],
}