Járműkezelés majdnem kész frontend
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py
|
||||
import uuid
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from sqlalchemy import select, desc, text, or_
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.db.session import get_db
|
||||
@@ -14,10 +15,47 @@ from app.models.identity import User
|
||||
from app.services.cost_service import cost_service
|
||||
from app.services.asset_service import AssetService
|
||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
|
||||
from app.schemas.asset import AssetResponse, AssetCreate
|
||||
from app.schemas.asset import AssetResponse, AssetCreate, AssetUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/vehicles/check")
|
||||
async def check_vehicle_exists(
|
||||
license_plate: str,
|
||||
vin: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Gyors, dedikált ellenőrző végpont a frontend számára.
|
||||
Ellenőrzi, hogy a megadott rendszám VAGY alvázszám (VIN) már létezik-e
|
||||
az adatbázisban, és visszaadja a tulajdonos ID-ját, ha létezik.
|
||||
|
||||
GET /api/v1/assets/vehicles/check?license_plate=ABC123&vin=WBA12345678901234
|
||||
"""
|
||||
normalized_plate = license_plate.strip().upper()
|
||||
|
||||
conditions = [Asset.license_plate == normalized_plate]
|
||||
if vin:
|
||||
normalized_vin = vin.strip().upper()
|
||||
conditions.append(Asset.vin == normalized_vin)
|
||||
|
||||
stmt = select(Asset).where(or_(*conditions))
|
||||
result = await db.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
return {
|
||||
"exists": True,
|
||||
"owner_id": existing.owner_person_id,
|
||||
"owner_org_id": existing.owner_org_id
|
||||
}
|
||||
|
||||
return {"exists": False, "owner_id": None, "owner_org_id": None}
|
||||
|
||||
@router.get("/vehicles", response_model=List[AssetResponse])
|
||||
async def get_user_vehicles(
|
||||
skip: int = 0,
|
||||
@@ -33,9 +71,21 @@ async def get_user_vehicles(
|
||||
|
||||
Garage-centric logic: In corporate mode, only returns vehicles physically
|
||||
parked in the active organization's garages (branches).
|
||||
|
||||
Sorting: is_primary vehicles appear first (based on individual_equipment JSONB),
|
||||
then ordered by created_at descending.
|
||||
"""
|
||||
from sqlalchemy import or_, select
|
||||
|
||||
# JSONB ordering: is_primary (True) first, then by created_at desc
|
||||
# individual_equipment -> 'is_primary' path, cast to text then boolean for ordering
|
||||
is_primary_expr = Asset.individual_equipment['is_primary'].astext.cast(
|
||||
# Use SQL cast to boolean via text expression
|
||||
type_=None
|
||||
)
|
||||
# Use a raw SQL expression for JSONB boolean ordering
|
||||
order_expr = text("(individual_equipment->>'is_primary')::boolean DESC NULLS LAST")
|
||||
|
||||
if current_user.scope_id is None:
|
||||
# Personal mode: only show vehicles owned/operated personally (no organization)
|
||||
stmt = (
|
||||
@@ -50,7 +100,7 @@ async def get_user_vehicles(
|
||||
Asset.operator_person_id == current_user.person_id
|
||||
)
|
||||
)
|
||||
.order_by(Asset.created_at.desc())
|
||||
.order_by(order_expr, Asset.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.options(selectinload(Asset.catalog))
|
||||
@@ -88,7 +138,7 @@ async def get_user_vehicles(
|
||||
Asset.branch_id.in_(branch_ids),
|
||||
Asset.status == "active"
|
||||
)
|
||||
.order_by(Asset.created_at.desc())
|
||||
.order_by(order_expr, Asset.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.options(selectinload(Asset.catalog))
|
||||
@@ -200,20 +250,48 @@ async def create_or_claim_vehicle(
|
||||
|
||||
A végpont a következőket végzi:
|
||||
- Ellenőrzi a felhasználó járműlimitjét
|
||||
- Ha a VIN már létezik, tulajdonjog-átvitelt kezdeményez
|
||||
- OKOS DUPLEX VÉDELEM: license_plate alapján ellenőrzi a duplikációt
|
||||
- Szabály A: Ha a rendszám létezik ÉS a tulajdonos ugyanaz → 400 hiba
|
||||
- Szabály B: Ha a rendszám létezik, de más a tulajdonos → data_status='draft'
|
||||
- Ha új, létrehozza a járművet és a kapcsolódó digitális ikreket
|
||||
- XP jutalom adása a felhasználónak
|
||||
"""
|
||||
try:
|
||||
# ── OKOS DUPLEX VÉDELEM: license_plate ellenőrzés ──
|
||||
license_plate_clean = payload.license_plate.strip().upper()
|
||||
|
||||
# Check if any existing asset has this license plate
|
||||
existing_stmt = select(Asset).where(
|
||||
Asset.license_plate == license_plate_clean
|
||||
)
|
||||
existing_result = await db.execute(existing_stmt)
|
||||
existing_asset = existing_result.scalar_one_or_none()
|
||||
|
||||
if existing_asset:
|
||||
# Szabály A: Saját jármű duplikációja
|
||||
if existing_asset.owner_person_id == current_user.person_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Ez a jármű már szerepel a garázsodban!"
|
||||
)
|
||||
|
||||
# Szabály B: Tulajdonosváltás / Átmeneti állapot
|
||||
# A rendszám létezik, de más a tulajdonosa
|
||||
# Kényszerítsük draft állapotba, jelezve az átmeneti státuszt
|
||||
payload.data_status = "draft"
|
||||
logger.info(
|
||||
f"License plate {license_plate_clean} exists with different owner "
|
||||
f"(owner_person_id={existing_asset.owner_person_id} != "
|
||||
f"current_user.person_id={current_user.person_id}). "
|
||||
f"Setting data_status='draft' for transfer scenario."
|
||||
)
|
||||
|
||||
# Determine organization ID based on user's active scope (garage isolation)
|
||||
# The owner_org_id MUST be set to the current_user.scope_id.
|
||||
# If the scope is null, it stays null (personal).
|
||||
org_id = None
|
||||
if current_user.scope_id is not None:
|
||||
try:
|
||||
org_id = int(current_user.scope_id)
|
||||
except (ValueError, TypeError):
|
||||
# If scope_id is not a valid integer, treat as personal (no organization)
|
||||
pass
|
||||
|
||||
asset = await AssetService.create_or_claim_vehicle(
|
||||
@@ -233,6 +311,88 @@ async def create_or_claim_vehicle(
|
||||
raise HTTPException(status_code=500, detail="Belső szerverhiba a jármű létrehozásakor")
|
||||
|
||||
|
||||
@router.put("/vehicles/{asset_id}", response_model=AssetResponse)
|
||||
async def update_vehicle(
|
||||
asset_id: uuid.UUID,
|
||||
payload: AssetUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Meglévő jármű adatainak frissítése.
|
||||
|
||||
Csak a megadott mezőket frissíti (partial update).
|
||||
Ellenőrzi, hogy a felhasználónak van-e jogosultsága a járműhöz.
|
||||
"""
|
||||
from sqlalchemy import or_
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
|
||||
# Get user's organization memberships
|
||||
org_stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
)
|
||||
org_result = await db.execute(org_stmt)
|
||||
user_org_ids = [row[0] for row in org_result.all()]
|
||||
|
||||
# Find the asset
|
||||
stmt = (
|
||||
select(Asset)
|
||||
.where(
|
||||
Asset.id == asset_id,
|
||||
or_(
|
||||
Asset.owner_person_id == current_user.person_id,
|
||||
Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False,
|
||||
Asset.operator_person_id == current_user.person_id,
|
||||
Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False
|
||||
)
|
||||
)
|
||||
.options(selectinload(Asset.catalog))
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
asset = result.scalar_one_or_none()
|
||||
|
||||
if not asset:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Asset not found or you don't have permission to modify it"
|
||||
)
|
||||
|
||||
try:
|
||||
# Build update dict from non-None fields
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
|
||||
# Handle is_primary → individual_equipment JSONB
|
||||
if 'is_primary' in update_data:
|
||||
equipment = dict(asset.individual_equipment or {})
|
||||
equipment['is_primary'] = update_data.pop('is_primary')
|
||||
update_data['individual_equipment'] = equipment
|
||||
|
||||
# Handle individual_equipment merge
|
||||
if 'individual_equipment' in update_data and update_data['individual_equipment'] is not None:
|
||||
existing_equipment = dict(asset.individual_equipment or {})
|
||||
existing_equipment.update(update_data['individual_equipment'])
|
||||
update_data['individual_equipment'] = existing_equipment
|
||||
|
||||
# Update the asset
|
||||
for field, value in update_data.items():
|
||||
if value is not None and hasattr(asset, field):
|
||||
setattr(asset, field, value)
|
||||
|
||||
asset.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(asset)
|
||||
|
||||
return asset
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Vehicle update error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Belső szerverhiba a jármű frissítésekor")
|
||||
|
||||
|
||||
@router.get("/{asset_id}/maintenance", response_model=List[AssetCostResponse])
|
||||
async def list_maintenance_records(
|
||||
asset_id: uuid.UUID,
|
||||
|
||||
Reference in New Issue
Block a user