167 lines
11 KiB
Python
Executable File
167 lines
11 KiB
Python
Executable File
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
from pydantic import BaseModel, validator
|
|
from typing import Optional, List, Dict
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy import text
|
|
import os
|
|
import shutil
|
|
from datetime import date, datetime
|
|
import uuid
|
|
import traceback
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
# MAPPA & DB KONFIG
|
|
UPLOAD_DIR = "/app/frontend/uploads"
|
|
try: os.makedirs(UPLOAD_DIR, exist_ok=True)
|
|
except: pass
|
|
|
|
EXCHANGE_RATES = { "EUR_TO_HUF": 400.0, "HUF_TO_EUR": 0.0025 }
|
|
raw_url = os.getenv("DATABASE_URL")
|
|
if not raw_url: raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
|
|
fixed_url = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
|
|
engine = create_async_engine(fixed_url)
|
|
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
|
|
app = FastAPI(title="Service Finder API")
|
|
|
|
# --- MODELLEK ---
|
|
class IssueReport(BaseModel): vehicle_id: int; description: str; is_critical: bool
|
|
class IssueResolve(BaseModel): vehicle_id: int
|
|
class VehicleRegister(BaseModel):
|
|
model_id: int; vin: str; plate: str; mileage: int; purchase_date: date; role: str = "OWNER"
|
|
class InviteRequest(BaseModel): email: str; role: str; access_level: str
|
|
|
|
# --- SEGÉDEK ---
|
|
async def create_audit_log(session, user_id, event, target_id, details, old_val=None, new_val=None):
|
|
try: await session.execute(text("INSERT INTO data.audit_logs (user_id, event_type, target_id, details, old_value, new_value) VALUES (:uid, :evt, :tid, :det, :old, :new)"), {"uid": user_id, "evt": event, "tid": target_id, "det": details, "old": old_val, "new": new_val})
|
|
except: pass
|
|
|
|
# --- ÚJ VÉGPONT: KÖLTSÉG TÍPUSOK LEKÉRÉSE (DB-BŐL!) ---
|
|
@app.get("/api/ref/cost_types")
|
|
async def get_cost_types():
|
|
async with AsyncSessionLocal() as session:
|
|
# Lekérjük az összes aktív típust
|
|
result = await session.execute(text("SELECT code, name, parent_code FROM ref.cost_types WHERE is_active = TRUE ORDER BY sort_order, name"))
|
|
rows = result.fetchall()
|
|
|
|
# Fát építünk a Frontendnek (Szülő -> Gyerekek)
|
|
tree = {}
|
|
# 1. lépés: Szülők
|
|
for r in rows:
|
|
if r.parent_code is None:
|
|
tree[r.code] = {"label": r.name, "subs": {}}
|
|
|
|
# 2. lépés: Gyerekek
|
|
for r in rows:
|
|
if r.parent_code and r.parent_code in tree:
|
|
tree[r.parent_code]["subs"][r.code] = r.name
|
|
|
|
return tree
|
|
|
|
# --- TÖBBI VÉGPONT (Változatlan, csak a Cost mentésnél validálhatnánk, de most egyszerűsítünk) ---
|
|
|
|
@app.post("/api/add_cost")
|
|
async def add_cost(vehicle_id: int = Form(...), cost_type: str = Form(...), amount: float = Form(...), currency: str = Form(...), mileage: int = Form(...), date_str: str = Form(...), description: str = Form(""), file: UploadFile = File(None)):
|
|
try:
|
|
user_id = 1; document_path = None
|
|
try: real_date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
except: return JSONResponse(status_code=400, content={"detail": "Dátum hiba"})
|
|
|
|
if file:
|
|
try:
|
|
ext = file.filename.split(".")[-1]; u_name = f"{uuid.uuid4()}.{ext}"; f_path = os.path.join(UPLOAD_DIR, u_name)
|
|
with open(f_path, "wb") as b: shutil.copyfileobj(file.file, b)
|
|
document_path = f"uploads/{u_name}"
|
|
except: pass
|
|
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
await session.execute(text("INSERT INTO data.costs (vehicle_id, user_id, cost_type, amount, currency, date, mileage_at_cost, description, document_url) VALUES (:vid, :uid, :type, :amt, :curr, :date, :mil, :desc, :doc)"),
|
|
{"vid": vehicle_id, "uid": user_id, "type": cost_type, "amt": amount, "curr": currency, "date": real_date, "mil": mileage, "desc": description, "doc": document_path})
|
|
res = await session.execute(text("SELECT start_mileage FROM data.vehicle_history WHERE vehicle_id = :vid AND end_date IS NULL"), {"vid": vehicle_id})
|
|
cur = res.scalar() or 0
|
|
if mileage > cur:
|
|
await session.execute(text("UPDATE data.vehicle_history SET start_mileage = :mil WHERE vehicle_id = :vid AND end_date IS NULL"), {"mil": mileage, "vid": vehicle_id})
|
|
await create_audit_log(session, user_id, "ADD_COST", vehicle_id, f"{cost_type}: {amount}")
|
|
return {"status": "success"}
|
|
except Exception as e: return JSONResponse(status_code=500, content={"detail": str(e)})
|
|
|
|
@app.get("/api/vehicle/{vehicle_id}")
|
|
async def get_vehicle_details(vehicle_id: int):
|
|
user_id = 1
|
|
async with AsyncSessionLocal() as session:
|
|
q = text("""SELECT v.id, v.vin, v.current_plate, v.production_year, m.name as brand, mo.model_name, mo.category, vh.role, vh.start_date, vh.start_mileage, u.default_currency, v.status, v.current_issue FROM data.vehicle_history vh JOIN data.vehicles v ON vh.vehicle_id = v.id JOIN ref.vehicle_models mo ON v.model_id = mo.id JOIN ref.vehicle_makes m ON mo.make_id = m.id JOIN data.users u ON vh.user_id = u.id WHERE v.id = :vid AND vh.user_id = :uid AND vh.end_date IS NULL""")
|
|
res = await session.execute(q, {"vid": vehicle_id, "uid": user_id})
|
|
car = res.fetchone()
|
|
if not car: raise HTTPException(status_code=404, detail="Nincs adat")
|
|
curr_year = date.today().year
|
|
costs = (await session.execute(text("SELECT amount, currency FROM data.costs WHERE vehicle_id = :vid AND EXTRACT(YEAR FROM date) = :year"), {"vid": vehicle_id, "year": curr_year})).fetchall()
|
|
total = 0.0
|
|
for c in costs:
|
|
if c.currency == car.default_currency: total += float(c.amount)
|
|
elif c.currency == 'EUR' and car.default_currency == 'HUF': total += float(c.amount) * EXCHANGE_RATES["EUR_TO_HUF"]
|
|
elif c.currency == 'HUF' and car.default_currency == 'EUR': total += float(c.amount) * EXCHANGE_RATES["HUF_TO_EUR"]
|
|
else: total += float(c.amount)
|
|
return {"id": car.id, "brand": car.brand, "model": car.model_name, "plate": car.current_plate, "vin": car.vin, "role": car.role, "start_date": car.start_date, "mileage": car.start_mileage, "currency": car.default_currency, "status": car.status, "current_issue": car.current_issue, "year_cost": total}
|
|
|
|
@app.get("/api/vehicle/{vehicle_id}/history")
|
|
async def get_vehicle_history(vehicle_id: int):
|
|
async with AsyncSessionLocal() as session:
|
|
costs = (await session.execute(text("SELECT id, cost_type as type, amount, currency, date, mileage_at_cost as mileage, description, document_url, 'COST' as source FROM data.costs WHERE vehicle_id = :vid ORDER BY date DESC"), {"vid": vehicle_id})).fetchall()
|
|
logs = (await session.execute(text("SELECT id, event_type as type, 0 as amount, '' as currency, created_at::date as date, 0 as mileage, details as description, '' as document_url, 'LOG' as source FROM data.audit_logs WHERE target_id = :vid AND event_type IN ('ISSUE_REPORT', 'ISSUE_RESOLVED') ORDER BY created_at DESC"), {"vid": vehicle_id})).fetchall()
|
|
combined = [dict(r._mapping) for r in costs] + [dict(r._mapping) for r in logs]
|
|
combined.sort(key=lambda x: x['date'], reverse=True)
|
|
return combined
|
|
|
|
# --- MARADÉK (Register, Fleet stb.) ---
|
|
@app.get("/api/my_vehicles")
|
|
async def get_my_garage():
|
|
async with AsyncSessionLocal() as session:
|
|
res = await session.execute(text("SELECT v.id as vehicle_id, v.vin, v.current_plate, m.name as brand, mo.model_name, mo.category, vh.start_mileage, vh.role, v.status, v.current_issue FROM data.vehicle_history vh JOIN data.vehicles v ON vh.vehicle_id = v.id JOIN ref.vehicle_models mo ON v.model_id = mo.id JOIN ref.vehicle_makes m ON mo.make_id = m.id WHERE vh.user_id = 1 AND vh.end_date IS NULL ORDER BY vh.start_date DESC"))
|
|
return [{"vehicle_id": r.vehicle_id, "brand": r.brand, "model": r.model_name, "plate": r.current_plate, "category": r.category, "role": r.role, "status": r.status, "current_issue": r.current_issue} for r in res.fetchall()]
|
|
@app.post("/api/report_issue")
|
|
async def report_issue(data: IssueReport):
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
await session.execute(text("UPDATE data.vehicles SET status = :st, current_issue = :desc WHERE id = :vid"), {"st": 'CRITICAL' if data.is_critical else 'WARNING', "desc": data.description, "vid": data.vehicle_id})
|
|
await create_audit_log(session, 1, "ISSUE_REPORT", data.vehicle_id, data.description, "OK", 'CRITICAL')
|
|
return {"status": "success"}
|
|
@app.post("/api/resolve_issue")
|
|
async def resolve_issue(data: IssueResolve):
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
await session.execute(text("UPDATE data.vehicles SET status = 'OK', current_issue = NULL WHERE id = :vid"), {"vid": data.vehicle_id})
|
|
await create_audit_log(session, 1, "ISSUE_RESOLVED", data.vehicle_id, "Megjavítva", "WARNING", "OK")
|
|
return {"status": "success"}
|
|
@app.get("/api/vehicles")
|
|
async def get_vehicles():
|
|
async with AsyncSessionLocal() as session:
|
|
return [{"id": r.id, "brand": r.brand, "model": r.model_name, "category": r.category} for r in (await session.execute(text("SELECT vm.id, m.name as brand, vm.model_name, vm.category FROM ref.vehicle_models vm JOIN ref.vehicle_makes m ON vm.make_id = m.id ORDER BY m.name"))).fetchall()]
|
|
@app.post("/api/register")
|
|
async def register_vehicle(data: VehicleRegister):
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
res = await session.execute(text("SELECT id FROM data.vehicles WHERE vin = :vin"), {"vin": data.vin})
|
|
vid = res.scalar()
|
|
if not vid: vid = (await session.execute(text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id"), {"mid": data.model_id, "vin": data.vin, "plt": data.plate})).scalar()
|
|
await session.execute(text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, 1, :role, :sd, :sm)"), {"vid": vid, "role": data.role, "sd": data.purchase_date, "sm": data.mileage})
|
|
await create_audit_log(session, 1, "REGISTER_VEHICLE", vid, "Regisztrálva")
|
|
return {"status": "success"}
|
|
@app.get("/api/fleet/members")
|
|
async def get_team_members():
|
|
async with AsyncSessionLocal() as session:
|
|
return [{"email": r.email, "role": r.role, "joined_at": r.joined_at, "country": r.country} for r in (await session.execute(text("SELECT u.email, fm.role, fm.joined_at, u.country FROM data.fleet_members fm JOIN data.users u ON fm.user_id = u.id WHERE fm.owner_id = 1"))).fetchall()]
|
|
@app.post("/api/fleet/invite")
|
|
async def invite_member(data: InviteRequest):
|
|
async with AsyncSessionLocal() as session:
|
|
async with session.begin():
|
|
await session.execute(text("INSERT INTO data.invitations (email, inviter_id, role, access_level, token, expires_at) VALUES (:e, 1, :r, :l, :t, NOW() + INTERVAL '7 days')"), {"e": data.email, "r": data.role, "l": data.access_level, "t": str(uuid.uuid4())})
|
|
return {"status": "success"}
|
|
@app.get("/")
|
|
async def serve_frontend():
|
|
if os.path.exists("/app/frontend/index.html"): return FileResponse("/app/frontend/index.html")
|
|
return {"error": "Frontend hiányzik"} |