Files
service-finder/code-server-config/data/User/History/-3487e1e/DXvc.py

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