Initial commit: Robot ökoszisztéma v2.0 - Stabilizált jármű és szerviz robotok

This commit is contained in:
Kincses
2026-03-04 02:03:03 +01:00
commit 250f4f4b8f
7942 changed files with 449625 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
from fastapi import FastAPI, HTTPException, Request
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()
# DB Config
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 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
class IssueReport(BaseModel):
vehicle_id: int
description: str
is_critical: bool
# --- SEGÉDFÜGGVÉNY: AUDIT LOG ÍRÁSA ---
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/my_vehicles")
async def get_my_garage():
user_id = 1
async with AsyncSessionLocal() as session:
# Most már lekérjük a státuszt is!
query = 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 = :uid AND vh.end_date IS NULL
ORDER BY vh.start_date DESC
""")
result = await session.execute(query, {"uid": user_id})
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 # <--- ÚJ MEZŐK
} for r in result.fetchall()]
@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 -- <--- ÚJ
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")
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
}
# --- ÚJ: HIBA BEJELENTÉS (LOGOLÁSSAL) ---
@app.post("/api/report_issue")
async def report_issue(data: IssueReport):
user_id = 1 # Demo User
async with AsyncSessionLocal() as session:
async with session.begin():
# 1. Frissítjük a járművet
new_status = 'CRITICAL' if data.is_critical else 'WARNING'
await session.execute(text("""
UPDATE data.vehicles
SET status = :st, current_issue = :desc
WHERE id = :vid
"""), {"st": new_status, "desc": data.description, "vid": data.vehicle_id})
# 2. ÍRUNK A NAPLÓBA (AUDIT LOG)
await create_audit_log(
session, user_id, "ISSUE_REPORT", data.vehicle_id,
details=f"Hiba: {data.description}",
old_val="OK", new_val=new_status
)
return {"status": "success", "message": "Hiba naplózva és rögzítve."}
# --- MARADÉK (REGISZTER, FLOTTA, MEGHÍVÁS) ---
# (Ezeket most rövidítve hagyom, de a fájlban benne kell lenniük a korábbi verzió szerint)
@app.get("/api/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
result = 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"))
return [{"id": r.id, "brand": r.brand, "model": r.model_name, "category": r.category} for r in result.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:
ins = text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id")
r = await session.execute(ins, {"mid": data.model_id, "vin": data.vin, "plt": data.plate})
vid = r.scalar()
hist = text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, 1, :role, :sd, :sm)")
await session.execute(hist, {"vid": vid, "role": data.role, "sd": data.purchase_date, "sm": data.mileage})
# LOGOLÁS ITT IS!
await create_audit_log(session, 1, "REGISTER_VEHICLE", vid, "Jármű regisztrálva")
return {"status": "success"}
@app.get("/api/fleet/members")
async def get_team_members():
async with AsyncSessionLocal() as session:
res = 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"))
return [{"email": r.email, "role": r.role, "joined_at": r.joined_at, "country": r.country} for r in res.fetchall()]
@app.post("/api/fleet/invite")
async def invite_member(data: InviteRequest):
token = str(uuid.uuid4())
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": token})
return {"status": "success", "message": "Meghívó elküldve!", "debug_token": token}
@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"}

View File

@@ -0,0 +1,177 @@
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
# ÚJ: KÖLTSÉG MODELL
class CostCreate(BaseModel):
vehicle_id: int
cost_type: str # FUEL, SERVICE, INSURANCE, TAX, OTHER
amount: float
currency: str # HUF, EUR
date: date
mileage: int
description: Optional[str] = ""
# --- LOGOLÁS ---
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 ---
# 1. KÖLTSÉG HOZZÁADÁSA (Okos Km frissítéssel!)
@app.post("/api/add_cost")
async def add_cost(data: CostCreate):
user_id = 1
async with AsyncSessionLocal() as session:
async with session.begin():
# Költség mentése
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
})
# KM ÓRA AUTOMATIKUS FRISSÍTÉSE (Ha a megadott km nagyobb, mint a jelenlegi)
# Először lekérjük a jelenlegit
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})
# Opcionális: Logoljuk a km frissítést is
await create_audit_log(session, user_id, "MILEAGE_UPDATE", data.vehicle_id, f"Km frissítve költség rögzítésekor: {data.mileage}")
await create_audit_log(session, user_id, "ADD_COST", data.vehicle_id, f"{data.cost_type}: {data.amount} {data.currency}")
return {"status": "success"}
# 2. SZERVIZKÖNYV LEKÉRÉSE (Történet)
@app.get("/api/vehicle/{vehicle_id}/history")
async def get_vehicle_history(vehicle_id: int):
user_id = 1
async with AsyncSessionLocal() as session:
# Lekérjük a költségeket
res_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})
costs = res_costs.fetchall()
# Lekérjük az Audit Log eseményeket (Hiba, Javítás)
res_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})
logs = res_logs.fetchall()
# Összefésüljük a két listát Pythonban és dátum szerint rendezzük
combined = []
for r in costs: combined.append(dict(r._mapping))
for r in logs: combined.append(dict(r._mapping))
# Rendezés dátum szerint csökkenőbe (legújabb elöl)
combined.sort(key=lambda x: x['date'], reverse=True)
return combined
# --- KORÁBBI VÉGPONTOK (Rövidítve a hely miatt, de ezek kellenek!) ---
@app.get("/api/my_vehicles")
async def get_my_garage():
user_id = 1
async with AsyncSessionLocal() as session:
query = 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 = :uid AND vh.end_date IS NULL
ORDER BY vh.start_date DESC
""")
result = await session.execute(query, {"uid": user_id})
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 result.fetchall()]
@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")
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}
@app.post("/api/report_issue")
async def report_issue(data: IssueReport):
user_id = 1
async with AsyncSessionLocal() as session:
async with session.begin():
new_status = 'CRITICAL' if data.is_critical else 'WARNING'
await session.execute(text("UPDATE data.vehicles SET status = :st, current_issue = :desc WHERE id = :vid"), {"st": new_status, "desc": data.description, "vid": data.vehicle_id})
await create_audit_log(session, user_id, "ISSUE_REPORT", data.vehicle_id, details=data.description, old_val="OK", new_val=new_status)
return {"status": "success"}
@app.post("/api/resolve_issue")
async def resolve_issue(data: IssueResolve):
user_id = 1
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, user_id, "ISSUE_RESOLVED", data.vehicle_id, details="Probléma megoldva", old_val="WARNING", new_val="OK")
return {"status": "success", "message": "Jármű státusza helyreállítva!"}
@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"}

View File

@@ -0,0 +1,140 @@
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel, validator, EmailStr
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 # Token generáláshoz
from dotenv import load_dotenv
load_dotenv()
# DB Config
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 VehicleRegister(BaseModel):
model_id: int
vin: str
plate: str
mileage: int
purchase_date: date
role: str = "OWNER"
@validator('vin')
def clean_vin(cls, v):
if len(v) < 5: raise ValueError('Túl rövid alvázszám')
return v.upper().replace("-", "").replace(" ", "")
@validator('plate')
def clean_plate(cls, v):
return v.upper().replace("-", "").replace(" ", "")
@validator('mileage')
def positive_mileage(cls, v):
if v < 0: raise ValueError('Negatív km nem lehet')
return v
class InviteRequest(BaseModel):
email: EmailStr
role: str # 'DRIVER', 'FLEET_MANAGER'
access_level: str # 'FULL', 'LOG_ONLY'
# --- VÉGPONTOK ---
@app.get("/api/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
result = 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, vm.model_name
"""))
return [{"id": r.id, "brand": r.brand, "model": r.model_name, "category": r.category} for r in result.fetchall()]
@app.get("/api/my_vehicles")
async def get_my_garage():
user_id = 1
async with AsyncSessionLocal() as session:
query = 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
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 = :uid AND vh.end_date IS NULL
ORDER BY vh.start_date DESC
""")
result = await session.execute(query, {"uid": user_id})
return [{"vehicle_id": r.vehicle_id, "brand": r.brand, "model": r.model_name, "plate": r.current_plate, "category": r.category, "role": r.role} for r in result.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:
ins = text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id")
r = await session.execute(ins, {"mid": data.model_id, "vin": data.vin, "plt": data.plate})
vid = r.scalar()
hist = text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, 1, :role, :sd, :sm)")
await session.execute(hist, {"vid": vid, "role": data.role, "sd": data.purchase_date, "sm": data.mileage})
return {"status": "success"}
# --- ÚJ FLOTTA VÉGPONTOK ---
# 1. Csapat lista
@app.get("/api/fleet/members")
async def get_team_members():
user_id = 1 # Demo Boss
async with AsyncSessionLocal() as session:
# Lekérjük a fleet_members táblát összekötve a users-el
query = 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 = :uid
""")
result = await session.execute(query, {"uid": user_id})
return [{"email": r.email, "role": r.role, "joined_at": r.joined_at, "country": r.country} for r in result.fetchall()]
# 2. Meghívó küldése
@app.post("/api/fleet/invite")
async def invite_member(data: InviteRequest):
user_id = 1
token = str(uuid.uuid4()) # Generálunk egy egyedi kódot
async with AsyncSessionLocal() as session:
async with session.begin():
# MENTÉS az invitations táblába
await session.execute(text("""
INSERT INTO data.invitations (email, inviter_id, role, access_level, token, expires_at)
VALUES (:email, :uid, :role, :lvl, :token, NOW() + INTERVAL '7 days')
"""), {
"email": data.email, "uid": user_id, "role": data.role,
"lvl": data.access_level, "token": token
})
# Itt küldenénk az EMAILT a valóságban
print(f"📧 EMAIL KÜLDÉSE: {data.email} -> Link: /invite/{token}")
return {"status": "success", "message": "Meghívó elküldve!", "debug_token": token}
@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"}

View File

@@ -0,0 +1,167 @@
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"}

View File

@@ -0,0 +1,230 @@
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
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
import shutil
from datetime import date
import uuid
from dotenv import load_dotenv
load_dotenv()
# --- KONFIGURÁCIÓ ---
# Itt adjuk meg, hova mentsen. Később ezt a mappát csatoljuk a NAS-hoz!
UPLOAD_DIR = "/app/frontend/uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
# Demo Árfolyamok (Később API-ból jön)
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"
@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
# --- SEGÉDFÜGGVÉNYEK ---
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})
# --- ÚJ VÉGPONTOK ---
# 1. KÖLTSÉG + FÁJL FELTÖLTÉS (Multipart Form)
@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(...), # Dátum stringként jön Formból
description: str = Form(""),
file: UploadFile = File(None) # Opcionális fájl
):
user_id = 1
document_path = None
# A) Fájl mentése (NAS Előkészítés)
if file:
file_ext = file.filename.split(".")[-1]
unique_name = f"{uuid.uuid4()}.{file_ext}"
file_path = os.path.join(UPLOAD_DIR, unique_name)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# A Frontendnek relatív útvonal kell: /uploads/nev.jpg
document_path = f"uploads/{unique_name}"
async with AsyncSessionLocal() as session:
async with session.begin():
# B) Adatbázis mentés
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": date_str, "mil": mileage, "desc": description,
"doc": document_path
})
# C) Km frissítés
res = await session.execute(text("SELECT start_mileage FROM data.vehicle_history WHERE vehicle_id = :vid AND end_date IS NULL"), {"vid": vehicle_id})
current = res.scalar() or 0
if mileage > current:
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, "MILEAGE_UPDATE", vehicle_id, f"Km: {mileage}")
await create_audit_log(session, user_id, "ADD_COST", vehicle_id, f"{cost_type}: {amount} {currency} + Doc")
return {"status": "success"}
# 2. ADATLAP LEKÉRÉSE (Valutaváltással!)
@app.get("/api/vehicle/{vehicle_id}")
async def get_vehicle_details(vehicle_id: int):
user_id = 1
async with AsyncSessionLocal() as session:
# Alapadatok lekérése
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")
# --- OKOS KÖLTSÉGSZÁMOLÁS ---
current_year = date.today().year
# Lekérjük az összes idei tételt, függetlenül a pénznemtől
q_costs = text("""
SELECT amount, currency
FROM data.costs
WHERE vehicle_id = :vid AND EXTRACT(YEAR FROM date) = :year
""")
costs_res = await session.execute(q_costs, {"vid": vehicle_id, "year": current_year})
costs = costs_res.fetchall()
total_cost = 0.0
user_curr = car.default_currency # pl. HUF
for c in costs:
if c.currency == user_curr:
total_cost += float(c.amount)
# Konverzió
elif c.currency == 'EUR' and user_curr == 'HUF':
total_cost += float(c.amount) * EXCHANGE_RATES["EUR_TO_HUF"]
elif c.currency == 'HUF' and user_curr == 'EUR':
total_cost += float(c.amount) * EXCHANGE_RATES["HUF_TO_EUR"]
else:
total_cost += float(c.amount) # Ismeretlen pénznem, hozzáadjuk simán (vagy hiba)
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_cost # Ez már a konvertált összeg!
}
@app.get("/api/vehicle/{vehicle_id}/history")
async def get_vehicle_history(vehicle_id: int):
async with AsyncSessionLocal() as session:
# Itt is lekérjük a document_url-t!
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 VÉGPONTOK (Rövidítve a hely miatt) ---
@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/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.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"}

View File

@@ -0,0 +1,227 @@
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
from fastapi.responses import FileResponse, JSONResponse
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
import shutil
from datetime import date, datetime # <--- FONTOS: datetime importálva
import uuid
import traceback
from dotenv import load_dotenv
load_dotenv()
# MAPPA KONFIGURÁCIÓ
UPLOAD_DIR = "/app/frontend/uploads"
try:
os.makedirs(UPLOAD_DIR, exist_ok=True)
except Exception as e:
print(f"HIBA a mappa létrehozásakor: {e}")
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"
@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):
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 Exception as e:
print(f"AUDIT LOG HIBA: {e}")
# --- API VÉGPONTOK ---
@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
# 1. DÁTUM KONVERTÁLÁSA (EZ VOLT A HIBA OKA!)
# A Frontend stringet küld ("2026-01-20"), mi átalakítjuk Date objektummá
try:
real_date = datetime.strptime(date_str, "%Y-%m-%d").date()
except ValueError:
return JSONResponse(status_code=400, content={"detail": f"Hibás dátum formátum: {date_str}"})
# 2. Fájl mentése
if file:
try:
file_ext = file.filename.split(".")[-1]
unique_name = f"{uuid.uuid4()}.{file_ext}"
file_path = os.path.join(UPLOAD_DIR, unique_name)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
document_path = f"uploads/{unique_name}"
except Exception as file_error:
print(f"FÁJL MENTÉSI HIBA: {file_error}")
# Nem állunk meg, csak logoljuk, a költség attól még létrejöhet kép nélkül is
document_path = None
# 3. Adatbázis mentés
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, # <--- ITT MÁR A KONVERTÁLT DÁTUMOT HASZNÁLJUK
"mil": mileage, "desc": description,
"doc": document_path
})
# Km frissítés
res = await session.execute(text("SELECT start_mileage FROM data.vehicle_history WHERE vehicle_id = :vid AND end_date IS NULL"), {"vid": vehicle_id})
current = res.scalar() or 0
if mileage > current:
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, "MILEAGE_UPDATE", vehicle_id, f"Km: {mileage}")
await create_audit_log(session, user_id, "ADD_COST", vehicle_id, f"{cost_type}: {amount} {currency}")
return {"status": "success"}
except Exception as e:
error_msg = traceback.format_exc()
print(f"KRITIKUS HIBA: {error_msg}")
return JSONResponse(status_code=500, content={"detail": f"Szerver hiba: {str(e)}"})
# --- EGYÉB VÉGPONTOK ---
@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")
current_year = date.today().year
q_costs = text("SELECT amount, currency FROM data.costs WHERE vehicle_id = :vid AND EXTRACT(YEAR FROM date) = :year")
costs = (await session.execute(q_costs, {"vid": vehicle_id, "year": current_year})).fetchall()
total_cost = 0.0
user_curr = car.default_currency
for c in costs:
if c.currency == user_curr: total_cost += float(c.amount)
elif c.currency == 'EUR' and user_curr == 'HUF': total_cost += float(c.amount) * EXCHANGE_RATES["EUR_TO_HUF"]
elif c.currency == 'HUF' and user_curr == 'EUR': total_cost += float(c.amount) * EXCHANGE_RATES["HUF_TO_EUR"]
else: total_cost += 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_cost}
@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
@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"}

View File

@@ -0,0 +1,94 @@
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Depends
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from datetime import datetime, timedelta
from jose import JWTError, jwt
import bcrypt # Közvetlen bcrypt használata a passlib helyett a hiba elkerülésére
import os, uuid, traceback
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = "SZUPER_TITKOS_KULCS_2026"
ALGORITHM = "HS256"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
DATABASE_URL = os.getenv("DATABASE_URL", "").replace("postgresql://", "postgresql+asyncpg://")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
app = FastAPI()
# --- JAVÍTOTT TITKOSÍTÁS (Passlib bug kikerülése) ---
def get_password_hash(password: str):
pwd_bytes = password.encode('utf-8')
# A bcrypt korlátja 72 byte, vágjuk le ha hosszabb (biztonsági best practice)
if len(pwd_bytes) > 72:
pwd_bytes = pwd_bytes[:72]
salt = bcrypt.gensalt()
return bcrypt.hashpw(pwd_bytes, salt).decode('utf-8')
def verify_password(plain_password: str, hashed_password: str):
pwd_bytes = plain_password.encode('utf-8')
if len(pwd_bytes) > 72:
pwd_bytes = pwd_bytes[:72]
return bcrypt.checkpw(pwd_bytes, hashed_password.encode('utf-8'))
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None: raise HTTPException(status_code=401)
return int(user_id)
except Exception: raise HTTPException(status_code=401)
@app.post("/api/auth/register")
async def register(email: str = Form(...), password: str = Form(...)):
try:
async with AsyncSessionLocal() as session:
async with session.begin():
hashed = get_password_hash(password)
await session.execute(
text("INSERT INTO data.users (email, password_hash) VALUES (:e, :p)"),
{"e": email, "p": hashed}
)
return {"status": "success"}
except Exception as e:
print(f"REGISZTRÁCIÓS HIBA: {str(e)}")
return JSONResponse(status_code=500, content={"detail": f"Adatbázis hiba: {str(e)}"})
@app.post("/api/auth/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT id, password_hash FROM data.users WHERE email = :e"), {"e": form_data.username})
user = res.fetchone()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(status_code=401, detail="Hibás email vagy jelszó")
token = jwt.encode({"sub": str(user.id), "exp": datetime.utcnow() + timedelta(days=1)}, SECRET_KEY, algorithm=ALGORITHM)
return {"access_token": token, "token_type": "bearer"}
@app.get("/api/my_vehicles")
async def my_vehicles(user_id: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
q = text("SELECT v.id as vehicle_id, v.current_plate as plate, m.name as brand, mo.model_name as model, v.status 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 = :uid")
res = await session.execute(q, {"uid": user_id})
return [dict(r._mapping) for r in res.fetchall()]
@app.get("/api/ref/cost_types")
async def cost_types():
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT code, name, parent_code FROM ref.cost_types"))
rows = res.fetchall()
tree = {}
for r in rows:
if not r.parent_code: tree[r.code] = {"label": r.name, "subs": {}}
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
@app.get("/")
async def index(): return FileResponse("/app/frontend/index.html")

View File

@@ -0,0 +1,76 @@
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Depends
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
import os, uuid, shutil
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = "SZUPER_TITKOS_KULCS_2026"
ALGORITHM = "HS256"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
DATABASE_URL = os.getenv("DATABASE_URL", "").replace("postgresql://", "postgresql+asyncpg://")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
app = FastAPI()
def get_password_hash(password): return pwd_context.hash(password)
def verify_password(plain, hashed): return pwd_context.verify(plain, hashed)
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: str = payload.get("sub")
if user_id is None: raise HTTPException(status_code=401)
return int(user_id)
except Exception: raise HTTPException(status_code=401)
@app.post("/api/auth/register")
async def register(email: str = Form(...), password: str = Form(...)):
async with AsyncSessionLocal() as session:
async with session.begin():
hashed = get_password_hash(password)
await session.execute(text("INSERT INTO data.users (email, password_hash) VALUES (:e, :p)"), {"e": email, "p": hashed})
return {"status": "success"}
@app.post("/api/auth/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT id, password_hash FROM data.users WHERE email = :e"), {"e": form_data.username})
user = res.fetchone()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(status_code=401, detail="Hibás adatok")
token = jwt.encode({"sub": str(user.id), "exp": datetime.utcnow() + timedelta(days=1)}, SECRET_KEY, algorithm=ALGORITHM)
return {"access_token": token, "token_type": "bearer"}
@app.get("/api/my_vehicles")
async def my_vehicles(user_id: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
q = text("SELECT v.id as vehicle_id, v.current_plate as plate, m.name as brand, mo.model_name as model, v.status 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 = :uid")
res = await session.execute(q, {"uid": user_id})
return [dict(r._mapping) for r in res.fetchall()]
@app.get("/api/ref/cost_types")
async def cost_types():
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT code, name, parent_code FROM ref.cost_types"))
rows = res.fetchall()
tree = {}
for r in rows:
if not r.parent_code: tree[r.code] = {"label": r.name, "subs": {}}
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
@app.get("/")
async def index(): return FileResponse("/app/frontend/index.html")

View File

@@ -0,0 +1,191 @@
from fastapi import FastAPI, HTTPException, Request
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()
# DB Config
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 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
class IssueReport(BaseModel):
vehicle_id: int
description: str
is_critical: bool
class IssueResolve(BaseModel):
vehicle_id: int
# --- LOGOLÁS ---
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/my_vehicles")
async def get_my_garage():
user_id = 1
async with AsyncSessionLocal() as session:
query = 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 = :uid AND vh.end_date IS NULL
ORDER BY vh.start_date DESC
""")
result = await session.execute(query, {"uid": user_id})
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 result.fetchall()]
@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")
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
}
# 1. HIBA BEJELENTÉS
@app.post("/api/report_issue")
async def report_issue(data: IssueReport):
user_id = 1
async with AsyncSessionLocal() as session:
async with session.begin():
new_status = 'CRITICAL' if data.is_critical else 'WARNING'
# Update Vehicle
await session.execute(text("UPDATE data.vehicles SET status = :st, current_issue = :desc WHERE id = :vid"),
{"st": new_status, "desc": data.description, "vid": data.vehicle_id})
# Log
await create_audit_log(session, user_id, "ISSUE_REPORT", data.vehicle_id,
details=data.description, old_val="OK", new_val=new_status)
return {"status": "success"}
# 2. HIBA JAVÍTÁS (EZ HIÁNYZOTT!)
@app.post("/api/resolve_issue")
async def resolve_issue(data: IssueResolve):
user_id = 1
async with AsyncSessionLocal() as session:
async with session.begin():
# Lekérjük a régi hibát a loghoz
res = await session.execute(text("SELECT status, current_issue FROM data.vehicles WHERE id = :vid"), {"vid": data.vehicle_id})
curr = res.fetchone()
# Visszaállítás
await session.execute(text("UPDATE data.vehicles SET status = 'OK', current_issue = NULL WHERE id = :vid"),
{"vid": data.vehicle_id})
# Logolás: Ki javította meg?
await create_audit_log(session, user_id, "ISSUE_RESOLVED", data.vehicle_id,
details="Probléma megoldva", old_val=curr.status if curr else "UNKNOWN", new_val="OK")
return {"status": "success", "message": "Jármű státusza helyreállítva!"}
# --- EGYÉB VÉGPONTOK (Register, Fleet...) ---
@app.get("/api/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
result = 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"))
return [{"id": r.id, "brand": r.brand, "model": r.model_name, "category": r.category} for r in result.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:
ins = text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id")
r = await session.execute(ins, {"mid": data.model_id, "vin": data.vin, "plt": data.plate})
vid = r.scalar()
hist = text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, 1, :role, :sd, :sm)")
await session.execute(hist, {"vid": vid, "role": data.role, "sd": data.purchase_date, "sm": data.mileage})
await create_audit_log(session, 1, "REGISTER_VEHICLE", vid, "Jármű regisztrálva")
return {"status": "success"}
@app.get("/api/fleet/members")
async def get_team_members():
async with AsyncSessionLocal() as session:
res = 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"))
return [{"email": r.email, "role": r.role, "joined_at": r.joined_at, "country": r.country} for r in res.fetchall()]
@app.post("/api/fleet/invite")
async def invite_member(data: InviteRequest):
token = str(uuid.uuid4())
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": token})
return {"status": "success", "message": "Meghívó elküldve!", "debug_token": token}
@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"}

View File

@@ -0,0 +1,91 @@
from fastapi import FastAPI, HTTPException, Form, Depends
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from datetime import datetime, timedelta, date
from jose import jwt
import bcrypt, os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = "SZUPER_TITKOS_KULCS_2026"
ALGORITHM = "HS256"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
DATABASE_URL = os.getenv("DATABASE_URL", "").replace("postgresql://", "postgresql+asyncpg://")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
app = FastAPI()
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
p = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return int(p.get("sub"))
except: raise HTTPException(status_code=401)
# --- METAADATOK ---
@app.get("/api/meta/vehicle-hierarchy")
async def get_hierarchy():
async with AsyncSessionLocal() as session:
q = text("SELECT vm.category, m.name as brand, vm.id as model_id, vm.model_name FROM ref.vehicle_models vm JOIN ref.vehicle_makes m ON vm.make_id = m.id ORDER BY vm.category, m.name")
res = await session.execute(q)
hierarchy = {}
for r in res:
if r.category not in hierarchy: hierarchy[r.category] = {}
if r.brand not in hierarchy[r.category]: hierarchy[r.category][r.brand] = []
hierarchy[r.category][r.brand].append({"id": r.model_id, "name": r.model_name})
return hierarchy
@app.get("/api/meta/cost-types")
async def get_cost_types():
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT code, name FROM ref.cost_types ORDER BY name"))
return {r.code: r.name for r in res.fetchall()}
# --- JÁRMŰ MŰVELETEK ---
@app.get("/api/my_vehicles")
async def my_vehicles(uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
q = text("""
SELECT v.id as vehicle_id, v.current_plate as plate, m.name as brand, mo.model_name as model, v.status,
(SELECT SUM(amount) FROM data.costs WHERE vehicle_id = v.id) as total_cost
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 = :uid AND vh.end_date IS NULL
""")
res = await session.execute(q, {"uid": uid})
return [dict(r._mapping) for r in res.fetchall()]
@app.post("/api/register")
async def register_vehicle(d: dict, uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
async with session.begin():
res = await session.execute(text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id"), {"mid": d['model_id'], "vin": d['vin'], "plt": d['plate']})
vid = res.scalar()
await session.execute(text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, :uid, 'OWNER', CURRENT_DATE, :sm)"), {"vid": vid, "uid": uid, "sm": d['mileage']})
return {"status": "success"}
@app.delete("/api/vehicle/{vid}")
async def delete_vehicle(vid: int, uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
async with session.begin():
# Soft delete: lezárjuk a history-t
await session.execute(text("UPDATE data.vehicle_history SET end_date = CURRENT_DATE WHERE vehicle_id = :vid AND user_id = :uid"), {"vid": vid, "uid": uid})
return {"status": "deleted"}
@app.post("/api/add_cost")
async def add_cost(d: dict, uid: int = Depends(get_current_user)):
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) VALUES (:vid, :uid, :type, :amt, 'HUF', CURRENT_DATE)"),
{"vid": d['vehicle_id'], "uid": uid, "type": d['type'], "amt": d['amount']})
return {"status": "success"}
@app.get("/")
async def index(): return FileResponse("/app/frontend/index.html")

View File

@@ -0,0 +1,195 @@
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"}

View File

@@ -0,0 +1,170 @@
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()
# DB Config
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 VehicleRegister(BaseModel):
model_id: int
vin: str
plate: str
mileage: int
purchase_date: date
role: str = "OWNER"
@validator('vin')
def clean_vin(cls, v):
if len(v) < 5: raise ValueError('Túl rövid alvázszám')
return v.upper().replace("-", "").replace(" ", "")
@validator('plate')
def clean_plate(cls, v):
return v.upper().replace("-", "").replace(" ", "")
@validator('mileage')
def positive_mileage(cls, v):
if v < 0: raise ValueError('Negatív km nem lehet')
return v
class InviteRequest(BaseModel):
email: str
role: str
access_level: str
# --- VÉGPONTOK ---
@app.get("/api/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
result = 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, vm.model_name
"""))
return [{"id": r.id, "brand": r.brand, "model": r.model_name, "category": r.category} for r in result.fetchall()]
@app.get("/api/my_vehicles")
async def get_my_garage():
user_id = 1
async with AsyncSessionLocal() as session:
query = 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
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 = :uid AND vh.end_date IS NULL
ORDER BY vh.start_date DESC
""")
result = await session.execute(query, {"uid": user_id})
return [{"vehicle_id": r.vehicle_id, "brand": r.brand, "model": r.model_name, "plate": r.current_plate, "category": r.category, "role": r.role} for r in result.fetchall()]
# ÚJ: RÉSZLETES ADATLAP LEKÉRÉSE
@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_basic = 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 -- A user pénzneme
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_basic = await session.execute(q_basic, {"vid": vehicle_id, "uid": user_id})
car = res_basic.fetchone()
if not car:
raise HTTPException(status_code=404, detail="Jármű nem található vagy nincs hozzáférése")
# 2. Utolsó ismert km óraállás (a history-ból vagy költségekből)
# Egyelőre visszaadjuk a start_mileage-t, később itt számolunk
current_km = car.start_mileage
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": current_km,
"currency": car.default_currency
}
@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:
ins = text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id")
r = await session.execute(ins, {"mid": data.model_id, "vin": data.vin, "plt": data.plate})
vid = r.scalar()
hist = text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, 1, :role, :sd, :sm)")
await session.execute(hist, {"vid": vid, "role": data.role, "sd": data.purchase_date, "sm": data.mileage})
return {"status": "success"}
@app.get("/api/fleet/members")
async def get_team_members():
user_id = 1
async with AsyncSessionLocal() as session:
query = 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 = :uid
""")
result = await session.execute(query, {"uid": user_id})
return [{"email": r.email, "role": r.role, "joined_at": r.joined_at, "country": r.country} for r in result.fetchall()]
@app.post("/api/fleet/invite")
async def invite_member(data: InviteRequest):
user_id = 1
token = str(uuid.uuid4())
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 (:email, :uid, :role, :lvl, :token, NOW() + INTERVAL '7 days')
"""), {
"email": data.email, "uid": user_id, "role": data.role,
"lvl": data.access_level, "token": token
})
print(f"📧 EMAIL KÜLDÉSE: {data.email} -> Link: /invite/{token}")
return {"status": "success", "message": "Meghívó elküldve!", "debug_token": token}
@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"}

View File

@@ -0,0 +1 @@
{"version":1,"resource":"vscode-remote://192.168.100.43:8443/home/coder/project/backend/main.py","entries":[{"id":"7zSH.py","timestamp":1768944686120},{"id":"gVs3.py","timestamp":1768944898840},{"id":"eaD4.py","timestamp":1768945068633},{"id":"0MBT.py","timestamp":1768945735412},{"id":"VR1d.py","timestamp":1768945973442},{"id":"2JOI.py","timestamp":1768946205711},{"id":"dYmD.py","timestamp":1768946545949},{"id":"HFdy.py","timestamp":1768946891365},{"id":"zNYa.py","timestamp":1768947662698},{"id":"HYjM.py","timestamp":1768947811545},{"id":"DXvc.py","timestamp":1768948349423},{"id":"TFdq.py","timestamp":1768953406975},{"id":"MoK7.py","timestamp":1768953978619},{"id":"gleo.py","timestamp":1768954265874},{"id":"z1at.py","timestamp":1768954519769},{"id":"WE3Y.py","timestamp":1768954748699},{"id":"sEal.py","timestamp":1768954903048}]}

View File

@@ -0,0 +1,134 @@
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel, validator
# KIVETTÜK AZ EmailStr-t, mert hiányzik a library hozzá!
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()
# DB Config
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 VehicleRegister(BaseModel):
model_id: int
vin: str
plate: str
mileage: int
purchase_date: date
role: str = "OWNER"
@validator('vin')
def clean_vin(cls, v):
if len(v) < 5: raise ValueError('Túl rövid alvázszám')
return v.upper().replace("-", "").replace(" ", "")
@validator('plate')
def clean_plate(cls, v):
return v.upper().replace("-", "").replace(" ", "")
@validator('mileage')
def positive_mileage(cls, v):
if v < 0: raise ValueError('Negatív km nem lehet')
return v
class InviteRequest(BaseModel):
email: str # <--- JAVÍTVA: Sima string, nem EmailStr
role: str
access_level: str
# --- VÉGPONTOK ---
@app.get("/api/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
result = 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, vm.model_name
"""))
return [{"id": r.id, "brand": r.brand, "model": r.model_name, "category": r.category} for r in result.fetchall()]
@app.get("/api/my_vehicles")
async def get_my_garage():
user_id = 1
async with AsyncSessionLocal() as session:
query = 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
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 = :uid AND vh.end_date IS NULL
ORDER BY vh.start_date DESC
""")
result = await session.execute(query, {"uid": user_id})
return [{"vehicle_id": r.vehicle_id, "brand": r.brand, "model": r.model_name, "plate": r.current_plate, "category": r.category, "role": r.role} for r in result.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:
ins = text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id")
r = await session.execute(ins, {"mid": data.model_id, "vin": data.vin, "plt": data.plate})
vid = r.scalar()
hist = text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, 1, :role, :sd, :sm)")
await session.execute(hist, {"vid": vid, "role": data.role, "sd": data.purchase_date, "sm": data.mileage})
return {"status": "success"}
# --- FLOTTA VÉGPONTOK ---
@app.get("/api/fleet/members")
async def get_team_members():
user_id = 1
async with AsyncSessionLocal() as session:
query = 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 = :uid
""")
result = await session.execute(query, {"uid": user_id})
return [{"email": r.email, "role": r.role, "joined_at": r.joined_at, "country": r.country} for r in result.fetchall()]
@app.post("/api/fleet/invite")
async def invite_member(data: InviteRequest):
user_id = 1
token = str(uuid.uuid4())
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 (:email, :uid, :role, :lvl, :token, NOW() + INTERVAL '7 days')
"""), {
"email": data.email, "uid": user_id, "role": data.role,
"lvl": data.access_level, "token": token
})
print(f"📧 EMAIL KÜLDÉSE: {data.email} -> Link: /invite/{token}")
return {"status": "success", "message": "Meghívó elküldve!", "debug_token": token}
@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"}

View File

@@ -0,0 +1,106 @@
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Depends
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from datetime import datetime, timedelta, date
from jose import JWTError, jwt
import bcrypt, os, uuid, traceback
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = "SZUPER_TITKOS_KULCS_2026"
ALGORITHM = "HS256"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
DATABASE_URL = os.getenv("DATABASE_URL", "").replace("postgresql://", "postgresql+asyncpg://")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
app = FastAPI()
# --- AUTH ---
def get_password_hash(password: str):
pwd_bytes = password.encode('utf-8')
return bcrypt.hashpw(pwd_bytes[:72], bcrypt.gensalt()).decode('utf-8')
def verify_password(plain, hashed):
return bcrypt.checkpw(plain.encode('utf-8')[:72], hashed.encode('utf-8'))
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return int(payload.get("sub"))
except: raise HTTPException(status_code=401)
# --- API ---
@app.post("/api/auth/register")
async def register(email: str = Form(...), password: str = Form(...)):
async with AsyncSessionLocal() as session:
async with session.begin():
h = get_password_hash(password)
await session.execute(text("INSERT INTO data.users (email, password_hash) VALUES (:e, :p)"), {"e": email, "p": h})
return {"status": "success"}
@app.post("/api/auth/login")
async def login(f: OAuth2PasswordRequestForm = Depends()):
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT id, password_hash FROM data.users WHERE email = :e"), {"e": f.username})
u = res.fetchone()
if not u or not verify_password(f.password, u.password_hash): raise HTTPException(status_code=401)
token = jwt.encode({"sub": str(u.id), "exp": datetime.utcnow() + timedelta(days=1)}, SECRET_KEY, algorithm=ALGORITHM)
return {"access_token": token, "token_type": "bearer"}
@app.get("/api/my_vehicles")
async def my_vehicles(uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
q = text("SELECT v.id as vehicle_id, v.current_plate as plate, m.name as brand, mo.model_name as model, v.status 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 = :uid AND vh.end_date IS NULL")
res = await session.execute(q, {"uid": uid})
return [dict(r._mapping) for r in res.fetchall()]
@app.get("/api/vehicles")
async def all_models():
async with AsyncSessionLocal() as session:
q = text("SELECT vm.id, m.name as brand, vm.model_name as model FROM ref.vehicle_models vm JOIN ref.vehicle_makes m ON vm.make_id = m.id ORDER BY brand, model")
res = await session.execute(q)
return [dict(r._mapping) for r in res.fetchall()]
class VehicleReg(BaseModel): model_id: int; vin: str; plate: str; mileage: int; purchase_date: date
@app.post("/api/register")
async def register_vehicle(d: VehicleReg, uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
async with session.begin():
# Új jármű vagy meglévő
res = await session.execute(text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id"), {"mid": d.model_id, "vin": d.vin, "plt": d.plate})
vid = res.scalar()
await session.execute(text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, :uid, 'OWNER', :sd, :sm)"), {"vid": vid, "uid": uid, "sd": d.purchase_date, "sm": d.mileage})
return {"status": "success"}
@app.get("/api/vehicle/{vid}")
async def get_details(vid: int, uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
q = text("SELECT v.id, v.current_plate as plate, m.name as brand, mo.model_name as model, vh.start_mileage as mileage, v.status, u.default_currency as currency 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")
res = await session.execute(q, {"vid": vid, "uid": uid})
car = res.fetchone()
if not car: raise HTTPException(status_code=404)
# Idei költség
c_q = text("SELECT SUM(amount) FROM data.costs WHERE vehicle_id = :vid AND EXTRACT(YEAR FROM date) = 2026")
cost = (await session.execute(c_q, {"vid": vid})).scalar() or 0
return {**dict(car._mapping), "year_cost": cost}
@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(...), uid: int = Depends(get_current_user)):
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) VALUES (:vid, :uid, :type, :amt, :curr, :date, :mil)"),
{"vid": vehicle_id, "uid": uid, "type": cost_type, "amt": amount, "curr": currency, "date": date_str, "mil": mileage})
return {"status": "success"}
@app.get("/")
async def index(): return FileResponse("/app/frontend/index.html")

View File

@@ -0,0 +1,81 @@
from fastapi import FastAPI, HTTPException, Form, Depends
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from datetime import datetime, timedelta, date
from jose import jwt
import bcrypt, os, traceback
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = "SZUPER_TITKOS_KULCS_2026"
ALGORITHM = "HS256"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
DATABASE_URL = os.getenv("DATABASE_URL", "").replace("postgresql://", "postgresql+asyncpg://")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
app = FastAPI()
# --- AUTH ---
def get_password_hash(p): return bcrypt.hashpw(p.encode('utf-8')[:72], bcrypt.gensalt()).decode('utf-8')
def verify_password(p, h): return bcrypt.checkpw(p.encode('utf-8')[:72], h.encode('utf-8'))
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
p = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return int(p.get("sub"))
except: raise HTTPException(status_code=401)
@app.post("/api/auth/register")
async def register(email: str = Form(...), password: str = Form(...)):
try:
async with AsyncSessionLocal() as session:
async with session.begin():
await session.execute(text("INSERT INTO data.users (email, password_hash) VALUES (:e, :p)"),
{"e": email, "p": get_password_hash(password)})
return {"status": "success"}
except Exception as e:
return JSONResponse(status_code=400, content={"detail": "Email már létezik vagy adatbázis hiba."})
@app.post("/api/auth/login")
async def login(f: OAuth2PasswordRequestForm = Depends()):
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT id, password_hash FROM data.users WHERE email = :e"), {"e": f.username})
u = res.fetchone()
if not u or not verify_password(f.password, u.password_hash): raise HTTPException(status_code=401, detail="Hibás adatok")
t = jwt.encode({"sub": str(u.id), "exp": datetime.utcnow() + timedelta(days=1)}, SECRET_KEY, algorithm=ALGORITHM)
return {"access_token": t, "token_type": "bearer"}
# --- DATA ---
@app.get("/api/meta/vehicle-hierarchy")
async def get_hierarchy():
async with AsyncSessionLocal() as session:
q = text("SELECT vm.category, m.name as brand, vm.id as model_id, vm.model_name FROM ref.vehicle_models vm JOIN ref.vehicle_makes m ON vm.make_id = m.id ORDER BY vm.category, m.name")
res = await session.execute(q)
h = {}
for r in res:
if r.category not in h: h[r.category] = {}
if r.brand not in h[r.category]: h[r.category][r.brand] = []
h[r.category][r.brand].append({"id": r.model_id, "name": r.model_name})
return h
@app.get("/api/meta/cost-types")
async def get_cost_types():
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT code, name FROM ref.cost_types ORDER BY name"))
return {r.code: r.name for r in res.fetchall()}
@app.get("/api/my_vehicles")
async def my_vehicles(uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
q = text("SELECT v.id as vehicle_id, v.current_plate as plate, m.name as brand, mo.model_name as model, v.status 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 = :uid AND vh.end_date IS NULL")
res = await session.execute(q, {"uid": uid})
return [dict(r._mapping) for r in res.fetchall()]
@app.get("/")
async def index(): return FileResponse("/app/frontend/index.html")

View File

@@ -0,0 +1,97 @@
from fastapi import FastAPI, HTTPException, Form, Depends
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from datetime import datetime, timedelta, date
from jose import jwt
import bcrypt, os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = "SZUPER_TITKOS_KULCS_2026"
ALGORITHM = "HS256"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
DATABASE_URL = os.getenv("DATABASE_URL", "").replace("postgresql://", "postgresql+asyncpg://")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
app = FastAPI()
# --- AUTH HELPER ---
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
p = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return int(p.get("sub"))
except: raise HTTPException(status_code=401)
# --- DINAMIKUS METAADATOK (Ez az újdonság!) ---
@app.get("/api/meta/vehicle-hierarchy")
async def get_hierarchy():
async with AsyncSessionLocal() as session:
# Lekérjük az összes kategóriát, márkát és modellt egyben
q = text("""
SELECT vm.category, m.name as brand, vm.id as model_id, vm.model_name
FROM ref.vehicle_models vm
JOIN ref.vehicle_makes m ON vm.make_id = m.id
ORDER BY vm.category, m.name, vm.model_name
""")
res = await session.execute(q)
rows = res.fetchall()
hierarchy = {}
for r in rows:
cat = r.category
brand = r.brand
if cat not in hierarchy: hierarchy[cat] = {}
if brand not in hierarchy[cat]: hierarchy[cat][brand] = []
hierarchy[cat][brand].append({"id": r.model_id, "name": r.model_name})
return hierarchy
@app.get("/api/meta/cost-types")
async def get_cost_types():
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT code, name FROM ref.cost_types ORDER BY name"))
return {r.code: r.name for r in res.fetchall()}
# --- CORE API ---
@app.post("/api/auth/login")
async def login(f: OAuth2PasswordRequestForm = Depends()):
async with AsyncSessionLocal() as session:
res = await session.execute(text("SELECT id, password_hash FROM data.users WHERE email = :e"), {"e": f.username})
u = res.fetchone()
if not u or not bcrypt.checkpw(f.password.encode('utf-8')[:72], u.password_hash.encode('utf-8')):
raise HTTPException(status_code=401)
t = jwt.encode({"sub": str(u.id), "exp": datetime.utcnow() + timedelta(days=1)}, SECRET_KEY, algorithm=ALGORITHM)
return {"access_token": t, "token_type": "bearer"}
@app.get("/api/my_vehicles")
async def my_vehicles(uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
q = text("""
SELECT v.id as vehicle_id, v.current_plate as plate, m.name as brand, mo.model_name as model, v.status
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 = :uid AND vh.end_date IS NULL
""")
res = await session.execute(q, {"uid": uid})
return [dict(r._mapping) for r in res.fetchall()]
class VehicleReg(BaseModel): model_id: int; vin: str; plate: str; mileage: int; purchase_date: date
@app.post("/api/register")
async def register_vehicle(d: VehicleReg, uid: int = Depends(get_current_user)):
async with AsyncSessionLocal() as session:
async with session.begin():
res = await session.execute(text("INSERT INTO data.vehicles (model_id, vin, current_plate) VALUES (:mid, :vin, :plt) RETURNING id"), {"mid": d.model_id, "vin": d.vin, "plt": d.plate})
vid = res.scalar()
await session.execute(text("INSERT INTO data.vehicle_history (vehicle_id, user_id, role, start_date, start_mileage) VALUES (:vid, :uid, 'OWNER', :sd, :sm)"), {"vid": vid, "uid": uid, "sd": d.purchase_date, "sm": d.mileage})
return {"status": "success"}
@app.get("/")
async def index(): return FileResponse("/app/frontend/index.html")

View File

@@ -0,0 +1,217 @@
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
from fastapi.responses import FileResponse, JSONResponse
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
import shutil
from datetime import date
import uuid
import traceback # Hogy lássuk a hibát
from dotenv import load_dotenv
load_dotenv()
# MAPPA KONFIGURÁCIÓ
UPLOAD_DIR = "/app/frontend/uploads"
# Megpróbáljuk létrehozni, ha nem menne, a scriptünk már megoldotta
try:
os.makedirs(UPLOAD_DIR, exist_ok=True)
except Exception as e:
print(f"HIBA a mappa létrehozásakor: {e}")
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"
@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):
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 Exception as e:
print(f"AUDIT LOG HIBA: {e}")
# --- API VÉGPONTOK (HIBATŰRŐ MÓDBAN) ---
@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)
):
# ITT A LÉNYEG: TRY-EXCEPT BLOKK
try:
user_id = 1
document_path = None
# 1. Fájl mentése
if file:
try:
file_ext = file.filename.split(".")[-1]
unique_name = f"{uuid.uuid4()}.{file_ext}"
file_path = os.path.join(UPLOAD_DIR, unique_name)
print(f"Fájl mentése ide: {file_path}") # Debug log
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
document_path = f"uploads/{unique_name}"
except Exception as file_error:
print(f"FÁJL MENTÉSI HIBA: {file_error}")
return JSONResponse(status_code=500, content={"detail": f"Nem sikerült a fájlt menteni: {str(file_error)}"})
# 2. Adatbázis mentés
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": date_str, "mil": mileage, "desc": description,
"doc": document_path
})
# Km frissítés
res = await session.execute(text("SELECT start_mileage FROM data.vehicle_history WHERE vehicle_id = :vid AND end_date IS NULL"), {"vid": vehicle_id})
current = res.scalar() or 0
if mileage > current:
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, "MILEAGE_UPDATE", vehicle_id, f"Km: {mileage}")
await create_audit_log(session, user_id, "ADD_COST", vehicle_id, f"{cost_type}: {amount} {currency}")
return {"status": "success"}
except Exception as e:
# Ha bármi baj van, nem omlunk össze, hanem visszaszólunk a frontendnek!
error_msg = traceback.format_exc()
print(f"KRITIKUS HIBA A SZERVEREN: {error_msg}")
return JSONResponse(status_code=500, content={"detail": f"Szerver hiba: {str(e)}"})
# --- TÖBBI VÉGPONT (Változatlan, de működő) ---
@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")
current_year = date.today().year
q_costs = text("SELECT amount, currency FROM data.costs WHERE vehicle_id = :vid AND EXTRACT(YEAR FROM date) = :year")
costs = (await session.execute(q_costs, {"vid": vehicle_id, "year": current_year})).fetchall()
total_cost = 0.0
user_curr = car.default_currency
for c in costs:
if c.currency == user_curr: total_cost += float(c.amount)
elif c.currency == 'EUR' and user_curr == 'HUF': total_cost += float(c.amount) * EXCHANGE_RATES["EUR_TO_HUF"]
elif c.currency == 'HUF' and user_curr == 'EUR': total_cost += float(c.amount) * EXCHANGE_RATES["HUF_TO_EUR"]
else: total_cost += 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_cost}
@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
@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"}