feat: add homepage
This commit is contained in:
@@ -2,13 +2,18 @@
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, File, HTTPException, UploadFile
|
||||
from fastapi import FastAPI, File, HTTPException, UploadFile, Depends
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import __version__
|
||||
from ..extractor import extract_compte_rendu
|
||||
from ..database import init_db, get_session, DatabaseService
|
||||
from ..database.service import DuplicateDocumentError
|
||||
|
||||
app = FastAPI(
|
||||
title="Plesna Gérance API",
|
||||
@@ -20,6 +25,46 @@ app = FastAPI(
|
||||
)
|
||||
|
||||
|
||||
# Initialize database on startup
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Initialize database on application startup."""
|
||||
init_db()
|
||||
|
||||
|
||||
# Pydantic models for API
|
||||
class SaveRequest(BaseModel):
|
||||
"""Request body for saving extracted data."""
|
||||
|
||||
source_file: str | None = None
|
||||
data: dict[str, Any]
|
||||
|
||||
|
||||
class SaveResponse(BaseModel):
|
||||
"""Response for save operation."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
document_id: int | None = None
|
||||
reference: str | None = None
|
||||
date: str | None = None
|
||||
|
||||
|
||||
class DocumentSummary(BaseModel):
|
||||
"""Summary of a saved document."""
|
||||
|
||||
id: int
|
||||
reference: str
|
||||
date: str
|
||||
type: str | None
|
||||
source_file: str | None
|
||||
immeuble_code: str | None
|
||||
immeuble_adresse: str | None
|
||||
solde_montant: float | None
|
||||
solde_type: str | None
|
||||
created_at: str
|
||||
|
||||
|
||||
# Determine the frontend dist path
|
||||
FRONTEND_DIST = Path(__file__).parent.parent.parent.parent / "frontend" / "dist"
|
||||
|
||||
@@ -105,6 +150,184 @@ async def extract_pdf(
|
||||
tmp_path.unlink()
|
||||
|
||||
|
||||
@app.post("/api/save", tags=["database"], response_model=SaveResponse)
|
||||
async def save_document(
|
||||
request: SaveRequest,
|
||||
session: Session = Depends(get_session),
|
||||
) -> SaveResponse:
|
||||
"""Sauvegarde les donnees extraites en base de donnees.
|
||||
|
||||
- **source_file**: Nom du fichier PDF original (optionnel)
|
||||
- **data**: Donnees extraites (format identique a la reponse de /api/extract)
|
||||
|
||||
Retourne un message de succes avec l'ID du document cree,
|
||||
ou une erreur si le document existe deja (doublon).
|
||||
"""
|
||||
try:
|
||||
db_service = DatabaseService(session)
|
||||
document = db_service.save_document(
|
||||
data=request.data, source_file=request.source_file
|
||||
)
|
||||
|
||||
return SaveResponse(
|
||||
success=True,
|
||||
message="Document sauvegarde avec succes",
|
||||
document_id=document.id,
|
||||
reference=document.reference,
|
||||
date=str(document.date),
|
||||
)
|
||||
|
||||
except DuplicateDocumentError as e:
|
||||
return SaveResponse(
|
||||
success=False,
|
||||
message=f"Document deja existant: reference={e.reference}, date={e.date}",
|
||||
reference=e.reference,
|
||||
date=str(e.date),
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Erreur lors de la sauvegarde: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/stats", tags=["database"])
|
||||
async def get_stats(
|
||||
session: Session = Depends(get_session),
|
||||
) -> dict:
|
||||
"""Retourne les statistiques globales de la base de donnees.
|
||||
|
||||
Compteurs pour chaque table principale.
|
||||
"""
|
||||
from sqlalchemy import func, select
|
||||
from ..database.models import Document, Immeuble, Lot, Locataire, Revenu, Depense
|
||||
|
||||
return {
|
||||
"documents": session.execute(select(func.count(Document.id))).scalar() or 0,
|
||||
"immeubles": session.execute(select(func.count(Immeuble.id))).scalar() or 0,
|
||||
"lots": session.execute(select(func.count(Lot.id))).scalar() or 0,
|
||||
"locataires": session.execute(select(func.count(Locataire.id))).scalar() or 0,
|
||||
"revenus": session.execute(select(func.count(Revenu.id))).scalar() or 0,
|
||||
"depenses": session.execute(select(func.count(Depense.id))).scalar() or 0,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/documents", tags=["database"])
|
||||
async def list_documents(
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
session: Session = Depends(get_session),
|
||||
) -> list[DocumentSummary]:
|
||||
"""Liste les documents importes en base.
|
||||
|
||||
- **limit**: Nombre maximum de documents a retourner (defaut: 100)
|
||||
- **offset**: Decalage pour la pagination (defaut: 0)
|
||||
|
||||
Retourne une liste de documents avec leurs metadonnees principales.
|
||||
"""
|
||||
db_service = DatabaseService(session)
|
||||
documents = db_service.list_documents(limit=limit, offset=offset)
|
||||
|
||||
return [
|
||||
DocumentSummary(
|
||||
id=doc.id,
|
||||
reference=doc.reference,
|
||||
date=str(doc.date),
|
||||
type=doc.type,
|
||||
source_file=doc.source_file,
|
||||
immeuble_code=doc.immeuble.code if doc.immeuble else None,
|
||||
immeuble_adresse=doc.immeuble.adresse if doc.immeuble else None,
|
||||
solde_montant=doc.solde_montant,
|
||||
solde_type=doc.solde_type,
|
||||
created_at=doc.created_at.isoformat() if doc.created_at else None,
|
||||
)
|
||||
for doc in documents
|
||||
]
|
||||
|
||||
|
||||
@app.get("/api/documents/{document_id}", tags=["database"])
|
||||
async def get_document(
|
||||
document_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
) -> dict:
|
||||
"""Recupere un document par son ID avec toutes ses donnees.
|
||||
|
||||
Retourne le document complet incluant le JSON original.
|
||||
"""
|
||||
db_service = DatabaseService(session)
|
||||
document = db_service.get_document_by_id(document_id)
|
||||
|
||||
if not document:
|
||||
raise HTTPException(status_code=404, detail="Document non trouve")
|
||||
|
||||
import json
|
||||
|
||||
return {
|
||||
"id": document.id,
|
||||
"reference": document.reference,
|
||||
"date": str(document.date),
|
||||
"type": document.type,
|
||||
"source_file": document.source_file,
|
||||
"immeuble": {
|
||||
"code": document.immeuble.code,
|
||||
"adresse": document.immeuble.adresse,
|
||||
"ville": document.immeuble.ville,
|
||||
"code_postal": document.immeuble.code_postal,
|
||||
}
|
||||
if document.immeuble
|
||||
else None,
|
||||
"solde": {
|
||||
"montant": document.solde_montant,
|
||||
"type": document.solde_type,
|
||||
"date_arrete": str(document.solde_date_arrete)
|
||||
if document.solde_date_arrete
|
||||
else None,
|
||||
},
|
||||
"editeur": {
|
||||
"nom": document.editeur_nom,
|
||||
"siret": document.editeur_siret,
|
||||
},
|
||||
"json_data": json.loads(document.json_data) if document.json_data else None,
|
||||
"created_at": document.created_at.isoformat() if document.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/check-duplicate", tags=["database"])
|
||||
async def check_duplicate(
|
||||
reference: str,
|
||||
date: str,
|
||||
session: Session = Depends(get_session),
|
||||
) -> dict:
|
||||
"""Verifie si un document existe deja en base.
|
||||
|
||||
- **reference**: Reference du document
|
||||
- **date**: Date du document (format YYYY-MM-DD)
|
||||
|
||||
Retourne {exists: true/false, document_id: ...}
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
doc_date = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Format de date invalide. Utiliser YYYY-MM-DD"
|
||||
)
|
||||
|
||||
db_service = DatabaseService(session)
|
||||
existing = db_service.check_duplicate(reference, doc_date)
|
||||
|
||||
return {
|
||||
"exists": existing is not None,
|
||||
"document_id": existing.id if existing else None,
|
||||
"reference": reference,
|
||||
"date": date,
|
||||
}
|
||||
|
||||
|
||||
# Mount static files for production (if dist exists)
|
||||
if FRONTEND_DIST.exists():
|
||||
# Serve static assets
|
||||
|
||||
Reference in New Issue
Block a user