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"}