diff --git a/CLAUDE.md b/CLAUDE.md index e087efa..c15bf03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,6 +43,7 @@ qu'aucun SMTP n'est configuré (voir `app/email_alerts.py` et `.env.example`). ```bash source .venv/bin/activate # créer avec: python3 -m venv .venv pip install -r requirements-dev.txt +python -m playwright install chromium # une seule fois : télécharge le navigateur headless (vérif visuelle du rendu CSS) uvicorn app.main:app --reload # lancer le serveur de dev (http://127.0.0.1:8000) pytest # lancer les tests ruff check . # linter diff --git a/app/routers/materiels.py b/app/routers/materiels.py index bbc0456..0405ecd 100644 --- a/app/routers/materiels.py +++ b/app/routers/materiels.py @@ -63,6 +63,7 @@ def lister_materiels( or (m.code_barre and q_normalise in m.code_barre.lower()) ] + categorie_selectionnee = None if categorie_id: categorie_selectionnee = session.get(Categorie, int(categorie_id)) ids_valides = ids_sous_arbre(categorie_selectionnee) if categorie_selectionnee else set() @@ -91,6 +92,7 @@ def lister_materiels( "materiels": materiels, "groupes": groupes, "categories_aplaties": categories_aplaties, + "categorie_selectionnee": categorie_selectionnee, "q": q, "categorie_id": categorie_id, "stock_bas": stock_bas, diff --git a/app/static/style.css b/app/static/style.css index 6f9a5f2..1cea177 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -7,21 +7,112 @@ * le réseau interne de la clinique, potentiellement sans accès internet. * Le "caractère" typographique vient du poids, du tracking et de * l'échelle plutôt que d'une police téléchargée. + * + * Deux couches de tokens : + * - primitives (--papier, --encre, --brique...) : les couleurs brutes, + * identiques en clair/sombre dans leur intention (ex: --brique reste + * "la couleur d'alerte" dans les deux thèmes, juste recalée en teinte). + * - sémantiques (--surface, --texte, --action-primaire...) : ce que les + * composants utilisent réellement, pour ne redéfinir qu'un seul bloc + * en mode sombre (voir prefers-color-scheme plus bas) plutôt que de + * traquer chaque usage individuel dans le fichier. */ :root { - --papier: #f5f3ec; /* fond, blanc "étiquette" chaud */ + --papier: #f5f3ec; /* fond, blanc "étiquette" chaud */ --papier-creux: #ebe8de; /* zones en creux (input, table stripe) */ --encre: #14181c; /* texte principal, quasi noir */ --acier: #5b6470; /* texte secondaire, bordures */ --acier-clair: #d8dbdf; /* bordures discrètes */ --brique: #b8402a; /* alerte stock bas */ + --brique-doux: #f3ded9; /* fond des badges d'alerte */ --labo: #2e7d6b; /* stock ok / validation */ + --labo-doux: #dfeae6; /* fond des badges "ok" */ --ambre: #c67a17; /* accent focus / scan actif */ + /* --encre/--papier sont une paire "texte / fond" qui s'inverse + volontairement en mode sombre (voir plus bas). La nav et le bouton + principal ont besoin de l'effet inverse : rester une barre foncée à + texte clair dans les DEUX thèmes (sinon la nav devient une barre + claire flottant sur une page sombre). D'où cette paire séparée, fixe, + jamais réassignée dans le bloc dark mode. */ + --contraste: #14181c; + --contraste-texte: #f5f3ec; + + /* Couche sémantique — c'est elle que les composants utilisent. */ + --surface: var(--papier); + --surface-creuse: var(--papier-creux); + --surface-carte: #ffffff; + --texte: var(--encre); + --texte-secondaire: var(--acier); + --bordure: var(--acier-clair); + --accent: var(--ambre); + --action-primaire: var(--contraste); + --action-primaire-texte: var(--contraste-texte); + --danger: var(--brique); + --danger-doux: var(--brique-doux); + --succes: var(--labo); + --succes-doux: var(--labo-doux); + + /* Échelle d'espacement : remplace les valeurs rem ad hoc dispersées + dans le reste du fichier, pour garder un rythme vertical cohérent. */ + --espace-1: 0.25rem; + --espace-2: 0.5rem; + --espace-3: 0.75rem; + --espace-4: 1rem; + --espace-5: 1.5rem; + --espace-6: 2.5rem; + + /* Échelle typographique. */ + --texte-xs: 0.75rem; + --texte-sm: 0.85rem; + --texte-base: 1rem; + --texte-lg: 1.3rem; + --texte-xl: 1.75rem; + --texte-2xl: 3rem; + + --rayon: 4px; + --transition: 150ms ease; + --police-corps: -apple-system, "Segoe UI", Roboto, Arial, sans-serif; --police-donnee: ui-monospace, "SF Mono", "Cascadia Code", "Consolas", "Liberation Mono", monospace; + + /* Sans cette déclaration, les contrôles natifs (case à cocher, flèche + de ) restent rendus en + clair par le navigateur même sur une page au thème sombre — c'est ce + qui causait des champs blancs éclatants dans le formulaire "Ajouter + un matériel" en mode sombre système. */ + color-scheme: light dark; +} + +/* + * Mode sombre : suit uniquement la préférence système + * (prefers-color-scheme), pas de bouton de bascule ni de JS — c'est du + * CSS pur, donc coût nul. Utile en pratique pour ce public (salle + * serveur, poste peu éclairé, veille de nuit). On garde les mêmes teintes + * (brique = toujours l'alerte, labo = toujours l'ok) juste recalées pour + * rester lisibles sur fond sombre. + */ +@media (prefers-color-scheme: dark) { + :root { + --papier: #1c1b17; + --papier-creux: #262420; + --encre: #f1efe6; + --acier: #9a9c93; + --acier-clair: #3a3934; + --brique: #e2694c; + --brique-doux: #3c231c; + --labo: #5fbfa4; + --labo-doux: #1d332c; + --ambre: #e0a340; + + --surface-carte: #232119; + /* Un peu plus clair qu'en mode jour pour se détacher d'un fond déjà + sombre, mais reste délibérément foncé (voir commentaire sur + --contraste plus haut). */ + --contraste: #2c2a22; + } } * { @@ -30,11 +121,13 @@ body { font-family: var(--police-corps); + font-size: var(--texte-base); + line-height: 1.5; max-width: 900px; margin: 0 auto 4rem; padding: 0 1rem; - color: var(--encre); - background: var(--papier); + color: var(--texte); + background: var(--surface); } h1, h2 { @@ -42,13 +135,40 @@ h1, h2 { letter-spacing: -0.01em; } +h1 { + font-size: var(--texte-xl); +} + +h2 { + font-size: var(--texte-lg); +} + +/* Lien d'évitement : invisible tant qu'il n'a pas le focus clavier, + permet de sauter directement au contenu sans traverser la nav à chaque + page — utile sur un poste où on navigue beaucoup au clavier/scan. */ +.lien-evitement { + position: absolute; + left: -999px; + top: 0; + z-index: 10; + padding: var(--espace-2) var(--espace-3); + background: var(--accent); + color: var(--contraste); + font-weight: 600; +} + +.lien-evitement:focus { + left: var(--espace-3); + top: var(--espace-3); +} + /* Focus clavier visible partout, indépendamment du style propre à chaque contrôle (accessibilité : ne jamais s'appuyer sur la seule couleur). */ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible { - outline: 2px solid var(--ambre); + outline: 2px solid var(--accent); outline-offset: 2px; } @@ -70,25 +190,48 @@ select:focus-visible { } nav { + position: sticky; + top: 0; + z-index: 5; display: flex; - gap: 1.5rem; + gap: var(--espace-5); align-items: center; margin: 0 -1rem 2.5rem; - padding: 1rem 1rem 0.85rem; - background: var(--encre); + padding: var(--espace-4) 1rem calc(var(--espace-4) - 0.15rem); + background: var(--contraste); } nav a { + display: inline-flex; + align-items: center; + gap: var(--espace-2); text-decoration: none; - color: var(--papier); + color: var(--contraste-texte); font-weight: 600; - font-size: 0.85rem; + font-size: var(--texte-sm); letter-spacing: 0.06em; text-transform: uppercase; + opacity: 0.75; + transition: opacity var(--transition), color var(--transition); +} + +nav a svg { + flex: none; + width: 16px; + height: 16px; } nav a:hover { - color: var(--ambre); + opacity: 1; + color: var(--accent); +} + +/* État actif : calculé dans base.html via request.url.path, exposé ici + par aria-current plutôt qu'une classe, pour que le style et la + sémantique accessible restent une seule et même source de vérité. */ +nav a[aria-current="page"] { + opacity: 1; + color: var(--accent); } /* @@ -111,10 +254,10 @@ details > summary::-webkit-details-marker { flex: none; width: 0.55em; height: 0.55em; - border-right: 2px solid var(--acier); - border-bottom: 2px solid var(--acier); + border-right: 2px solid var(--texte-secondaire); + border-bottom: 2px solid var(--texte-secondaire); transform: rotate(-45deg); - transition: transform 0.15s ease; + transition: transform var(--transition); } details[open] > summary .noeud-chevron { @@ -126,8 +269,8 @@ details[open] > summary .noeud-chevron { display: flex; flex-wrap: wrap; align-items: center; - gap: 0.75rem; - margin-bottom: 1.25rem; + gap: var(--espace-3); + margin-bottom: var(--espace-4); } .barre-recherche input[type="search"], @@ -135,12 +278,12 @@ details[open] > summary .noeud-chevron { flex: 1; min-width: 200px; max-width: 360px; - background: var(--papier-creux); + background: var(--surface-creuse); border: none; } .barre-filtres select { - background: var(--papier-creux); + background: var(--surface-creuse); border: none; } @@ -148,8 +291,8 @@ details[open] > summary .noeud-chevron { display: flex; align-items: center; gap: 0.4rem; - font-size: 0.85rem; - color: var(--acier); + font-size: var(--texte-sm); + color: var(--texte-secondaire); white-space: nowrap; } @@ -158,28 +301,54 @@ details[open] > summary .noeud-chevron { padding: 0; } +/* Compteur de résultats à côté des filtres : feedback simple pour savoir + qu'une recherche/filtre "vide" est bien un résultat, pas un bug. */ +.compteur-resultats { + font-size: var(--texte-sm); + color: var(--texte-secondaire); + white-space: nowrap; +} + +/* Fil d'Ariane de catégorie : apparaît uniquement quand /materiels est + filtré par categorie_id, pour rappeler quel sous-arbre est affiché + (le select seul ne le montre pas une fois le formulaire soumis). */ +.fil-ariane-categorie { + display: flex; + align-items: center; + gap: var(--espace-2); + flex-wrap: wrap; + font-size: var(--texte-sm); + color: var(--texte-secondaire); + margin: -0.5rem 0 var(--espace-4); +} + +.fil-ariane-categorie .chemin-categorie { + font-weight: 600; + color: var(--texte); +} + /* En-tête de groupe quand la liste des matériels est regroupée par catégorie (tri=categorie) : un simple séparateur textuel, pas une carte à part entière, pour rester léger sur une longue liste. */ .groupe-categorie-entete { font-family: var(--police-donnee); - font-size: 0.75rem; + font-size: var(--texte-xs); letter-spacing: 0.05em; text-transform: uppercase; - color: var(--acier); + color: var(--texte-secondaire); padding: 0.9rem 0.5rem 0.3rem; } .lien-effacer { - font-size: 0.85rem; - color: var(--acier); + font-size: var(--texte-sm); + color: var(--texte-secondaire); white-space: nowrap; } .liste-vide { - color: var(--acier); + color: var(--texte-secondaire); font-style: italic; - padding: 1rem 0.25rem; + padding: var(--espace-4) 0.25rem; } /* Liste plate (résultats de recherche de catégories, destinataires). */ @@ -187,16 +356,16 @@ details[open] > summary .noeud-chevron { list-style: none; margin: 0 0 1.5rem; padding: 0; - border-top: 1px solid var(--acier-clair); + border-top: 1px solid var(--bordure); } .liste-resultats li { display: flex; justify-content: space-between; align-items: center; - gap: 1rem; + gap: var(--espace-4); padding: 0.6rem 0.25rem; - border-bottom: 1px solid var(--acier-clair); + border-bottom: 1px solid var(--bordure); } .chemin-categorie { @@ -207,10 +376,32 @@ details[open] > summary .noeud-chevron { white-space: nowrap; } -/* Planification de l'email récapitulatif (/destinataires) : un select de - raccourcis courants et un champ libre pour une expression cron - personnalisée, tous deux visibles en même temps plutôt que de basculer - l'un/l'autre en JavaScript. */ +/* Carte de réglage : regroupe visuellement plusieurs formulaires qui + modifient le même paramètre (ex: planification du résumé de stock, qui + se règle soit par raccourci, soit en cron libre) pour qu'ils se lisent + comme un seul réglage à deux modes de saisie plutôt que deux blocs + indépendants. */ +.carte-reglage { + background: var(--surface-carte); + border: 1px solid var(--bordure); + border-radius: var(--rayon); + padding: var(--espace-4) var(--espace-5); + margin: var(--espace-4) 0 var(--espace-5); + display: flex; + flex-direction: column; + gap: var(--espace-3); +} + +.carte-reglage .formulaire-planification, +.carte-reglage > form { + margin: 0; +} + +.texte-secondaire { + margin: 0; + color: var(--texte-secondaire); +} + .formulaire-planification select { min-width: 260px; } @@ -220,6 +411,53 @@ details[open] > summary .noeud-chevron { min-width: 200px; } +/* --------------------------------------------------------------------- + Badges de statut : généralisation du "point + libellé" déjà utilisé sur + l'écran de scan (.statut-puce), réutilisé aussi dans la liste des + matériels pour que le stock bas se voie au premier coup d'œil sans + dépendre uniquement d'une couleur de texte. + ------------------------------------------------------------------ */ + +.badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.15rem 0.55rem; + border-radius: 999px; + font-family: var(--police-corps); + font-size: var(--texte-xs); + font-weight: 600; + white-space: nowrap; +} + +.badge-bas { + background: var(--danger-doux); + color: var(--danger); +} + +.badge-ok { + background: var(--succes-doux); + color: var(--succes); +} + +.statut-puce { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + flex: none; +} + +.badge-bas .statut-puce, +.statut-puce.statut-bas { + background: var(--danger); +} + +.badge-ok .statut-puce, +.statut-puce.statut-ok { + background: var(--succes); +} + /* --------------------------------------------------------------------- Arborescence de catégories : profondeur illimitée, repliée par défaut. ------------------------------------------------------------------ */ @@ -234,7 +472,7 @@ details[open] > summary .noeud-chevron { .arbre-categories ul { margin-left: 0.9rem; padding-left: 0.85rem; - border-left: 1px solid var(--acier-clair); + border-left: 1px solid var(--bordure); } .noeud-categorie { @@ -246,11 +484,12 @@ details[open] > summary .noeud-chevron { align-items: center; gap: 0.6rem; padding: 0.4rem 0.5rem; - border-radius: 4px; + border-radius: var(--rayon); + transition: background var(--transition); } .noeud-categorie > details > summary:hover { - background: var(--papier-creux); + background: var(--surface-creuse); } .noeud-nom { @@ -259,20 +498,20 @@ details[open] > summary .noeud-chevron { .noeud-compte { font-family: var(--police-donnee); - font-size: 0.75rem; - color: var(--acier); + font-size: var(--texte-xs); + color: var(--texte-secondaire); } .noeud-actions { display: flex; flex-wrap: wrap; - gap: 0.75rem; + gap: var(--espace-3); align-items: center; padding: 0.4rem 0.5rem 0.6rem 1.9rem; } .formulaire-racine { - margin-bottom: 1.5rem; + margin-bottom: var(--espace-5); } /* --------------------------------------------------------------------- @@ -286,23 +525,24 @@ details[open] > summary .noeud-chevron { list-style: none; margin: 0 0 2rem; padding: 0; - border-top: 1px solid var(--acier-clair); + border-top: 1px solid var(--bordure); } .liste-materiels > li { - border-bottom: 1px solid var(--acier-clair); + border-bottom: 1px solid var(--bordure); } .ligne-materiel > summary { display: grid; grid-template-columns: auto minmax(0, 2fr) minmax(0, 1.3fr) minmax(0, 1fr) auto; align-items: center; - gap: 0.75rem; + gap: var(--espace-3); padding: 0.6rem 0.5rem; + transition: background var(--transition); } .ligne-materiel > summary:hover { - background: var(--papier-creux); + background: var(--surface-creuse); } .materiel-nom, @@ -320,7 +560,7 @@ details[open] > summary .noeud-chevron { .materiel-categorie, .materiel-code { font-size: 0.8rem; - color: var(--acier); + color: var(--texte-secondaire); } .materiel-code { @@ -328,6 +568,10 @@ details[open] > summary .noeud-chevron { } .materiel-quantite { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.4rem; font-family: var(--police-donnee); font-weight: 700; text-align: right; @@ -335,13 +579,48 @@ details[open] > summary .noeud-chevron { } .materiel-quantite.quantite-basse { - color: var(--brique); + color: var(--danger); +} + +/* Sous ~640px, la grille 5 colonnes de la ligne de matériel n'a plus la + place de rester sur une seule ligne (le nom et la catégorie se + compressent trop). On repasse sur 2 lignes : nom + quantité au-dessus, + catégorie + code en dessous, en pleine largeur. */ +@media (max-width: 640px) { + .ligne-materiel > summary { + grid-template-columns: auto 1fr auto; + grid-template-areas: + "chevron nom quantite" + "chevron meta meta"; + row-gap: 0.2rem; + } + + .noeud-chevron { + grid-area: chevron; + } + + .materiel-nom { + grid-area: nom; + } + + .materiel-quantite { + grid-area: quantite; + } + + .materiel-categorie { + grid-area: meta; + } + + .materiel-code { + grid-area: meta; + display: none; + } } .formulaire-edition { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: 0.75rem; + gap: var(--espace-3); padding: 0.25rem 0.5rem 1.25rem 1.9rem; } @@ -350,55 +629,80 @@ details[open] > summary .noeud-chevron { flex-direction: column; gap: 0.3rem; font-size: 0.8rem; - color: var(--acier); + color: var(--texte-secondaire); } .formulaire-actions { display: flex; - gap: 0.5rem; + gap: var(--espace-2); align-items: end; } .formulaire-inline { display: inline-flex; - gap: 0.5rem; + gap: var(--espace-2); align-items: center; } .formulaire-grille { display: grid; grid-template-columns: 1fr; - gap: 0.75rem; + gap: var(--espace-3); max-width: 400px; } -.bouton-danger { - background: color-mix(in srgb, var(--brique) 12%, white); - color: var(--brique); - border: 1px solid color-mix(in srgb, var(--brique) 40%, white); -} - -.message-erreur { - color: var(--brique); -} - input, select, button { font-family: inherit; font-size: 1rem; padding: 0.5rem 0.6rem; - border: 1px solid var(--acier-clair); - border-radius: 4px; + border: 1px solid var(--bordure); + border-radius: var(--rayon); + background: var(--surface-carte); + color: var(--texte); + transition: border-color var(--transition), background var(--transition), color var(--transition); } button { cursor: pointer; border-color: var(--encre); - background: white; + background: var(--surface-carte); + color: var(--texte); } button:hover { - background: var(--encre); - color: var(--papier); + background: var(--surface-creuse); +} + +/* Bouton principal : réservé à l'action la plus importante d'un + formulaire (créer, enregistrer). Les autres actions (filtrer, + réinitialiser, ajouter un enfant...) restent au style neutre par + défaut pour que le principal se distingue vraiment. */ +.bouton-primaire { + background: var(--action-primaire); + color: var(--action-primaire-texte); + border-color: var(--action-primaire); + font-weight: 600; +} + +.bouton-primaire:hover { + background: var(--accent); + border-color: var(--accent); + color: var(--contraste); +} + +.bouton-danger { + background: color-mix(in srgb, var(--danger) 12%, var(--surface-carte)); + color: var(--danger); + border: 1px solid color-mix(in srgb, var(--danger) 40%, var(--surface-carte)); +} + +.bouton-danger:hover { + background: var(--danger); + color: var(--action-primaire-texte); +} + +.message-erreur { + color: var(--danger); } /* --------------------------------------------------------------------- @@ -413,51 +717,51 @@ button:hover { .scanner-entete { text-align: center; - margin-bottom: 2rem; + margin-bottom: var(--espace-6); } .scanner-eyebrow { font-family: var(--police-donnee); - font-size: 0.75rem; + font-size: var(--texte-xs); letter-spacing: 0.15em; text-transform: uppercase; - color: var(--acier); + color: var(--texte-secondaire); margin: 0 0 0.25rem; } .scanner-entete h1 { - font-size: 1.75rem; + font-size: var(--texte-xl); font-weight: 800; - margin: 0 0 1rem; + margin: 0 0 var(--espace-4); } .scanner-formulaire { position: relative; - margin-bottom: 1.5rem; + margin-bottom: var(--espace-5); } .scanner-label { display: block; - font-size: 0.75rem; + font-size: var(--texte-xs); letter-spacing: 0.08em; text-transform: uppercase; - color: var(--acier); + color: var(--texte-secondaire); margin-bottom: 0.4rem; } .scanner-champ { width: 100%; font-family: var(--police-donnee); - font-size: 1.5rem; + font-size: var(--texte-lg); letter-spacing: 0.05em; padding: 0.9rem 1rem; - background: var(--papier-creux); + background: var(--surface-creuse); border: none; - border-radius: 4px 4px 0 0; + border-radius: var(--rayon) var(--rayon) 0 0; } .scanner-champ::placeholder { - color: var(--acier); + color: var(--texte-secondaire); opacity: 0.7; } @@ -469,13 +773,13 @@ button:hover { */ .laser-ligne { height: 3px; - background: var(--acier-clair); - border-radius: 0 0 4px 4px; + background: var(--bordure); + border-radius: 0 0 var(--rayon) var(--rayon); overflow: hidden; } .scanner-champ:focus + .laser-ligne { - background: var(--ambre); + background: var(--accent); animation: balayage-laser 1.6s ease-in-out infinite; } @@ -488,12 +792,12 @@ button:hover { scan trouvé, mais avec un formulaire de création à la place du détail du stock — pour créer le matériel sans quitter l'écran de scan. */ .etiquette-creation .etiquette-nom { - color: var(--brique); + color: var(--danger); } .etiquette-sous-titre { padding: 0 1.25rem 1.25rem; - color: var(--acier); + color: var(--texte-secondaire); } .etiquette-formulaire { @@ -507,9 +811,9 @@ button:hover { * une courte animation d'apparition ("tampon" qui vient de s'imprimer). */ .etiquette-resultat { - background: white; - border: 1px solid var(--acier-clair); - border-radius: 4px; + background: var(--surface-carte); + border: 1px solid var(--bordure); + border-radius: var(--rayon); overflow: hidden; animation: apparition-etiquette 0.25s ease-out; } @@ -532,60 +836,45 @@ button:hover { } .etiquette-fil { - font-size: 0.75rem; + font-size: var(--texte-xs); letter-spacing: 0.06em; text-transform: uppercase; - color: var(--acier); - margin: 1rem 0 0.15rem; + color: var(--texte-secondaire); + margin: var(--espace-4) 0 0.15rem; } .etiquette-nom { - font-size: 1.3rem; + font-size: var(--texte-lg); font-weight: 800; - margin: 0 0 1rem; + margin: 0 0 var(--espace-4); } .etiquette-quantite { display: flex; align-items: baseline; - gap: 0.5rem; + gap: var(--espace-2); } .etiquette-nombre { font-family: var(--police-donnee); - font-size: 3rem; + font-size: var(--texte-2xl); font-weight: 700; line-height: 1; } .etiquette-unite { - color: var(--acier); + color: var(--texte-secondaire); } .etiquette-statut { display: flex; align-items: center; gap: 0.4rem; - font-size: 0.85rem; - color: var(--acier); + font-size: var(--texte-sm); + color: var(--texte-secondaire); margin: 0.6rem 0 0; } -.statut-puce { - width: 8px; - height: 8px; - border-radius: 50%; - display: inline-block; -} - -.statut-ok { - background: var(--labo); -} - -.statut-bas { - background: var(--brique); -} - /* * Deux gestes distincts sur le stock : "ajuster" (formulaire-ajustement, * un delta relatif — pas forcément ±1) et "définir" (formulaire-definir, @@ -596,32 +885,32 @@ button:hover { .etiquette-controles { display: flex; flex-direction: column; - gap: 0.75rem; - margin-top: 1.25rem; - padding: 1rem 1.25rem 1.25rem; - background: var(--papier-creux); + gap: var(--espace-3); + margin-top: var(--espace-5); + padding: var(--espace-4) 1.25rem 1.25rem; + background: var(--surface-creuse); } .etiquette-champ-quantite { display: flex; flex-direction: column; gap: 0.3rem; - font-size: 0.75rem; + font-size: var(--texte-xs); letter-spacing: 0.04em; text-transform: uppercase; - color: var(--acier); + color: var(--texte-secondaire); } .etiquette-champ-quantite input { font-family: var(--police-donnee); font-size: 1.1rem; - background: white; + background: var(--surface-carte); } .formulaire-definir { display: flex; align-items: end; - gap: 0.75rem; + gap: var(--espace-3); } .formulaire-definir .etiquette-champ-quantite { @@ -632,8 +921,8 @@ button:hover { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; - background: var(--acier-clair); - margin-top: 0.5rem; + background: var(--bordure); + margin-top: var(--espace-2); } .bouton-scan { @@ -643,12 +932,12 @@ button:hover { padding: 0.9rem; font-weight: 700; letter-spacing: 0.03em; - background: white; + background: var(--surface-carte); } .bouton-scan:hover { - background: var(--papier-creux); - color: var(--encre); + background: var(--surface-creuse); + color: var(--texte); } .bouton-scan:active { @@ -656,11 +945,11 @@ button:hover { } .bouton-reception { - color: var(--labo); + color: var(--succes); } .bouton-sortie { - color: var(--brique); + color: var(--danger); } /* Respecte la préférence système : pas d'animation pour qui la désactive. */ @@ -671,4 +960,7 @@ button:hover { .etiquette-resultat { animation: none; } + * { + transition: none !important; + } } diff --git a/app/templates/base.html b/app/templates/base.html index 9bdd9a2..b4cd6a4 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -7,13 +7,50 @@ + Aller au contenu + + {# + Icônes SVG dessinées à la main (traits simples, currentColor) plutôt + qu'une icon font externe : le poste peut tourner sans accès internet + (voir CLAUDE.md), donc rien qui dépende d'un CDN. + + L'état actif compare request.url.path au préfixe de chaque section : + "request" est injecté automatiquement dans le contexte Jinja par + Starlette (TemplateResponse(request, ...)), donc aucune route n'a + besoin de le transmettre explicitement pour que ça fonctionne ici. + #} + {% set chemin = request.url.path %} -
+
{% block contenu %}{% endblock %}
diff --git a/app/templates/categories.html b/app/templates/categories.html index d3e9c8a..a894aeb 100644 --- a/app/templates/categories.html +++ b/app/templates/categories.html @@ -53,7 +53,7 @@
- +
{% if q %} diff --git a/app/templates/destinataires.html b/app/templates/destinataires.html index 7ecf4f5..a854aad 100644 --- a/app/templates/destinataires.html +++ b/app/templates/destinataires.html @@ -25,46 +25,56 @@
- +

Résumé périodique du stock

-

- Un email récapitulatif (stock bas en premier, puis le reste du stock) est - envoyé automatiquement selon l'horaire ci-dessous, aux destinataires listés - plus haut. - {% if prochaine_execution %} - Prochain envoi : {{ prochaine_execution.strftime("%d/%m/%Y à %H:%M") }}. - {% endif %} -

-{% if erreur_cron %} -

{{ erreur_cron }}

-{% endif %} - -{% set exemples_valeurs = exemples_cron | map(attribute=0) | list %} -
- - -
+

-
- - -
-

- Format cron : minute (0-59) heure (0-23) jour (1-31) mois (1-12) jour de la - semaine. Utilisez les noms de jours (mon, tue, wed, thu, fri, sat, sun), - pas de chiffres — leur numérotation ne suit pas le cron Unix habituel et - prête à confusion. -

+ {% if erreur_cron %} +

{{ erreur_cron }}

+ {% endif %} + + {% set exemples_valeurs = exemples_cron | map(attribute=0) | list %} +
+ + +
+ +
+ + +
+ +

+ Format cron : minute (0-59) heure (0-23) jour (1-31) mois (1-12) jour de la + semaine. Utilisez les noms de jours (mon, tue, wed, thu, fri, sat, sun), + pas de chiffres — leur numérotation ne suit pas le cron Unix habituel et + prête à confusion. +

+ {% endblock %} diff --git a/app/templates/materiels.html b/app/templates/materiels.html index 444ce72..85cc8c6 100644 --- a/app/templates/materiels.html +++ b/app/templates/materiels.html @@ -21,7 +21,10 @@ {{ materiel.nom }} {{ materiel.categorie.chemin() }} {{ materiel.code_barre or "—" }} - {{ materiel.quantite }} + + {{ materiel.quantite }} + {% if sous_seuil %}bas{% endif %} +
@@ -87,8 +90,23 @@ {% if q or categorie_id or stock_bas or tri != "nom_asc" %} Réinitialiser {% endif %} + + {{ materiels|length }} matériel(s)
+{# + Le select de catégorie ci-dessus ne montre plus, une fois soumis, quel + sous-arbre est réellement affiché (juste son nom dans la liste + déroulante). Ce fil d'Ariane le rappelle explicitement, avec un lien + pour revenir à "toutes les catégories". +#} +{% if categorie_selectionnee %} +

+ Filtré sur {{ categorie_selectionnee.chemin() }} + (retirer) +

+{% endif %} +