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,95 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
# Cím beállítása
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def build_db():
print(f"🔌 Kapcsolódás a rendszerhez...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("🏗️ 'DATA' séma előkészítése...")
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS data;"))
# 1. FELHASZNÁLÓK TÁBLA (Céges/Magán logika)
print("👤 Users tábla létrehozása...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) DEFAULT 'PRIVATE', -- 'PRIVATE', 'COMPANY', 'ADMIN'
company_name VARCHAR(255), -- Ha cég
tax_number VARCHAR(50), -- Ha cég
is_active BOOLEAN DEFAULT FALSE, -- Email megerősítésig
created_at TIMESTAMP DEFAULT NOW()
);
"""))
# 2. JÁRMŰ TÖRZS (A Fizikai Vas - Tulajdonos nélkül!)
print("🚗 Vehicles tábla létrehozása (A Vas)...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.vehicles (
id SERIAL PRIMARY KEY,
model_id INTEGER REFERENCES ref.vehicle_models(id),
vin VARCHAR(50) UNIQUE NOT NULL, -- ALVÁZSZÁM (Az igazi kulcs)
current_plate VARCHAR(20) NOT NULL, -- A mostani rendszám
production_year INTEGER,
created_at TIMESTAMP DEFAULT NOW()
);
"""))
# 3. ÉLETÚT ÉS JOGOSULTSÁG (A Történelem)
print("📜 History tábla létrehozása (Kié mikor volt?)...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.vehicle_history (
id SERIAL PRIMARY KEY,
vehicle_id INTEGER REFERENCES data.vehicles(id),
user_id INTEGER REFERENCES data.users(id),
role VARCHAR(20) NOT NULL, -- 'OWNER' (Tulaj), 'DRIVER' (Sofőr), 'LEASE' (Lízingelő)
start_date DATE NOT NULL, -- Mikor vette át?
end_date DATE, -- Mikor adta le? (Ha NULL, akkor nála van!)
start_mileage INTEGER, -- Óraállás átvételkor
end_mileage INTEGER, -- Óraállás leadáskor
is_active BOOLEAN GENERATED ALWAYS AS (end_date IS NULL) STORED -- Segédmező a gyors kereséshez
);
"""))
# 4. KÖLTSÉGEK (Szeparált pénzügyek)
print("💸 Costs tábla létrehozása...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.costs (
id SERIAL PRIMARY KEY,
vehicle_id INTEGER REFERENCES data.vehicles(id),
user_id INTEGER REFERENCES data.users(id), -- Ki fizette? (Csak ő láthatja!)
cost_type VARCHAR(50) NOT NULL, -- FUEL, SERVICE, TAX, INSURANCE, LEASE...
amount DECIMAL(10, 2) NOT NULL,
date DATE NOT NULL,
mileage_at_cost INTEGER,
description TEXT,
document_url VARCHAR(255), -- Fotó linkje
created_at TIMESTAMP DEFAULT NOW()
);
"""))
print("✅ KÉSZ! A Professzionális Adatmodell felépült.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(build_db())

View File

@@ -0,0 +1,51 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
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"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def check_data():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("\n🚗 A TE GARÁZSOD (User ID: 1):")
print("-" * 80)
# Ez a lekérdezés összeköti a Történelmet (History), a Vasat (Vehicle) és a Katalógust (Model)
query = text("""
SELECT
vh.role,
vh.start_date,
v.vin,
v.current_plate,
m.name as brand,
mo.model_name
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;
""")
result = await conn.execute(query)
rows = result.fetchall()
if not rows:
print("📭 A garázs üres.")
else:
for r in rows:
print(f"🔹 {r.brand} {r.model_name} | 🆔 {r.vin} | 🔢 {r.current_plate}")
print(f" Jogviszony: {r.role} | Kezdete: {r.start_date}")
print("-" * 80)
await engine.dispose()
if __name__ == "__main__":
asyncio.run(check_data())

View File

@@ -0,0 +1,35 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
# Adatbázis elérés
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def create_user():
print(f"🔌 Kapcsolódás...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("👤 1-es számú felhasználó beszúrása...")
# Kényszerítjük az ID=1-et, hogy passzoljon a main.py-hoz
await conn.execute(text("""
INSERT INTO data.users (id, email, password_hash, role, is_active)
VALUES (1, 'demo@user.com', 'dummy_hash', 'PRIVATE', TRUE)
ON CONFLICT (id) DO NOTHING;
"""))
# Frissítjük a számlálót, hogy a következő user ID 2 legyen (ne akadjon össze)
await conn.execute(text("SELECT setval('data.users_id_seq', (SELECT MAX(id) FROM data.users));"))
print("✅ KÉSZ! A Demo User (ID: 1) létezik.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(create_user())

View File

@@ -0,0 +1,42 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
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"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def hire_driver():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("👤 Kovács János (User ID: 2) létrehozása...")
# Létrehozzuk a User-t
await conn.execute(text("""
INSERT INTO data.users (id, email, password_hash, role, country, default_currency, is_active)
VALUES (2, 'sofor@ceg.hu', 'hash123', 'PRIVATE', 'HU', 'HUF', TRUE)
ON CONFLICT (id) DO NOTHING;
"""))
# Frissítjük a sorrendet
await conn.execute(text("SELECT setval('data.users_id_seq', (SELECT MAX(id) FROM data.users));"))
print("🤝 Hozzárendelés a Te cégedhez (ID: 1)...")
# Betesszük a fleet_members táblába
await conn.execute(text("""
INSERT INTO data.fleet_members (user_id, owner_id, role)
VALUES (2, 1, 'DRIVER')
ON CONFLICT (user_id, owner_id) DO NOTHING;
"""))
print("✅ KÉSZ! Kovács János mostantól a csapatod tagja.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(hire_driver())

View File

@@ -0,0 +1,17 @@
services:
service_finder_api:
build: .
container_name: service_finder_api
env_file:
- .env
networks:
- existing_net
ports:
- "8000:8000"
restart: unless-stopped
networks:
existing_net:
external: true
name: docker-server_internal_net

View File

@@ -0,0 +1,81 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
# Cím összeállítása (Ugyanaz a logika, mint a main.py-ban)
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def init_db():
print(f"🔌 Kapcsolódás ide: {DATABASE_URL}")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("🏗️ Séma és Táblák létrehozása...")
# 1. Séma létrehozása
await conn.execute(text("CREATE SCHEMA IF NOT EXISTS ref;"))
# 2. Márka tábla
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ref.vehicle_makes (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE
);
"""))
# 3. Modell tábla
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ref.vehicle_models (
id SERIAL PRIMARY KEY,
make_id INTEGER REFERENCES ref.vehicle_makes(id),
model_name VARCHAR(100) NOT NULL,
category VARCHAR(50)
);
"""))
print("🧹 Meglévő adatok törlése (Tiszta lap)...")
await conn.execute(text("TRUNCATE TABLE ref.vehicle_models, ref.vehicle_makes RESTART IDENTITY CASCADE;"))
print("🚗 Adatok beszúrása...")
# Adatok
makes = ["BMW", "Audi", "Mercedes-Benz", "Toyota", "Honda", "Suzuki", "Volkswagen"]
models = [
("BMW", "3 Series", "Autó"), ("BMW", "5 Series", "Autó"), ("BMW", "X5", "Autó"), ("BMW", "R 1250 GS", "Motor"),
("Audi", "A3", "Autó"), ("Audi", "A4", "Autó"), ("Audi", "A6", "Autó"), ("Audi", "Q5", "Autó"),
("Mercedes-Benz", "C-Class", "Autó"), ("Mercedes-Benz", "E-Class", "Autó"), ("Mercedes-Benz", "S-Class", "Autó"), ("Mercedes-Benz", "G-Class", "Autó"),
("Toyota", "Corolla", "Autó"), ("Toyota", "Yaris", "Autó"), ("Toyota", "RAV4", "Autó"), ("Toyota", "Hilux", "Autó"),
("Honda", "Civic", "Autó"), ("Honda", "CR-V", "Autó"), ("Honda", "Africa Twin", "Motor"), ("Honda", "CB500X", "Motor"),
("Suzuki", "Swift", "Autó"), ("Suzuki", "Vitara", "Autó"), ("Suzuki", "S-Cross", "Autó"), ("Suzuki", "DL 650 V-Strom", "Motor"),
("Volkswagen", "Golf", "Autó"), ("Volkswagen", "Passat", "Autó"), ("Volkswagen", "Polo", "Autó"), ("Volkswagen", "Tiguan", "Autó")
]
# Márkák beszúrása
for make in makes:
await conn.execute(text(f"INSERT INTO ref.vehicle_makes (name) VALUES ('{make}') ON CONFLICT DO NOTHING;"))
# Modellek beszúrása (kicsit trükkös, mert kell a make_id)
for brand, model, cat in models:
# Megkeressük az ID-t
result = await conn.execute(text(f"SELECT id FROM ref.vehicle_makes WHERE name = '{brand}'"))
make_id = result.scalar()
# Beszúrjuk a modellt
await conn.execute(text(f"""
INSERT INTO ref.vehicle_models (make_id, model_name, category)
VALUES ({make_id}, '{model}', '{cat}')
"""))
print("✅ KÉSZ! Az adatbázis feltöltve.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(init_db())

View File

@@ -0,0 +1,61 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from dotenv import load_dotenv
load_dotenv()
# Cím beállítása
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def inspect_schema():
print(f"🔎 Kapcsolódás az adatbázishoz...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
# SQL lekérdezés a rendszer katalógusból (information_schema)
# Ez megmondja milyen táblák és oszlopok léteznek
query = text("""
SELECT
table_schema,
table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema IN ('public', 'ref', 'data')
ORDER BY table_schema, table_name, ordinal_position;
""")
result = await conn.execute(query)
rows = result.fetchall()
if not rows:
print("⚠️ Nem találtam táblákat a 'ref' vagy 'data' sémákban!")
current_table = ""
for row in rows:
schema = row.table_schema
table = row.table_name
full_table_name = f"{schema}.{table}"
# Ha új táblához érünk, kiírjuk a nevét
if full_table_name != current_table:
print(f"\n📦 TÁBLA: {full_table_name.upper()}")
print("-" * 50)
print(f"{'OSZLOP NÉV':<20} | {'TÍPUS':<15} | {'KÖTELEZŐ?'}")
print("-" * 50)
current_table = full_table_name
# Oszlop adatok
req = "IGEN" if row.is_nullable == 'NO' else "nem"
print(f"{row.column_name:<20} | {row.data_type:<15} | {req}")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(inspect_schema())

View File

@@ -0,0 +1,69 @@
import asyncio
from sqlalchemy import inspect, text
from sqlalchemy.ext.asyncio import create_async_engine
# CSERÉLD KI a saját adataidra, ha nem ezeket használod!
DATABASE_URL = "postgresql+asyncpg://kincses:MiskociA74@postgres-db:5432/service_finder"
async def run_inspection():
# Aszinkron motor létrehozása
engine = create_async_engine(DATABASE_URL)
async with engine.connect() as conn:
def do_inspect(sync_conn):
inspector = inspect(sync_conn)
# 1. Sémák lekérése (amiket mi hoztunk létre + public)
target_schemas = ['public', 'data', 'ref']
all_schemas = inspector.get_schema_names()
schemas_to_check = [s for s in target_schemas if s in all_schemas]
print(f"{'='*60}")
print(f" JÁRMŰNYILVÁNTARTÓ RENDSZER - ADATBÁZIS AUDIT")
print(f"{'='*60}")
for schema in schemas_to_check:
print(f"\n--- SÉMA: {schema.upper()} ---")
tables = inspector.get_table_names(schema=schema)
if not tables:
print(" (Nincsenek táblák ebben a sémában)")
continue
for table_name in tables:
print(f"\n[Tábla: {schema}.{table_name}]")
# Oszlopok részletei
columns = inspector.get_columns(table_name, schema=schema)
for col in columns:
pk = " [PK]" if col['primary_key'] else ""
nullable = "NULL" if col['nullable'] else "NOT NULL"
print(f" L {col['name']:<15} | {str(col['type']):<12} | {nullable}{pk}")
# Idegen kulcsok (Kapcsolatok)
fks = inspector.get_foreign_keys(table_name, schema=schema)
for fk in fks:
constrained = fk['constrained_columns']
referred_schema = fk['referred_schema']
referred_table = fk['referred_table']
referred_cols = fk['referred_columns']
print(f" --> Kapcsolat: {constrained} -> {referred_schema}.{referred_table}({referred_cols})")
# Mivel az SQLAlchemy inspector alapvetően szinkron, run_sync-et használunk
await conn.run_sync(do_inspect)
# 2. Extra: Kiterjesztések ellenőrzése
print(f"\n{'='*60}")
print(" AKTÍV POSTGRESQL KITERJESZTÉSEK:")
result = await conn.execute(text("SELECT extname FROM pg_extension;"))
for row in result:
print(f" - {row[0]}")
print(f"{'='*60}")
await engine.dispose()
if __name__ == "__main__":
try:
asyncio.run(run_inspection())
except Exception as e:
print(f"HIBA TÖRTÉNT: {e}")

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,120 @@
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, validator
from typing import Optional
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
import os
from datetime import date
from dotenv import load_dotenv
load_dotenv()
# --- 1. ADATBÁZIS KAPCSOLAT ---
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")
# --- 2. ADATMODELLEK (VALIDÁCIÓVAL) ---
class VehicleRegister(BaseModel):
model_id: int
vin: str # Alvázszám (KÖTELEZŐ!)
plate: str # Rendszám
mileage: int # Km óra
purchase_date: date
role: str = "OWNER" # Alapértelmezett: Tulajdonos
# Adatőr: Automatikus nagybetűsítés és tisztítás
@validator('vin')
def clean_vin(cls, v):
if len(v) < 5: raise ValueError('Az alvázszám túl rövid!')
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('A kilométer nem lehet negatív!')
return v
# --- 3. VÉGPONTOK ---
# Lista lekérése (Katalógus)
@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()]
# ÚJ: JÁRMŰ REGISZTRÁCIÓ (A Nagy Logika)
@app.post("/api/register")
async def register_vehicle(data: VehicleRegister):
async with AsyncSessionLocal() as session:
async with session.begin(): # Tranzakció indítása
try:
# 1. Megnézzük, létezik-e már ez a VAS (Alvázszám alapján)?
check_query = text("SELECT id FROM data.vehicles WHERE vin = :vin")
result = await session.execute(check_query, {"vin": data.vin})
existing_car_id = result.scalar()
vehicle_id = existing_car_id
# 2. HA NEM LÉTEZIK -> Létrehozzuk a VASAT
if not vehicle_id:
print(f"🆕 Új autó az adatbázisban: {data.vin}")
insert_car = text("""
INSERT INTO data.vehicles (model_id, vin, current_plate)
VALUES (:mid, :vin, :plt)
RETURNING id
""")
res = await session.execute(insert_car, {
"mid": data.model_id, "vin": data.vin, "plt": data.plate
})
vehicle_id = res.scalar()
else:
print(f"♻️ Létező autó átvétele: {data.vin} (ID: {vehicle_id})")
# Itt később lezárhatjuk az előző tulajdonos history-ját!
# 3. BEJEGYZÉS A TÖRTÉNELEMBE (HISTORY)
# Ez köti össze a User-t (most fixen ID=1) az Autóval
insert_history = text("""
INSERT INTO data.vehicle_history
(vehicle_id, user_id, role, start_date, start_mileage)
VALUES (:vid, 1, :role, :s_date, :s_mil)
""")
await session.execute(insert_history, {
"vid": vehicle_id,
"role": data.role,
"s_date": data.purchase_date,
"s_mil": data.mileage
})
return {"status": "success", "message": "Jármű sikeresen rögzítve a flottában!"}
except Exception as e:
print(f"❌ Hiba: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Frontend kiszolgálása
@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,49 @@
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
import os
from dotenv import load_dotenv
load_dotenv()
app = FastAPI(title="Service Finder API")
# --- A DUPLA JAVÍTÁS ---
raw_url = os.getenv("DATABASE_URL")
if not raw_url:
# Vészhelyzeti fallback (ha nem lenne .env)
raw_url = "postgresql://admin:PASSWORD_111@postgres-db:5432/service_finder"
# 1. Javítás: Driver csere (asyncpg)
fixed_url = raw_url.replace("postgresql://", "postgresql+asyncpg://")
# 2. Javítás: Adatbázis név csere (Ha véletlenül _db a vége, levágjuk)
fixed_url = fixed_url.replace("/service_finder_db", "/service_finder")
print(f"🔧 VÉGLEGES ADATBÁZIS CÍM: {fixed_url}")
# Kapcsolódás a javított címmel
engine = create_async_engine(fixed_url)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@app.get("/")
async def root():
return {"message": "Service Finder API működik! 🚀"}
@app.get("/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
try:
query = text("""
SELECT 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
""")
result = await session.execute(query)
vehicles = result.fetchall()
return [{"brand": r.brand, "model": r.model_name, "category": r.category} for r in vehicles]
except Exception as e:
return {"error": str(e)}

View File

@@ -0,0 +1,52 @@
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
import os
from dotenv import load_dotenv
# Betöltjük a környezeti változókat
load_dotenv()
app = FastAPI(title="Service Finder API")
# --- A FIX ---
# Kiolvassuk a címet
raw_url = os.getenv("DATABASE_URL")
# Ha nincs beállítva, adunk egy vészhelyzeti alapértelmezettet
if not raw_url:
print("⚠️ FIGYELEM: Nincs DATABASE_URL, alapértelmezett használata!")
# Itt a konténer belső nevét (postgres-db) használjuk
raw_url = "postgresql://admin:MiskociA74@postgres-db:5432/service_finder"
# A LÉNYEG: Ha hiányzik az '+asyncpg', hozzáadjuk!
if "asyncpg" not in raw_url:
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://")
print(f"🔧 URL javítva erre: {DATABASE_URL}")
else:
DATABASE_URL = raw_url
# Motor indítása a javított URL-lel
engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@app.get("/")
async def root():
return {"message": "Service Finder API működik! 🚀"}
@app.get("/vehicles")
async def get_vehicles():
async with AsyncSessionLocal() as session:
try:
query = text("""
SELECT 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
""")
result = await session.execute(query)
vehicles = result.fetchall()
return [{"brand": r.brand, "model": r.model_name, "category": r.category} for r in vehicles]
except Exception as e:
return {"error": str(e)}

View File

@@ -0,0 +1,71 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
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"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def seed_cost_types():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("🔨 Költség típusok tábla létrehozása...")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS ref.cost_types (
code VARCHAR(50) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
parent_code VARCHAR(50) REFERENCES ref.cost_types(code), -- Hierarchia!
is_active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0
);
"""))
print("📥 Adatok feltöltése...")
# Először a szülők (Főkategóriák)
parents = [
('FUEL', '⛽ Tankolás', 10),
('PURCHASE', '💰 Beszerzés / Pénzügy', 20),
('INSURANCE', '📄 Biztosítás', 30),
('TAX', '🏛️ Adó / Illeték', 40),
('SERVICE', '🔧 Szerviz', 50),
('OTHER', 'Egyéb', 99)
]
for p in parents:
await conn.execute(text("INSERT INTO ref.cost_types (code, name, sort_order) VALUES (:c, :n, :s) ON CONFLICT (code) DO NOTHING"), {"c": p[0], "n": p[1], "s": p[2]})
# Aztán a gyerekek (Alkategóriák)
children = [
# Beszerzés
('PURCHASE_PRICE', 'Vételár', 'PURCHASE'),
('FINANCE_LEASE', 'Lízing díj', 'PURCHASE'),
('FINANCE_LOAN', 'Hitel törlesztő', 'PURCHASE'),
('IMPORT_FEE', 'Honosítás / Regadó', 'PURCHASE'),
# Biztosítás
('INSURANCE_KGFB', 'Kötelező (KGFB)', 'INSURANCE'),
('INSURANCE_CASCO', 'Casco', 'INSURANCE'),
('INSURANCE_GAP', 'GAP', 'INSURANCE'),
('INSURANCE_ASSIST', 'Assistance', 'INSURANCE'),
# Adó
('TAX_WEIGHT', 'Gépjárműadó (Súlyadó)', 'TAX'),
('TAX_COMPANY', 'Cégautóadó', 'TAX'),
('TAX_TRANSFER', 'Vagyonszerzési illeték', 'TAX'),
('TAX_OTHER', 'Egyéb adó', 'TAX'),
# Szerviz
('SERVICE_MAINTENANCE', 'Kötelező karbantartás', 'SERVICE'),
('SERVICE_REPAIR', 'Javítás', 'SERVICE'),
('SERVICE_TIRE', 'Gumicsere', 'SERVICE'),
('SERVICE_MOT', 'Műszaki vizsga', 'SERVICE')
]
for c in children:
await conn.execute(text("INSERT INTO ref.cost_types (code, name, parent_code) VALUES (:c, :n, :p) ON CONFLICT (code) DO NOTHING"), {"c": c[0], "n": c[1], "p": c[2]})
print("✅ KÉSZ! A tudás most már az adatbázisban van, nem a HTML-ben.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(seed_cost_types())

View File

@@ -0,0 +1,46 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
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"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def build_audit_system():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("📜 Audit Log tábla létrehozása...")
# Ez a tábla sosem töröl, csak ír! (Append-only)
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.audit_logs (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES data.users(id), -- KI csinálta?
event_type VARCHAR(50) NOT NULL, -- MIT? (pl. ISSUE_REPORT, MILEAGE_UPDATE)
target_id INTEGER, -- MELYIK autón? (Vehicle ID)
old_value TEXT, -- MI volt előtte? (A visszaállításhoz)
new_value TEXT, -- MI lett utána?
details TEXT, -- Egyéb megjegyzés (pl. hiba leírása)
ip_address VARCHAR(45), -- Honnan? (Biztonság)
created_at TIMESTAMP DEFAULT NOW()
);
"""))
print("🚦 Jármű Státusz mezők hozzáadása...")
# Bővítjük a járműveket, hogy tárolják a hibát
await conn.execute(text("""
ALTER TABLE data.vehicles
ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'OK', -- OK, WARNING, CRITICAL
ADD COLUMN IF NOT EXISTS current_issue TEXT; -- A hiba leírása
"""))
print("✅ KÉSZ! A mindent látó szem (Audit Log) aktív.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(build_audit_system())

View File

@@ -0,0 +1,56 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
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"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def expand_categories():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("📥 Új Főkategóriák beszúrása...")
parents = [
('EQUIPMENT', '🛠️ Felszerelés / Extrák', 25), # Vételár után
('OPERATIONAL', '🅿️ Üzemeltetés / Út', 15), # Tankolás után
('FINE', '👮 Bírság / Büntetés', 90)
]
for p in parents:
await conn.execute(text("INSERT INTO ref.cost_types (code, name, sort_order) VALUES (:c, :n, :s) ON CONFLICT (code) DO NOTHING"), {"c": p[0], "n": p[1], "s": p[2]})
print("📥 Új Alkategóriák beszúrása...")
children = [
# Felszerelés (EQUIPMENT)
('EQUIP_SHELVING', 'Raktérburkolat / Polcrendszer', 'EQUIPMENT'),
('EQUIP_BRANDING', 'Matricázás / Fóliázás', 'EQUIPMENT'),
('EQUIP_TOWBAR', 'Vonóhorog / Tetőcsomagtartó', 'EQUIPMENT'),
('EQUIP_SAFETY', 'Kötelező tartozékok (Eü. csomag)', 'EQUIPMENT'),
('EQUIP_GPS', 'GPS / Nyomkövető hardver', 'EQUIPMENT'),
('EQUIP_WINTER_TIRE', 'Téli gumi szett (Beszerzés)', 'EQUIPMENT'),
# Üzemeltetés (OPERATIONAL)
('OP_PARKING', 'Parkolás', 'OPERATIONAL'),
('OP_TOLL', 'Útdíj / Matrica / Behajtás', 'OPERATIONAL'),
('OP_WASH', 'Mosás / Kozmetika', 'OPERATIONAL'),
('OP_ADBLUE', 'AdBlue', 'OPERATIONAL'),
('OP_EV_CHARGE', 'Elektromos töltés', 'OPERATIONAL'),
('OP_STORAGE', 'Gumi hotel / Tárolás', 'OPERATIONAL'),
# Bírság (FINE)
('FINE_SPEEDING', 'Gyorshajtás', 'FINE'),
('FINE_PARKING', 'Parkolási bírság', 'FINE'),
('FINE_ADMIN', 'Közigazgatási bírság', 'FINE')
]
for c in children:
await conn.execute(text("INSERT INTO ref.cost_types (code, name, parent_code) VALUES (:c, :n, :p) ON CONFLICT (code) DO NOTHING"), {"c": c[0], "n": c[1], "p": c[2]})
print("✅ KÉSZ! A költséglista most már teljeskörű.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(expand_categories())

View File

@@ -0,0 +1,46 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
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"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def upgrade_db():
print(f"🔌 Kapcsolódás...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("🌍 Felhasználói tábla bővítése (Ország + Alapértelmezett pénznem)...")
# 1. Users tábla bővítése
await conn.execute(text("""
ALTER TABLE data.users
ADD COLUMN IF NOT EXISTS country VARCHAR(2) DEFAULT 'HU',
ADD COLUMN IF NOT EXISTS default_currency VARCHAR(3) DEFAULT 'HUF';
"""))
print("💶 Költség tábla bővítése (Tranzakciós pénznem)...")
# 2. Costs tábla bővítése
await conn.execute(text("""
ALTER TABLE data.costs
ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'HUF';
"""))
# Frissítjük a meglévő Demo Usert (ID=1)
print("👤 Demo User beállítása: Magyarország / HUF")
await conn.execute(text("""
UPDATE data.users
SET country = 'HU', default_currency = 'HUF'
WHERE id = 1;
"""))
print("✅ KÉSZ! Az adatbázis mostantól támogatja a több pénznemet.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(upgrade_db())

View File

@@ -0,0 +1,26 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
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"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def add_doc_column():
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("📄 Dokumentum oszlop hozzáadása a Costs táblához...")
await conn.execute(text("""
ALTER TABLE data.costs
ADD COLUMN IF NOT EXISTS document_url VARCHAR(255);
"""))
print("✅ KÉSZ! Mehetnek a fájlok.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(add_doc_column())

View File

@@ -0,0 +1,58 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
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"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def upgrade_invites():
print(f"🔌 Kapcsolódás...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("📨 Invitations (Meghívók) tábla létrehozása...")
# Ez tárolja a függőben lévő meghívásokat
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.invitations (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
inviter_id INTEGER REFERENCES data.users(id), -- Ki hívta meg? (Cég)
role VARCHAR(20) NOT NULL, -- Milyen szerepre? (MANAGER, DRIVER)
access_level VARCHAR(20) DEFAULT 'FULL', -- A/B Sofőr szint
token VARCHAR(100) UNIQUE NOT NULL, -- A titkos link kódja
status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, ACCEPTED, EXPIRED
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP
);
"""))
print("🤝 Fleet Members (Többcéges tagság) tábla létrehozása...")
# Ez teszi lehetővé, hogy valaki több céghez is tartozzon
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS data.fleet_members (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES data.users(id), -- A Dolgozó
owner_id INTEGER REFERENCES data.users(id), -- A Cég / Tulajdonos
role VARCHAR(20) NOT NULL, -- FLEET_MANAGER, DRIVER
joined_at TIMESTAMP DEFAULT NOW(),
-- Egy ember egy cégnél csak egyszer szerepelhet
UNIQUE(user_id, owner_id)
);
"""))
# Takarítás: A régi 'parent_id' már nem kell, mert a fleet_members kiváltja
# De biztonságból egyelőre csak NULL-ra állítjuk, nem töröljük az oszlopot
# await conn.execute(text("UPDATE data.users SET parent_id = NULL;"))
print("✅ KÉSZ! A rendszer készen áll a biztonságos meghívókra.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(upgrade_invites())

View File

@@ -0,0 +1,61 @@
import asyncio
import os
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
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"
DATABASE_URL = raw_url.replace("postgresql://", "postgresql+asyncpg://").replace("/service_finder_db", "/service_finder")
async def upgrade_permissions():
print(f"🔌 Kapcsolódás...")
engine = create_async_engine(DATABASE_URL)
async with engine.begin() as conn:
print("👑 USERS tábla bővítése (Rendszer és Szervezeti szintek)...")
# 1. SYS_ROLE: Rendszergazda / Moderátor / User
await conn.execute(text("""
ALTER TABLE data.users
ADD COLUMN IF NOT EXISTS sys_role VARCHAR(20) DEFAULT 'USER';
"""))
# 2. HIERARCHIA: Ki a főnököd? (Parent User ID)
# Ha NULL, akkor ő a Cégtulajdonos (SuperUser). Ha van ID, akkor Alkalmazott.
await conn.execute(text("""
ALTER TABLE data.users
ADD COLUMN IF NOT EXISTS parent_id INTEGER REFERENCES data.users(id);
"""))
# 3. ORG_ROLE: Cégen belüli szerep (Owner, Fleet Manager, Employee)
await conn.execute(text("""
ALTER TABLE data.users
ADD COLUMN IF NOT EXISTS org_role VARCHAR(20) DEFAULT 'OWNER';
"""))
print("🚦 VEHICLE_HISTORY tábla bővítése (Sofőr jogosultságok)...")
# 4. ACCESS_LEVEL: Mit láthat a sofőr? (COST_MANAGER vs LOG_ONLY)
await conn.execute(text("""
ALTER TABLE data.vehicle_history
ADD COLUMN IF NOT EXISTS access_level VARCHAR(20) DEFAULT 'FULL';
-- Lehetséges értékek: 'FULL', 'COST_MANAGER', 'LOG_ONLY'
"""))
# --- DEMO ADATOK FRISSÍTÉSE ---
print("👤 Demo User (ID:1) kinevezése Rendszergazdának és Cégtulajdonosnak...")
await conn.execute(text("""
UPDATE data.users
SET sys_role = 'SYS_ADMIN', org_role = 'OWNER', parent_id = NULL
WHERE id = 1;
"""))
print("✅ KÉSZ! A jogosultsági mátrix beépítve az adatbázisba.")
await engine.dispose()
if __name__ == "__main__":
asyncio.run(upgrade_permissions())