frontend kínlódás

This commit is contained in:
Roo
2026-03-31 06:20:43 +00:00
parent 2508ae7452
commit c7cbe60976
46 changed files with 6091 additions and 136 deletions

View File

@@ -7,10 +7,12 @@ from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_, distinct
from sqlalchemy.orm import selectinload
from fastapi import HTTPException
from app.models import Asset, AssetAssignment, AssetTelemetry, AssetFinancials, VehicleModelDefinition
from app.models.identity import User
from app.models.vehicle.history import LogSeverity
from app.schemas.asset import AssetCreate
from app.services.config_service import config
from app.services.gamification_service import GamificationService
from app.services.security_service import security_service
@@ -33,20 +35,23 @@ class AssetService:
db: AsyncSession,
user_id: int,
org_id: int,
vin: Optional[str] = None,
license_plate: Optional[str] = None,
catalog_id: int = None,
asset_data: AssetCreate,
draft: bool = False
):
"""
Intelligens Jármű Rögzítés:
Ha új: létrehozza.
Intelligens Jármű Rögzítés - Thick Digital Twin támogatással:
Ha új: létrehozza a teljes technikai adatokkal.
Ha már létezik: Transzfer folyamatot indít.
Ha draft=True vagy VIN hiányzik: draft státuszban hozza létre.
Automatikus státusz meghatározás az adatkomplettség alapján.
Catalog Snapshot Sync: Ha catalog_id van, betölti a hiányzó technikai adatokat.
"""
try:
vin_clean = vin.strip().upper() if vin else None
license_plate_clean = license_plate.strip().upper() if license_plate else None
# Clean input data
vin_clean = asset_data.vin.strip().upper() if asset_data.vin else None
license_plate_clean = asset_data.license_plate.strip().upper()
# Use organization_id from asset_data if provided, otherwise use the passed org_id
target_org_id = asset_data.organization_id or org_id
# 1. ADMIN LIMIT ELLENŐRZÉS (csak aktív járművek számítanak)
user_stmt = select(User).where(User.id == user_id)
@@ -54,17 +59,35 @@ class AssetService:
# Get vehicle limit using the new function that checks both user AND organization limits
# Returns the HIGHER value of user-specific and organization-specific limits
allowed_limit = await AssetService.get_user_vehicle_limit(db, user_id, org_id)
allowed_limit = await AssetService.get_user_vehicle_limit(db, user_id, target_org_id)
# Csak aktív járművek számítanak a limitbe (draft-ok nem)
count_stmt = select(func.count(Asset.id)).where(
Asset.current_organization_id == org_id,
Asset.current_organization_id == target_org_id,
Asset.status == "active"
)
current_count = (await db.execute(count_stmt)).scalar()
if current_count >= allowed_limit and not draft:
raise ValueError(f"Limit túllépés! A csomagod {allowed_limit} autót engedélyez.")
# Determine status based on data completeness (use Pydantic validator's logic)
# Check the 5 core fields: license_plate, brand, model, vehicle_class, fuel_type
core_fields_complete = all([
asset_data.license_plate and asset_data.license_plate.strip(),
asset_data.brand and asset_data.brand.strip(),
asset_data.model and asset_data.model.strip(),
asset_data.vehicle_class and asset_data.vehicle_class.strip(),
asset_data.fuel_type and asset_data.fuel_type.strip()
])
# Determine final status
if draft:
status = "draft"
elif not core_fields_complete:
status = "draft"
else:
status = "active"
if current_count >= allowed_limit and status == "active":
raise ValueError(f"Limit túllépés! A csomagod {allowed_limit} aktív autót engedélyez.")
# 2. LÉTEZIK-E MÁR A JÁRMŰ? (csak ha van VIN)
existing_asset = None
@@ -74,41 +97,95 @@ class AssetService:
if existing_asset:
# HA MÁR A JELENLEGI SZERVEZETNÉL VAN
if existing_asset.current_organization_id == org_id:
if existing_asset.current_organization_id == target_org_id:
raise ValueError("Ez a jármű már a te garázsodban van.")
# TRANSZFER FOLYAMAT INDÍTÁSA
return await AssetService.initiate_ownership_transfer(
db, existing_asset, user_id, org_id, license_plate_clean or ""
db, existing_asset, user_id, target_org_id, license_plate_clean or ""
)
# 3. ÚJ JÁRMŰ LÉTREHOZÁSA (Standard vagy Draft Flow)
# Dynamic Gatekeeper Logic: If both vin and catalog_id are missing, status = 'draft'
# If core data is provided (either vin OR catalog_id), status = 'active'
# Also respect the draft parameter if explicitly set
if draft:
status = "draft"
elif not vin_clean and not catalog_id:
status = "draft"
else:
status = "active"
# 3. CATALOG SNAPSHOT SYNC - Ha catalog_id van, betöltjük a hiányzó technikai adatokat
catalog_data = {}
if asset_data.catalog_id:
catalog_stmt = select(VehicleModelDefinition).where(
VehicleModelDefinition.id == asset_data.catalog_id
)
catalog = (await db.execute(catalog_stmt)).scalar_one_or_none()
if catalog:
# Map catalog fields to asset fields (only if not already provided by user)
catalog_data = {
'brand': catalog.make if not asset_data.brand else None,
'model': catalog.marketing_name if not asset_data.model else None,
'vehicle_class': catalog.vehicle_class if not asset_data.vehicle_class else None,
'fuel_type': catalog.fuel_type if not asset_data.fuel_type else None,
'power_kw': catalog.power_kw if not asset_data.power_kw else None,
'engine_capacity': catalog.engine_capacity if not asset_data.engine_capacity else None,
'euro_classification': catalog.euro_class if not asset_data.euro_classification else None,
'body_type': catalog.body_type if not asset_data.trim_level else None,
}
# Remove None values
catalog_data = {k: v for k, v in catalog_data.items() if v is not None}
# 4. ÚJ JÁRMŰ LÉTREHOZÁSA - Thick Digital Twin
# Először összeállítjuk az összes adatot (user input + catalog snapshot)
# Get default vehicle class from config if not provided
default_vehicle_class = await config.get_setting(db, "DEFAULT_VEHICLE_CLASS", default="car")
asset_fields = {
'vin': vin_clean,
'license_plate': license_plate_clean,
'catalog_id': asset_data.catalog_id,
'current_organization_id': target_org_id,
'owner_person_id': user.person_id,
'owner_org_id': asset_data.owner_org_id or target_org_id,
'operator_org_id': asset_data.operator_org_id,
'status': status,
'individual_equipment': asset_data.individual_equipment or {},
'created_at': datetime.utcnow(),
new_asset = Asset(
vin=vin_clean,
license_plate=license_plate_clean,
catalog_id=catalog_id,
current_organization_id=org_id,
owner_person_id=user.person_id,
owner_org_id=org_id,
status=status,
individual_equipment={},
created_at=datetime.utcnow()
)
# Classification
'brand': asset_data.brand or catalog_data.get('brand'),
'model': asset_data.model or catalog_data.get('model'),
'vehicle_class': asset_data.vehicle_class or catalog_data.get('vehicle_class') or default_vehicle_class,
'trim_level': asset_data.trim_level,
# Technical Specs
'fuel_type': asset_data.fuel_type or catalog_data.get('fuel_type'),
'engine_capacity': asset_data.engine_capacity or catalog_data.get('engine_capacity'),
'power_kw': asset_data.power_kw or catalog_data.get('power_kw'),
'torque_nm': asset_data.torque_nm,
'cylinder_layout': asset_data.cylinder_layout,
'transmission_type': asset_data.transmission_type,
'drive_type': asset_data.drive_type,
'euro_classification': asset_data.euro_classification or catalog_data.get('euro_classification'),
# Physical Dimensions
'curb_weight': asset_data.curb_weight,
'max_weight': asset_data.max_weight,
'cargo_volume_x': asset_data.cargo_volume_x,
'cargo_volume_y': asset_data.cargo_volume_y,
'door_count': asset_data.door_count,
'seat_count': asset_data.seat_count,
# Equipment
'roof_type': asset_data.roof_type,
'audio_system_type': asset_data.audio_system_type,
# Timeline
'year_of_manufacture': asset_data.year_of_manufacture,
'first_registration_date': asset_data.first_registration_date,
}
# Remove None values from the dictionary
asset_fields = {k: v for k, v in asset_fields.items() if v is not None}
new_asset = Asset(**asset_fields)
db.add(new_asset)
await db.flush()
# Digitális Iker Alapmodulok
db.add(AssetAssignment(asset_id=new_asset.id, organization_id=org_id, status="active"))
db.add(AssetAssignment(asset_id=new_asset.id, organization_id=target_org_id, status="active"))
db.add(AssetTelemetry(asset_id=new_asset.id))
db.add(AssetFinancials(
asset_id=new_asset.id,
@@ -122,7 +199,7 @@ class AssetService:
await GamificationService.award_points(db, user_id, int(reward), "NEW_ASSET_REG")
# Check if this is user's first vehicle and award "First Car" badge
await AssetService._award_first_car_badge(db, user_id, org_id)
await AssetService._award_first_car_badge(db, user_id, target_org_id)
await db.commit()
return new_asset
@@ -207,11 +284,14 @@ class AssetService:
return [make for make in makes if make] # Filter out None/empty
@staticmethod
async def get_models(db: AsyncSession, make: str) -> List[str]:
"""Get all distinct models for a given make."""
async def get_models(db: AsyncSession, make: str, vehicle_class: str = None) -> List[str]:
"""Get all distinct models for a given make, optionally filtered by vehicle_class."""
stmt = select(distinct(VehicleModelDefinition.marketing_name)).where(
VehicleModelDefinition.make == make
).order_by(VehicleModelDefinition.marketing_name)
)
if vehicle_class:
stmt = stmt.where(VehicleModelDefinition.vehicle_class == vehicle_class)
stmt = stmt.order_by(VehicleModelDefinition.marketing_name)
result = await db.execute(stmt)
models = result.scalars().all()
return [model for model in models if model]