v1.0.0 — GSPARC Mezzouna API initiale

This commit is contained in:
Nabil Derouiche
2026-05-28 18:48:40 +00:00
commit cc143fd321
30 changed files with 1241 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
"""Routes de l'application GSPARC Mezzouna."""
from fastapi import APIRouter
from app.routes.auth_routes import router as auth_router
from app.routes.vehicles import router as vehicles_router
from app.routes.fuel import router as fuel_router
from app.routes.anomalies import router as anomalies_router
from app.routes.dashboard import router as dashboard_router
# Router principal qui regroupe tout
api_router = APIRouter()
api_router.include_router(dashboard_router)
api_router.include_router(vehicles_router)
api_router.include_router(fuel_router)
api_router.include_router(anomalies_router)
+34
View File
@@ -0,0 +1,34 @@
"""Routes — anomalies."""
from fastapi import APIRouter, Depends, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from app.auth import require_auth
from app.baserow import list_rows, update_row
from app.templates import templates
from app.config import TABLE_ANOMALIES, TABLE_VEHICULES
router = APIRouter(prefix="/anomalies")
@router.get("/", response_class=HTMLResponse)
async def liste_anomalies(request: Request, user=Depends(require_auth)):
anomalies = await list_rows(TABLE_ANOMALIES, size=100)
vehicules = await list_rows(TABLE_VEHICULES)
return templates.TemplateResponse("anomalies.html", {
"request": request,
"anomalies": anomalies,
"vehicules": {v["رقم_الماتريكول"]: v["النوع"] for v in vehicules},
})
@router.post("/{row_id}/resolve")
async def resoudre_anomalie(
request: Request,
row_id: int,
action: str = Form(""),
user=Depends(require_auth),
):
await update_row(TABLE_ANOMALIES, row_id, {
"تمت_المعالجة": True,
"إجراء_التصحيح": action,
})
return RedirectResponse(url="/anomalies", status_code=303)
+25
View File
@@ -0,0 +1,25 @@
"""Routes — authentification (login/logout pages)."""
from fastapi import APIRouter, Form, Request
from fastapi.responses import RedirectResponse, HTMLResponse
from app.auth import login_user, logout_user
from app.templates import templates
router = APIRouter()
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request, error: str = ""):
return templates.TemplateResponse("login.html", {"request": request, "error": error})
@router.post("/login")
async def login(request: Request, username: str = Form(...), password: str = Form(...)):
if login_user(request, username, password):
return RedirectResponse(url="/", status_code=303)
return templates.TemplateResponse("login.html", {"request": request, "error": "اسم المستخدم أو كلمة المرور غير صحيحة"}, status_code=401)
@router.get("/logout")
async def logout(request: Request):
logout_user(request)
return RedirectResponse(url="/login")
+44
View File
@@ -0,0 +1,44 @@
"""Routes — tableau de bord principal."""
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from app.auth import require_auth
from app.baserow import list_rows
from app.templates import templates
from app.config import TABLE_VEHICULES, TABLE_APPROVISIONNEMENTS, TABLE_ANOMALIES
router = APIRouter()
@router.get("/", response_class=HTMLResponse)
async def dashboard(request: Request, user=Depends(require_auth)):
vehicules = await list_rows(TABLE_VEHICULES)
appros = await list_rows(TABLE_APPROVISIONNEMENTS, size=50)
anomalies = await list_rows(TABLE_ANOMALIES, size=20)
# Stats globales
total_appros = len(appros)
total_anomalies = len([a for a in anomalies if not a.get("تمت_المعالجة")])
total_vehicules = len(vehicules)
# Consommation moyenne par véhicule
stats_vehicules = []
for v in vehicules:
matricule = v.get("رقم_الماتريكول", "")
appros_v = [a for a in appros if a.get("رقم_الماتريكول") == matricule]
conso_values = [a.get("الاستهلاك_100كم") for a in appros_v if a.get("الاستهلاك_100كم")]
conso_moy = round(sum(conso_values) / len(conso_values), 2) if conso_values else 0
stats_vehicules.append({
"matricule": matricule,
"type": v.get("النوع", ""),
"nb_appros": len(appros_v),
"conso_moyenne": conso_moy,
"nb_anomalies": len([a for a in anomalies if a.get("رقم_الماتريكول") == matricule and not a.get("تمت_المعالجة")]),
})
return templates.TemplateResponse("dashboard.html", {
"request": request,
"total_v": total_vehicules,
"total_appro": total_appros,
"total_anomalies": total_anomalies,
"stats_vehicules": stats_vehicules,
})
+88
View File
@@ -0,0 +1,88 @@
"""Routes — approvisionnements en carburant."""
from fastapi import APIRouter, Depends, Request, Form, UploadFile, File
from fastapi.responses import HTMLResponse, RedirectResponse
from app.auth import require_auth
from app.baserow import list_rows, create_row, update_row, get_row
from app.templates import templates
from app.config import TABLE_VEHICULES, TABLE_APPROVISIONNEMENTS, TABLE_ANOMALIES
from app.business import calculer_kilometrage, calculer_consommation, detecter_anomalies
from app.ocr import extract_text, parse_receipt_ocr
router = APIRouter(prefix="/fuel")
@router.get("/", response_class=HTMLResponse)
async def formulaire(request: Request, user=Depends(require_auth)):
vehicules = await list_rows(TABLE_VEHICULES)
appros = await list_rows(TABLE_APPROVISIONNEMENTS, size=50)
return templates.TemplateResponse("fuel_form.html", {
"request": request,
"vehicules": vehicules,
"appros": appros,
})
@router.post("/add")
async def ajouter_approvisionnement(
request: Request,
matricule: str = Form(...),
date: str = Form(...),
produit: str = Form("Gasoual Normal"),
quantite: float = Form(...),
prix_litre: float = Form(...),
valeur: float = Form(...),
compteur_avant: float = Form(None),
compteur_apres: float = Form(None),
no_recu: str = Form(""),
user=Depends(require_auth),
):
# Calcul automatique
km_parcouru = calculer_kilometrage(compteur_avant or 0, compteur_apres or 0)
conso_100 = calculer_consommation(quantite, km_parcouru) if km_parcouru > 0 else None
data = {
"التاريخ": date,
"رقم_الماتريكول": matricule,
"المنتج": produit,
"الكمية_باللتر": quantite,
"السعر_الليتر": prix_litre,
"القيمة_الجملية": valeur,
"العداد_قبل": compteur_avant,
"العداد_بعد": compteur_apres,
"الكلم_المقطوع": km_parcouru,
"الاستهلاك_100كم": conso_100,
"رقم_الايصال": no_recu,
}
# Détection d'anomalies
appro_precedents = await list_rows(TABLE_APPROVISIONNEMENTS, size=100)
dernier = next((a for a in appro_precedents if a.get("رقم_الماتريكول") == matricule), None)
codes = detecter_anomalies(data, dernier)
if codes:
data["شذوذ"] = True
data["نوع_الشذوذ"] = ", ".join(codes)
await create_row(TABLE_APPROVISIONNEMENTS, data)
return RedirectResponse(url="/fuel", status_code=303)
@router.post("/ocr-upload")
async def upload_recu_ocr(
request: Request,
file: UploadFile = File(...),
user=Depends(require_auth),
):
"""Upload d'un reçu + OCR automatique."""
content = await file.read()
text = await extract_text(content, file.filename or "receipt.jpg")
parsed = parse_receipt_ocr(text)
vehicules = await list_rows(TABLE_VEHICULES)
return templates.TemplateResponse("fuel_form.html", {
"request": request,
"vehicules": vehicules,
"appros": [],
"ocr_text": text,
"ocr_parsed": parsed,
})
+62
View File
@@ -0,0 +1,62 @@
"""Routes — véhicules."""
from fastapi import APIRouter, Depends, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from app.auth import require_auth
from app.baserow import list_rows, create_row, update_row, delete_row, get_row
from app.templates import templates
from app.config import TABLE_VEHICULES
router = APIRouter(prefix="/vehicules")
@router.get("/", response_class=HTMLResponse)
async def liste_vehicules(request: Request, user=Depends(require_auth)):
vehicules = await list_rows(TABLE_VEHICULES)
return templates.TemplateResponse("vehicules.html", {"request": request, "vehicules": vehicules})
@router.post("/add")
async def ajouter_vehicule(
request: Request,
matricule: str = Form(...),
type_v: str = Form(...),
marque: str = Form(""),
capacite: float = Form(None),
notes: str = Form(""),
user=Depends(require_auth),
):
await create_row(TABLE_VEHICULES, {
"رقم_الماتريكول": matricule,
"النوع": type_v,
"العلامة": marque,
"حمولة_الخزان": capacite,
"ملاحظات": notes,
})
return RedirectResponse(url="/vehicules", status_code=303)
@router.post("/{row_id}/delete")
async def supprimer_vehicule(request: Request, row_id: int, user=Depends(require_auth)):
await delete_row(TABLE_VEHICULES, row_id)
return RedirectResponse(url="/vehicules", status_code=303)
@router.post("/{row_id}/edit")
async def modifier_vehicule(
request: Request,
row_id: int,
matricule: str = Form(...),
type_v: str = Form(...),
marque: str = Form(""),
capacite: float = Form(None),
notes: str = Form(""),
user=Depends(require_auth),
):
await update_row(TABLE_VEHICULES, row_id, {
"رقم_الماتريكول": matricule,
"النوع": type_v,
"العلامة": marque,
"حمولة_الخزان": capacite,
"ملاحظات": notes,
})
return RedirectResponse(url="/vehicules", status_code=303)