Files

195 lines
10 KiB
Python
Executable File

from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel, validator
from typing import Optional, List
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
import os
from datetime import date
import uuid
from dotenv import load_dotenv
load_dotenv()
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 CostCreate(BaseModel):
vehicle_id: int
cost_type: str
amount: float
currency: str
date: date
mileage: int
description: Optional[str] = ""
class VehicleRegister(BaseModel):
model_id: int
vin: str
plate: str
mileage: int
purchase_date: date
role: str = "OWNER"
@validator('vin')
def clean_vin(cls, v): return v.upper().replace("-", "").replace(" ", "")
@validator('plate')
def clean_plate(cls, v): return v.upper().replace("-", "").replace(" ", "")
@validator('mileage')
def positive_mileage(cls, v): return v if v >= 0 else 0
class InviteRequest(BaseModel):
email: str
role: str
access_level: str
# --- LOG HELPER ---
async def create_audit_log(session, user_id, event, target_id, details, old_val=None, new_val=None):
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})
# --- VÉGPONTOK ---
@app.get("/api/vehicle/{vehicle_id}")
async def get_vehicle_details(vehicle_id: int):
user_id = 1
async with AsyncSessionLocal() as session:
# 1. Alapadatok
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")
# 2. ÉVES KÖLTSÉG SZÁMÍTÁSA (Idei év)
# Egyszerűség kedvéért most csak a User pénznemében lévőket adjuk össze
# (Később itt lesz a valutaváltó logika)
current_year = date.today().year
q_cost = text("""
SELECT COALESCE(SUM(amount), 0)
FROM data.costs
WHERE vehicle_id = :vid
AND EXTRACT(YEAR FROM date) = :year
AND currency = :curr
""")
res_cost = await session.execute(q_cost, {"vid": vehicle_id, "year": current_year, "curr": car.default_currency})
year_cost = res_cost.scalar()
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": year_cost # <--- EZT HIÁNYOLTAD!
}
# --- MARADÉK VÉGPONTOK (Cost, History, Report, Resolve, Fleet...) ---
@app.post("/api/add_cost")
async def add_cost(data: CostCreate):
user_id = 1
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) VALUES (:vid, :uid, :type, :amt, :curr, :date, :mil, :desc)"),
{"vid": data.vehicle_id, "uid": user_id, "type": data.cost_type, "amt": data.amount, "curr": data.currency, "date": data.date, "mil": data.mileage, "desc": data.description})
res = await session.execute(text("SELECT start_mileage FROM data.vehicle_history WHERE vehicle_id = :vid AND end_date IS NULL"), {"vid": data.vehicle_id})
current = res.scalar() or 0
if data.mileage > current:
await session.execute(text("UPDATE data.vehicle_history SET start_mileage = :mil WHERE vehicle_id = :vid AND end_date IS NULL"), {"mil": data.mileage, "vid": data.vehicle_id})
await create_audit_log(session, user_id, "MILEAGE_UPDATE", data.vehicle_id, f"Km: {data.mileage}")
await create_audit_log(session, user_id, "ADD_COST", data.vehicle_id, f"{data.cost_type}: {data.amount}")
return {"status": "success"}
@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, '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, '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
@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():
st = 'CRITICAL' if data.is_critical else 'WARNING'
await session.execute(text("UPDATE data.vehicles SET status = :st, current_issue = :desc WHERE id = :vid"), {"st": st, "desc": data.description, "vid": data.vehicle_id})
await create_audit_log(session, 1, "ISSUE_REPORT", data.vehicle_id, data.description, "OK", st)
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"}