from fastapi import FastAPI, Request, Form, HTTPException, UploadFile, File from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles import sqlite3, os, shutil, uuid from typing import Optional, List app = FastAPI(title="GarageManager") templates = Jinja2Templates(directory="templates") DB_PATH = os.environ.get("DB_PATH", "/data/garage.db") UPLOAD_DIR = os.environ.get("UPLOAD_DIR", "/data/uploads") os.makedirs(UPLOAD_DIR, exist_ok=True) app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads") def get_db(): os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") return conn def init_db(): conn = get_db() conn.executescript(""" CREATE TABLE IF NOT EXISTS vehicules ( id INTEGER PRIMARY KEY AUTOINCREMENT, immatriculation TEXT NOT NULL UNIQUE, marque TEXT NOT NULL, modele TEXT NOT NULL, annee INTEGER, vin TEXT, kilometrage INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS vehicule_infos ( id INTEGER PRIMARY KEY AUTOINCREMENT, vehicule_id INTEGER NOT NULL REFERENCES vehicules(id) ON DELETE CASCADE, type TEXT NOT NULL DEFAULT 'texte' CHECK(type IN ('texte','url','image')), titre TEXT, contenu TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS interventions ( id INTEGER PRIMARY KEY AUTOINCREMENT, vehicule_id INTEGER NOT NULL REFERENCES vehicules(id) ON DELETE CASCADE, description TEXT NOT NULL, statut TEXT NOT NULL DEFAULT 'en_attente' CHECK(statut IN ('en_attente','en_cours','termine')), cout_main_oeuvre REAL DEFAULT 0, technicien TEXT, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS pieces ( id INTEGER PRIMARY KEY AUTOINCREMENT, intervention_id INTEGER NOT NULL REFERENCES interventions(id) ON DELETE CASCADE, nom TEXT NOT NULL, reference TEXT, marque TEXT, prix REAL DEFAULT 0, quantite INTEGER DEFAULT 1, photo TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) conn.commit() conn.close() @app.on_event("startup") async def startup(): init_db() # DASHBOARD @app.get("/", response_class=HTMLResponse) async def dashboard(request: Request): conn = get_db() stats = { "nb_vehicules": conn.execute("SELECT COUNT(*) FROM vehicules").fetchone()[0], "nb_en_attente": conn.execute("SELECT COUNT(*) FROM interventions WHERE statut='en_attente'").fetchone()[0], "nb_en_cours": conn.execute("SELECT COUNT(*) FROM interventions WHERE statut='en_cours'").fetchone()[0], "nb_termine": conn.execute("SELECT COUNT(*) FROM interventions WHERE statut='termine'").fetchone()[0], "cout_pieces": conn.execute("SELECT COALESCE(SUM(p.prix*p.quantite),0) FROM pieces p JOIN interventions i ON i.id=p.intervention_id WHERE i.statut='termine'").fetchone()[0], "cout_mo": conn.execute("SELECT COALESCE(SUM(cout_main_oeuvre),0) FROM interventions WHERE statut='termine'").fetchone()[0], } stats["cout_total"] = stats["cout_pieces"] + stats["cout_mo"] recent = conn.execute(""" SELECT i.id, i.description, i.statut, i.created_at, i.cout_main_oeuvre, v.immatriculation, v.marque, v.modele, v.id as vehicule_id, COALESCE(SUM(p.prix*p.quantite),0) as cout_pieces FROM interventions i JOIN vehicules v ON i.vehicule_id = v.id LEFT JOIN pieces p ON p.intervention_id = i.id GROUP BY i.id ORDER BY i.created_at DESC LIMIT 5 """).fetchall() conn.close() return templates.TemplateResponse("dashboard.html", {"request": request, "stats": stats, "recent": recent, "page": "dashboard"}) # VEHICULES @app.get("/vehicules", response_class=HTMLResponse) async def vehicules_list(request: Request, q: str = ""): conn = get_db() if q: rows = conn.execute( "SELECT v.*, COUNT(i.id) as nb_interventions FROM vehicules v LEFT JOIN interventions i ON i.vehicule_id=v.id WHERE v.immatriculation LIKE ? OR v.marque LIKE ? OR v.modele LIKE ? OR v.vin LIKE ? GROUP BY v.id ORDER BY v.marque", (f"%{q}%", f"%{q}%", f"%{q}%", f"%{q}%") ).fetchall() else: rows = conn.execute("SELECT v.*, COUNT(i.id) as nb_interventions FROM vehicules v LEFT JOIN interventions i ON i.vehicule_id=v.id GROUP BY v.id ORDER BY v.marque").fetchall() conn.close() return templates.TemplateResponse("vehicules.html", {"request": request, "vehicules": rows, "q": q, "page": "vehicules"}) @app.get("/vehicules/new", response_class=HTMLResponse) async def vehicule_new(request: Request): return templates.TemplateResponse("vehicule_form.html", {"request": request, "vehicule": None, "page": "vehicules"}) @app.post("/vehicules/new") async def vehicule_create(immatriculation: str = Form(...), marque: str = Form(...), modele: str = Form(...), annee: Optional[int] = Form(None), vin: str = Form(""), kilometrage: Optional[int] = Form(None)): conn = get_db() conn.execute("INSERT INTO vehicules (immatriculation, marque, modele, annee, vin, kilometrage) VALUES (?,?,?,?,?,?)", (immatriculation.upper().strip(), marque.strip(), modele.strip(), annee, vin.strip(), kilometrage)) conn.commit(); conn.close() return RedirectResponse("/vehicules", status_code=303) @app.get("/vehicules/{id}", response_class=HTMLResponse) async def vehicule_detail(request: Request, id: int): conn = get_db() vehicule = conn.execute("SELECT * FROM vehicules WHERE id=?", (id,)).fetchone() if not vehicule: raise HTTPException(404) interventions = conn.execute(""" SELECT i.*, COALESCE(SUM(p.prix*p.quantite),0) as cout_pieces, COUNT(p.id) as nb_pieces FROM interventions i LEFT JOIN pieces p ON p.intervention_id=i.id WHERE i.vehicule_id=? GROUP BY i.id ORDER BY i.created_at DESC """, (id,)).fetchall() infos = conn.execute("SELECT * FROM vehicule_infos WHERE vehicule_id=? ORDER BY created_at DESC", (id,)).fetchall() conn.close() return templates.TemplateResponse("vehicule_detail.html", {"request": request, "vehicule": vehicule, "interventions": interventions, "infos": infos, "page": "vehicules"}) @app.get("/vehicules/{id}/edit", response_class=HTMLResponse) async def vehicule_edit(request: Request, id: int): conn = get_db() vehicule = conn.execute("SELECT * FROM vehicules WHERE id=?", (id,)).fetchone() conn.close() return templates.TemplateResponse("vehicule_form.html", {"request": request, "vehicule": vehicule, "page": "vehicules"}) @app.post("/vehicules/{id}/edit") async def vehicule_update(id: int, immatriculation: str = Form(...), marque: str = Form(...), modele: str = Form(...), annee: Optional[int] = Form(None), vin: str = Form(""), kilometrage: Optional[int] = Form(None)): conn = get_db() conn.execute("UPDATE vehicules SET immatriculation=?,marque=?,modele=?,annee=?,vin=?,kilometrage=? WHERE id=?", (immatriculation.upper().strip(), marque.strip(), modele.strip(), annee, vin.strip(), kilometrage, id)) conn.commit(); conn.close() return RedirectResponse(f"/vehicules/{id}", status_code=303) @app.post("/vehicules/{id}/delete") async def vehicule_delete(id: int): conn = get_db() conn.execute("DELETE FROM vehicules WHERE id=?", (id,)) conn.commit(); conn.close() return RedirectResponse("/vehicules", status_code=303) # VEHICULE INFOS @app.post("/vehicules/{id}/infos/add") async def vehicule_info_add(id: int, type: str = Form("texte"), titre: str = Form(""), contenu: str = Form(...), photo: UploadFile = File(None)): conn = get_db() contenu_final = contenu.strip() if type == "image" and photo and photo.filename: ext = os.path.splitext(photo.filename)[1] filename = f"{uuid.uuid4()}{ext}" path = os.path.join(UPLOAD_DIR, filename) with open(path, "wb") as f: shutil.copyfileobj(photo.file, f) contenu_final = f"/uploads/{filename}" conn.execute("INSERT INTO vehicule_infos (vehicule_id, type, titre, contenu) VALUES (?,?,?,?)", (id, type, titre.strip(), contenu_final)) conn.commit(); conn.close() return RedirectResponse(f"/vehicules/{id}", status_code=303) @app.post("/vehicules/{vid}/infos/{iid}/delete") async def vehicule_info_delete(vid: int, iid: int): conn = get_db() conn.execute("DELETE FROM vehicule_infos WHERE id=? AND vehicule_id=?", (iid, vid)) conn.commit(); conn.close() return RedirectResponse(f"/vehicules/{vid}", status_code=303) # INTERVENTIONS @app.get("/interventions", response_class=HTMLResponse) async def interventions_list(request: Request, statut: str = ""): conn = get_db() if statut: rows = conn.execute(""" SELECT i.*, v.immatriculation, v.marque, v.modele, COALESCE(SUM(p.prix*p.quantite),0) as cout_pieces FROM interventions i JOIN vehicules v ON v.id=i.vehicule_id LEFT JOIN pieces p ON p.intervention_id=i.id WHERE i.statut=? GROUP BY i.id ORDER BY i.created_at DESC """, (statut,)).fetchall() else: rows = conn.execute(""" SELECT i.*, v.immatriculation, v.marque, v.modele, COALESCE(SUM(p.prix*p.quantite),0) as cout_pieces FROM interventions i JOIN vehicules v ON v.id=i.vehicule_id LEFT JOIN pieces p ON p.intervention_id=i.id GROUP BY i.id ORDER BY i.created_at DESC """).fetchall() conn.close() return templates.TemplateResponse("interventions.html", {"request": request, "interventions": rows, "statut": statut, "page": "interventions"}) @app.get("/interventions/new", response_class=HTMLResponse) async def intervention_new(request: Request, vehicule_id: Optional[int] = None): conn = get_db() vehicules = conn.execute("SELECT id, immatriculation||' - '||marque||' '||modele as label FROM vehicules ORDER BY marque").fetchall() conn.close() return templates.TemplateResponse("intervention_form.html", {"request": request, "intervention": None, "vehicules": vehicules, "preselect_vehicule": vehicule_id, "page": "interventions"}) @app.post("/interventions/new") async def intervention_create(vehicule_id: int = Form(...), description: str = Form(...), statut: str = Form("en_attente"), cout_main_oeuvre: float = Form(0), technicien: str = Form(""), notes: str = Form("")): conn = get_db() conn.execute("INSERT INTO interventions (vehicule_id, description, statut, cout_main_oeuvre, technicien, notes) VALUES (?,?,?,?,?,?)", (vehicule_id, description.strip(), statut, cout_main_oeuvre, technicien.strip(), notes.strip())) conn.commit() iid = conn.execute("SELECT last_insert_rowid()").fetchone()[0] conn.close() return RedirectResponse(f"/interventions/{iid}", status_code=303) @app.get("/interventions/{id}", response_class=HTMLResponse) async def intervention_detail(request: Request, id: int): conn = get_db() intervention = conn.execute(""" SELECT i.*, v.immatriculation, v.marque, v.modele, v.id as vid, COALESCE(SUM(p.prix*p.quantite),0) as cout_pieces FROM interventions i JOIN vehicules v ON v.id=i.vehicule_id LEFT JOIN pieces p ON p.intervention_id=i.id WHERE i.id=? GROUP BY i.id """, (id,)).fetchone() if not intervention: raise HTTPException(404) pieces = conn.execute("SELECT * FROM pieces WHERE intervention_id=? ORDER BY created_at", (id,)).fetchall() conn.close() return templates.TemplateResponse("intervention_detail.html", {"request": request, "intervention": intervention, "pieces": pieces, "page": "interventions"}) @app.get("/interventions/{id}/edit", response_class=HTMLResponse) async def intervention_edit(request: Request, id: int): conn = get_db() intervention = conn.execute("SELECT * FROM interventions WHERE id=?", (id,)).fetchone() vehicules = conn.execute("SELECT id, immatriculation||' - '||marque||' '||modele as label FROM vehicules ORDER BY marque").fetchall() conn.close() return templates.TemplateResponse("intervention_form.html", {"request": request, "intervention": intervention, "vehicules": vehicules, "preselect_vehicule": None, "page": "interventions"}) @app.post("/interventions/{id}/edit") async def intervention_update(id: int, vehicule_id: int = Form(...), description: str = Form(...), statut: str = Form("en_attente"), cout_main_oeuvre: float = Form(0), technicien: str = Form(""), notes: str = Form("")): conn = get_db() conn.execute("UPDATE interventions SET vehicule_id=?,description=?,statut=?,cout_main_oeuvre=?,technicien=?,notes=?,updated_at=CURRENT_TIMESTAMP WHERE id=?", (vehicule_id, description.strip(), statut, cout_main_oeuvre, technicien.strip(), notes.strip(), id)) conn.commit(); conn.close() return RedirectResponse(f"/interventions/{id}", status_code=303) @app.post("/interventions/{id}/delete") async def intervention_delete(id: int): conn = get_db() row = conn.execute("SELECT vehicule_id FROM interventions WHERE id=?", (id,)).fetchone() vid = row["vehicule_id"] if row else None conn.execute("DELETE FROM interventions WHERE id=?", (id,)) conn.commit(); conn.close() return RedirectResponse(f"/vehicules/{vid}" if vid else "/vehicules", status_code=303) # PIECES @app.post("/interventions/{id}/pieces/add") async def piece_add(id: int, nom: str = Form(...), reference: str = Form(""), marque: str = Form(""), prix: float = Form(0), quantite: int = Form(1), photo: UploadFile = File(None)): conn = get_db() photo_path = None if photo and photo.filename: ext = os.path.splitext(photo.filename)[1] filename = f"{uuid.uuid4()}{ext}" path = os.path.join(UPLOAD_DIR, filename) with open(path, "wb") as f: shutil.copyfileobj(photo.file, f) photo_path = f"/uploads/{filename}" conn.execute("INSERT INTO pieces (intervention_id, nom, reference, marque, prix, quantite, photo) VALUES (?,?,?,?,?,?,?)", (id, nom.strip(), reference.strip(), marque.strip(), prix, quantite, photo_path)) conn.commit(); conn.close() return RedirectResponse(f"/interventions/{id}", status_code=303) @app.post("/interventions/{iid}/pieces/{pid}/delete") async def piece_delete(iid: int, pid: int): conn = get_db() conn.execute("DELETE FROM pieces WHERE id=? AND intervention_id=?", (pid, iid)) conn.commit(); conn.close() return RedirectResponse(f"/interventions/{iid}", status_code=303)