""" Routes pour la gestion des matériels : le cœur du stock. Un "matériel" représente un type d'objet suivi en stock (nom + code-barre optionnel + quantité), pas un exemplaire individuel — voir app/models.py. """ from itertools import groupby from fastapi import APIRouter, Depends, Form from fastapi.requests import Request from fastapi.responses import RedirectResponse from sqlmodel import Session, select from app.categories_arbre import aplatir, categories_racines, ids_sous_arbre from app.database import get_session from app.email_alerts import verifier_et_alerter from app.models import Categorie, Materiel from app.templates_engine import templates router = APIRouter() # Chaque clé de tri associe une fonction de clé de tri Python et un # indicateur d'ordre inverse. "categorie" trie par chemin complet puis nom # : les matériels d'une même catégorie se retrouvent ainsi consécutifs, # ce qui permet de les regrouper sous un même en-tête dans le template # sans logique de regroupement séparée à maintenir. CLES_TRI = { "nom_asc": (lambda m: m.nom.lower(), False), "nom_desc": (lambda m: m.nom.lower(), True), "quantite_asc": (lambda m: m.quantite, False), "quantite_desc": (lambda m: m.quantite, True), "categorie": (lambda m: (m.categorie.chemin().lower(), m.nom.lower()), False), } @router.get("/") @router.get("/materiels") def lister_materiels( request: Request, q: str = "", categorie_id: str = "", stock_bas: bool = False, tri: str = "nom_asc", session: Session = Depends(get_session), ): """Liste les matériels avec recherche, filtre par catégorie (elle et ses sous-catégories), filtre "stock bas uniquement" et tri — tous combinables, indispensable dès que la liste devient longue. Tout est fait côté serveur en Python (pas de pagination, juste des filtres) : à l'échelle d'un stock IT d'un département, ça reste très largement suffisant et évite d'avoir à gérer un état côté client. """ materiels = list(session.exec(select(Materiel)).all()) if q: q_normalise = q.lower() materiels = [ m for m in materiels if q_normalise in m.nom.lower() 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() materiels = [m for m in materiels if m.categorie_id in ids_valides] if stock_bas: materiels = [ m for m in materiels if m.seuil_alerte is not None and m.quantite < m.seuil_alerte ] cle, inverse = CLES_TRI.get(tri, CLES_TRI["nom_asc"]) materiels.sort(key=cle, reverse=inverse) groupes = None if tri == "categorie": groupes = [ (chemin, list(items)) for chemin, items in groupby(materiels, key=lambda m: m.categorie.chemin()) ] categories_aplaties = aplatir(categories_racines(session)) return templates.TemplateResponse( request, "materiels.html", { "materiels": materiels, "groupes": groupes, "categories_aplaties": categories_aplaties, "categorie_selectionnee": categorie_selectionnee, "q": q, "categorie_id": categorie_id, "stock_bas": stock_bas, "tri": tri, }, ) @router.post("/materiels") def creer_materiel( nom: str = Form(...), categorie_id: int = Form(...), code_barre: str = Form(""), quantite: int = Form(0), seuil_alerte: str = Form(""), session: Session = Depends(get_session), ): """Crée un nouveau type de matériel dans le stock.""" materiel = Materiel( nom=nom, categorie_id=categorie_id, code_barre=code_barre or None, quantite=quantite, seuil_alerte=int(seuil_alerte) if seuil_alerte else None, ) session.add(materiel) session.commit() session.refresh(materiel) verifier_et_alerter(materiel, session) return RedirectResponse(url="/materiels", status_code=303) @router.post("/materiels/{materiel_id}/modifier") def modifier_materiel( materiel_id: int, nom: str = Form(...), categorie_id: int = Form(...), code_barre: str = Form(""), quantite: int = Form(0), seuil_alerte: str = Form(""), session: Session = Depends(get_session), ): """Met à jour un matériel existant.""" materiel = session.get(Materiel, materiel_id) if materiel: materiel.nom = nom materiel.categorie_id = categorie_id materiel.code_barre = code_barre or None materiel.quantite = quantite materiel.seuil_alerte = int(seuil_alerte) if seuil_alerte else None session.add(materiel) session.commit() session.refresh(materiel) verifier_et_alerter(materiel, session) return RedirectResponse(url="/materiels", status_code=303) @router.post("/materiels/{materiel_id}/supprimer") def supprimer_materiel(materiel_id: int, session: Session = Depends(get_session)): """Supprime définitivement un matériel du stock.""" materiel = session.get(Materiel, materiel_id) if materiel: session.delete(materiel) session.commit() return RedirectResponse(url="/materiels", status_code=303)