From 0a3fd8de74e89b7ec5bf59f0dd1af4bc846d4e53 Mon Sep 17 00:00:00 2001 From: Roo Date: Thu, 11 Jun 2026 11:22:47 +0000 Subject: [PATCH] =?UTF-8?q?J=C3=A1rm=C5=B1kezel=C3=A9s=20majdnem=20k=C3=A9?= =?UTF-8?q?sz=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .roo/history.md | 137 +- backend/app/api/v1/endpoints/assets.py | 178 ++- backend/app/models/vehicle/asset.py | 18 + backend/app/schemas/asset.py | 59 + backend/app/services/asset_service.py | 10 +- .../dashboard/PrivateVehicleManager.vue | 238 ++++ .../components/dashboard/VehicleFormModal.vue | 1159 +++++++++++++++++ .../components/vehicle/VehicleCardCompact.vue | 41 + .../vehicle/VehicleCardStandard.vue | 170 +++ .../components/vehicle/VehicleDetailModal.vue | 183 +++ .../components/vehicle/VehiclePlateBadge.vue | 84 ++ frontend/src/data/mockVehicles.ts | 50 + frontend/src/stores/vehicle.ts | 103 +- frontend/src/types/vehicle.ts | 55 + frontend/src/views/DashboardView.vue | 143 +- frontend/src/views/ProfileView.vue | 8 - ..._spec_fix_double_prefix_and_4level_arch.md | 92 ++ plans/vehicle_edit_404_fix_analysis.md | 120 ++ 18 files changed, 2749 insertions(+), 99 deletions(-) create mode 100644 frontend/src/components/dashboard/PrivateVehicleManager.vue create mode 100644 frontend/src/components/dashboard/VehicleFormModal.vue create mode 100644 frontend/src/components/vehicle/VehicleCardCompact.vue create mode 100644 frontend/src/components/vehicle/VehicleCardStandard.vue create mode 100644 frontend/src/components/vehicle/VehicleDetailModal.vue create mode 100644 frontend/src/components/vehicle/VehiclePlateBadge.vue create mode 100644 frontend/src/data/mockVehicles.ts create mode 100644 frontend/src/types/vehicle.ts create mode 100644 plans/logic_spec_fix_double_prefix_and_4level_arch.md create mode 100644 plans/vehicle_edit_404_fix_analysis.md diff --git a/.roo/history.md b/.roo/history.md index 40320bb..52b82f2 100644 --- a/.roo/history.md +++ b/.roo/history.md @@ -482,4 +482,139 @@ Két kritikus hiba javítása a járművek csatlakoztatása előtt: (1) "Széf" ### ✅ Verifikáció - Vite production build: 121 module transformed, 0 hiba, 3.56s alatt - Python syntax check: minden módosított fájl hibátlan -- `sync_engine` futtatva: séma konzisztens \ No newline at end of file +- `sync_engine` futtatva: séma konzisztens + + +## 2026-06-10 - is_primary (Elsődleges Jármű) Flag Implementáció + +### 🎯 Cél +`is_primary` mező bevezetése a járművek számára, amely lehetővé teszi a felhasználók számára, hogy megjelöljék elsődleges járművüket. A mező az `individual_equipment` JSONB oszlopban tárolódik, így nincs szükség adatbázis migrációra. + +### 🔧 Változtatások + +**1. Backend - Pydantic Schemas:** [`backend/app/schemas/asset.py`](backend/app/schemas/asset.py:107) +- `is_primary: bool = Field(default=False)` hozzáadva az `AssetResponse`-hez (107. sor) +- `is_primary: bool = Field(default=False)` hozzáadva az `AssetCreate`-hez (158. sor) +- Mindkettő `from_attributes=True` konfigurációval, így a Pydantic olvassa a property-t az SQLAlchemy modellből + +**2. Backend - SQLAlchemy Model:** [`backend/app/models/vehicle/asset.py`](backend/app/models/vehicle/asset.py:157) +- `is_primary` property getter (157-166. sor): kiolvassa az `individual_equipment` JSONB dict `is_primary` kulcsát +- `is_primary.setter` (168-173. sor): az értéket az `individual_equipment['is_primary']`-be menti +- Nincs új oszlop - a meglévő JSONB oszlopot használja + +**3. Backend - API Végpont:** [`backend/app/api/v1/endpoints/assets.py`](backend/app/api/v1/endpoints/assets.py:49) +- JSONB ordering: `text("(individual_equipment->>'is_primary')::boolean DESC NULLS LAST")` +- Mind a személyes, mind a céges módban `is_primary` szerint rendez (True elöl), majd `created_at` szerint + +**4. Backend - Asset Service:** [`backend/app/services/asset_service.py`](backend/app/services/asset_service.py:148) +- `create_or_claim_vehicle` metódusban az `is_primary` merge-elése az `individual_equipment` JSONB-be + +**5. Frontend - Vehicle Store:** [`frontend/src/stores/vehicle.ts`](frontend/src/stores/vehicle.ts:46) +- `is_primary: boolean` mező hozzáadva a `Vehicle` interfészhez + +**6. Frontend - DashboardView:** [`frontend/src/views/DashboardView.vue`](frontend/src/views/DashboardView.vue:349) +- `displayVehicles` computed property: valós backend adatok előtérbe helyezése, hiány esetén mock adatokkal feltöltés 3 járműig +- Deduplikáció `license_plate` alapján + +**7. Frontend - PrivateVehicleManager:** [`frontend/src/components/dashboard/PrivateVehicleManager.vue`](frontend/src/components/dashboard/PrivateVehicleManager.vue:129) +- Ugyanaz a smart merge logika: valós járművek + mock fallback, deduplikációval + +### ✅ Verifikáció +- Python syntax check: mind a 4 módosított fájl hibátlan +- `sync_engine` futtatva: 1019/1019 OK, séma konzisztens (nincs migráció - JSONB approach) +- API restart sikeres +- `POST /assets/vehicles` `is_primary: True`-val: 201 Created, `individual_equipment: {'is_primary': True}` +- `GET /assets/vehicles` ordering: `[True] TEST-PRI` előbb, mint `[False] ABC-123` + + +## 2026-06-10 - Fix 'toLocaleString' Crash & Implement Vehicle POST/PUT API + +### 🎯 Cél +1. `toLocaleString()` crash javítása a járműkártyákon, amikor `mileage` undefined +2. Pinia store `createVehicle` / `updateVehicle` action-ök implementálása +3. `VehicleFormModal.vue` bekötése a valós API-ra +4. Backend `PUT /assets/vehicles/{id}` végpont hozzáadása + +### 🔧 Változtatások + +**1. `frontend/src/components/vehicle/VehicleCardStandard.vue`:** +- `vehicle.mileage.toLocaleString()` → `(vehicle.mileage ?? 0).toLocaleString()` (safe fallback 0-ra) +- `VehicleDetailModal.vue` már eleve használta a `vehicle?.mileage?.toLocaleString() || '—'` biztonságos formát + +**2. `frontend/src/stores/vehicle.ts`:** +- Új `createVehicle(vehicleData)` action: `POST /assets/vehicles`, siker esetén `unshift` a state-be +- Új `updateVehicle(id, vehicleData)` action: `PUT /assets/vehicles/{id}`, siker esetén frissíti a state-et +- Mindkét action try-catch blokkal, részletes hibaüzenetekkel + +**3. `frontend/src/components/dashboard/VehicleFormModal.vue`:** +- `useVehicleStore` importálva +- `handleSave()` most már a store action-öket hívja: `createVehicle` új járműnél, `updateVehicle` szerkesztésnél +- Az `AssetCreate` sémában nem szereplő mezők (color, notes, engine_code) automatikusan az `individual_equipment` JSONB-be kerülnek +- Siker esetén `emit('saved')` és `resetForm()` + +**4. `backend/app/schemas/asset.py`:** +- Új `AssetUpdate` Pydantic schema: minden mező opcionális, partial update támogatással +- Tartalmazza az `is_primary` flag-et és az `individual_equipment` merge támogatást + +**5. `backend/app/api/v1/endpoints/assets.py`:** +- Új `PUT /assets/vehicles/{asset_id}` végpont: partial update, jogosultság-ellenőrzéssel +- `is_primary` → `individual_equipment` JSONB-be mentés +- `individual_equipment` merge (meglévő + új kulcsok) +- `updated_at` automatikus frissítése + +### ✅ Verifikáció +- Vite build: 154 modul, 3.83s, hiba nélkül +- Backend Python syntax check: assets.py és asset.py schema OK +- A `toLocaleString` crash megszűnt (null/undefined mileage esetén 0-t jelenít meg) +- Az új jármű létrehozás és szerkesztés most már a valós API-t használja + +## 2026-06-10 - Vehicle Edit 404 Fix (PUT /vehicles/{id} Dual Entity Bug) + +### 🎯 Cél +A manager dashboardon a járműkártya szerkesztésekor a Mentés gomb `PUT /api/v1/assets/vehicles/{id}` kérése HTTP 404 Not Found hibát adott. + +### 🔧 Root Cause #1 - Stale uvicorn (Javítva) +- Az `sf_api` konténerben az uvicorn `--reload` nélkül indult PID 1-ként +- A futó Python folyamat a régi, cache-elt modulokat használta (`sys.modules`) +- **Javítás:** `docker compose restart sf_api` + +### 🔧 Root Cause #2 - Dual Entity ID mismatch (Javítva) +- A projekt Dual Entity modellt használ: `User.id` (28) vs `Person.id` (29) +- A `PUT /vehicles/{asset_id}` végpont a tulajdonos-ellenőrzésnél `current_user.id`-t használt `current_user.person_id` helyett +- **Javítás:** `backend/app/api/v1/endpoints/assets.py:278,280` - `current_user.id` → `current_user.person_id` + +### ✅ Verifikáció +- `PUT /vehicles/{id}` current_mileage=50000 → **200 OK** ✅ +- `PUT /vehicles/{id}` license_plate=TESZT-001 → **200 OK** ✅ +- `GET /health` → database: connected ✅ +- Frontend kód nem szorult javításra +- Teljes analízis: `plans/vehicle_edit_404_fix_analysis.md` + +## 2026-06-10 - Fix Store Reactivity, Dynamic Counter & Smart Duplicate Prevention + +### 🎯 Cél +Jármű létrehozás reaktivitásának és duplikációvédelemnek a javítása három rétegben: backend smart duplicate protection, frontend toast értesítések és frontend-side validáció. + +### 🔧 Változtatások + +**1. `backend/app/schemas/asset.py`:** +- `AssetCreate` sémához `data_status` mező hozzáadva (Optional[str]) a tulajdonosváltásos duplikációs forgatókönyv támogatásához. + +**2. `backend/app/api/v1/endpoints/assets.py`:** +- Smart duplicate protection a POST `/vehicles` végpontban: + - **A szabály:** Ha a rendszám létezik ÉS ugyanaz az owner → HTTP 400 "Ez a jármű már szerepel a garázsodban!" + - **B szabály:** Ha a rendszám létezik, de más owner → `payload.data_status = "draft"` (tulajdonosváltás esete) + +**3. `frontend/src/components/dashboard/PrivateVehicleManager.vue`:** +- Counter módosítása: `{{ vehicles.length }} / 5 jármű` → `{{ vehicles.length }} jármű` (dinamikus, eltávolítva a keménykódolt limitet) + +**4. `frontend/src/components/dashboard/VehicleFormModal.vue`:** +- Toast értesítési rendszer hozzáadva (`showToast` függvény, toast sablon slide-in animációval) +- Frontend duplikáció validáció (`checkDuplicateLicensePlate` függvény) +- `handleSave` frissítve: siker esetén toast (`✅ ... sikeresen elmentve!`), hiba esetén toast (`❌ ...`) +- CSS animáció a toast-hoz + +### ✅ Verifikáció +- Vite build: 155 modul transzformálva, 4.09s, hiba nélkül ✅ +- Backend smart duplicate protection mindkét szabállyal implementálva ✅ +- Frontend toast és duplikáció validáció működik ✅ \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/assets.py b/backend/app/api/v1/endpoints/assets.py index a13b9e2..9ca6bf3 100755 --- a/backend/app/api/v1/endpoints/assets.py +++ b/backend/app/api/v1/endpoints/assets.py @@ -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, diff --git a/backend/app/models/vehicle/asset.py b/backend/app/models/vehicle/asset.py index 8f272a9..f1e2c46 100644 --- a/backend/app/models/vehicle/asset.py +++ b/backend/app/models/vehicle/asset.py @@ -154,6 +154,24 @@ class Asset(Base): """Always False for now, as verification is not yet implemented.""" return False + @property + def is_primary(self) -> bool: + """ + Elsődleges jármű flag. + Tárolva: individual_equipment JSONB mezőben ('is_primary' kulcs alatt). + Ha nincs beállítva, alapértelmezett: False. + """ + if self.individual_equipment and isinstance(self.individual_equipment, dict): + return bool(self.individual_equipment.get('is_primary', False)) + return False + + @is_primary.setter + def is_primary(self, value: bool) -> None: + """Setter: az is_primary értéket az individual_equipment JSONB-be menti.""" + if self.individual_equipment is None or not isinstance(self.individual_equipment, dict): + self.individual_equipment = {} + self.individual_equipment['is_primary'] = bool(value) + @property def profile_completion_percentage(self) -> int: """ diff --git a/backend/app/schemas/asset.py b/backend/app/schemas/asset.py index 50abfba..13bf07b 100755 --- a/backend/app/schemas/asset.py +++ b/backend/app/schemas/asset.py @@ -104,6 +104,9 @@ class AssetResponse(BaseModel): # === PROFILE COMPLETION === profile_completion_percentage: int = Field(default=0, ge=0, le=100) + # === PRIMARY VEHICLE FLAG === + is_primary: bool = Field(default=False, description="Elsődleges jármű (tárolva: individual_equipment JSONB)") + model_config = ConfigDict(from_attributes=True) @@ -143,6 +146,9 @@ class AssetCreate(BaseModel): audio_system_type: Optional[str] = Field(None, max_length=100, description="Hangrendszer") individual_equipment: Dict[str, Any] = Field(default_factory=dict, description="Egyedi felszerelések") + # === MILEAGE (Optional) === + current_mileage: Optional[int] = Field(None, ge=0, description="Aktuális km óra állás") + # === TIMELINE (Optional) === year_of_manufacture: Optional[int] = Field(None, ge=1900, le=2100, description="Gyártási év") first_registration_date: Optional[datetime] = Field(None, description="Első forgalomba helyezés dátuma") @@ -151,6 +157,12 @@ class AssetCreate(BaseModel): organization_id: Optional[int] = Field(None, description="Szervezet ID (alapértelmezett a felhasználó szervezete)") branch_id: Optional[UUID] = Field(None, description="Garázs (Branch) ID, ahova a járművet rendeljük. Ha nincs megadva, a szervezet központi garázsába kerül.") + # === PRIMARY VEHICLE FLAG === + is_primary: bool = Field(default=False, description="Elsődleges jármű (tárolva: individual_equipment JSONB)") + + # === DATA STATUS (for transfer/duplicate scenarios) === + data_status: Optional[str] = Field(None, description="Adat státusz (pl. 'draft' tulajdonosváltáskor)") + # === STATUS VALIDATION === @validator('status', pre=True, always=True) def determine_status(cls, v, values): @@ -170,4 +182,51 @@ class AssetCreate(BaseModel): # === COMPUTED FIELD: status === status: Optional[str] = Field(None, description="Automatikusan számított státusz (draft/active)") + model_config = ConfigDict(from_attributes=True) + + +class AssetUpdate(BaseModel): + """ Jármű adatainak frissítése - minden mező opcionális. """ + # === CORE IDENTIFICATION === + license_plate: Optional[str] = Field(None, min_length=2, max_length=20, description="Rendszám") + vin: Optional[str] = Field(None, min_length=1, max_length=50, description="VIN szám") + + # === CLASSIFICATION === + brand: Optional[str] = Field(None, max_length=100, description="Márka") + model: Optional[str] = Field(None, max_length=100, description="Modell") + vehicle_class: Optional[str] = Field(None, max_length=50, description="Járműosztály") + fuel_type: Optional[str] = Field(None, max_length=50, description="Üzemanyag típus") + + # === TECHNICAL SPECS === + catalog_id: Optional[int] = Field(None, description="Katalógus ID") + engine_capacity: Optional[int] = Field(None, ge=0, description="Hengerűrtartalom (cm³)") + power_kw: Optional[int] = Field(None, ge=0, description="Teljesítmény (kW)") + torque_nm: Optional[int] = Field(None, ge=0, description="Nyomaték (Nm)") + cylinder_layout: Optional[str] = Field(None, max_length=50, description="Hengerelrendezés") + transmission_type: Optional[str] = Field(None, max_length=50, description="Váltó típus") + drive_type: Optional[str] = Field(None, max_length=50, description="Hajtás") + euro_classification: Optional[str] = Field(None, max_length=10, description="EURO besorolás") + + # === PHYSICAL DIMENSIONS === + curb_weight: Optional[int] = Field(None, ge=0, description="Saját tömeg (kg)") + max_weight: Optional[int] = Field(None, ge=0, description="Össztömeg (kg)") + cargo_volume_x: Optional[float] = Field(None, ge=0, description="Csomagtartó hossz (cm)") + cargo_volume_y: Optional[float] = Field(None, ge=0, description="Csomagtartó szélesség (cm)") + door_count: Optional[int] = Field(None, ge=0, description="Ajtók száma") + seat_count: Optional[int] = Field(None, ge=0, description="Ülések száma") + + # === EQUIPMENT === + trim_level: Optional[str] = Field(None, max_length=100, description="Felszereltségi szint") + roof_type: Optional[str] = Field(None, max_length=50, description="Tető típus") + audio_system_type: Optional[str] = Field(None, max_length=100, description="Hangrendszer") + individual_equipment: Optional[Dict[str, Any]] = Field(None, description="Egyedi felszerelések") + + # === TIMELINE === + year_of_manufacture: Optional[int] = Field(None, ge=1900, le=2100, description="Gyártási év") + first_registration_date: Optional[datetime] = Field(None, description="Első forgalomba helyezés dátuma") + + # === STATUS === + current_mileage: Optional[int] = Field(None, ge=0, description="Aktuális km óra állás") + is_primary: Optional[bool] = Field(None, description="Elsődleges jármű") + model_config = ConfigDict(from_attributes=True) \ No newline at end of file diff --git a/backend/app/services/asset_service.py b/backend/app/services/asset_service.py index d02487a..096222f 100755 --- a/backend/app/services/asset_service.py +++ b/backend/app/services/asset_service.py @@ -145,6 +145,11 @@ class AssetService: print(f"DEBUG: resolved branch_id={branch_id} for target_org_id={target_org_id}") + # Build individual_equipment with is_primary flag + equipment = dict(asset_data.individual_equipment or {}) + if asset_data.is_primary: + equipment['is_primary'] = True + asset_fields = { 'vin': vin_clean, 'license_plate': license_plate_clean, @@ -155,7 +160,7 @@ class AssetService: 'owner_org_id': asset_data.organization_id or target_org_id, 'operator_org_id': None, # AssetCreate doesn't have operator_org_id 'status': status, - 'individual_equipment': asset_data.individual_equipment or {}, + 'individual_equipment': equipment, 'created_at': datetime.utcnow(), # Classification @@ -182,6 +187,9 @@ class AssetService: 'door_count': asset_data.door_count, 'seat_count': asset_data.seat_count, + # Mileage + 'current_mileage': asset_data.current_mileage, + # Equipment 'roof_type': asset_data.roof_type, 'audio_system_type': asset_data.audio_system_type, diff --git a/frontend/src/components/dashboard/PrivateVehicleManager.vue b/frontend/src/components/dashboard/PrivateVehicleManager.vue new file mode 100644 index 0000000..8ec7d1d --- /dev/null +++ b/frontend/src/components/dashboard/PrivateVehicleManager.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/frontend/src/components/dashboard/VehicleFormModal.vue b/frontend/src/components/dashboard/VehicleFormModal.vue new file mode 100644 index 0000000..3db3b06 --- /dev/null +++ b/frontend/src/components/dashboard/VehicleFormModal.vue @@ -0,0 +1,1159 @@ + + + + + diff --git a/frontend/src/components/vehicle/VehicleCardCompact.vue b/frontend/src/components/vehicle/VehicleCardCompact.vue new file mode 100644 index 0000000..1116d79 --- /dev/null +++ b/frontend/src/components/vehicle/VehicleCardCompact.vue @@ -0,0 +1,41 @@ + + + diff --git a/frontend/src/components/vehicle/VehicleCardStandard.vue b/frontend/src/components/vehicle/VehicleCardStandard.vue new file mode 100644 index 0000000..d242c90 --- /dev/null +++ b/frontend/src/components/vehicle/VehicleCardStandard.vue @@ -0,0 +1,170 @@ + + + diff --git a/frontend/src/components/vehicle/VehicleDetailModal.vue b/frontend/src/components/vehicle/VehicleDetailModal.vue new file mode 100644 index 0000000..888a83b --- /dev/null +++ b/frontend/src/components/vehicle/VehicleDetailModal.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/frontend/src/components/vehicle/VehiclePlateBadge.vue b/frontend/src/components/vehicle/VehiclePlateBadge.vue new file mode 100644 index 0000000..7670adc --- /dev/null +++ b/frontend/src/components/vehicle/VehiclePlateBadge.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/src/data/mockVehicles.ts b/frontend/src/data/mockVehicles.ts new file mode 100644 index 0000000..e400542 --- /dev/null +++ b/frontend/src/data/mockVehicles.ts @@ -0,0 +1,50 @@ +import type { VehicleData } from '../types/vehicle' + +/** + * Mock vehicle data used as fallback when the API store is empty. + * Single source of truth — import this instead of duplicating data. + */ +export const mockVehicles: VehicleData[] = [ + { + id: 1, + license_plate: 'ABC-123', + brand: 'BMW', + model: 'X5 xDrive30d', + year: 2021, + mileage: 45230, + engine: '3.0 TDI', + color: 'Sötétkék', + nextService: '5 400 km múlva', + image: null, + nickname: 'Gizike', + countryCode: 'H', + }, + { + id: 2, + license_plate: 'DEF-456', + brand: 'Toyota', + model: 'Corolla Hybrid', + year: 2023, + mileage: 12450, + engine: '1.8 Hybrid', + color: 'Fehér', + nextService: '12 500 km múlva', + image: null, + nickname: 'Kispiros', + countryCode: 'H', + }, + { + id: 3, + license_plate: 'W-XY-1234', + brand: 'Volkswagen', + model: 'Golf 8 R-Line', + year: 2022, + mileage: 28700, + engine: '2.0 TSI', + color: 'Szürke', + nextService: '2 100 km múlva', + image: null, + nickname: '', + countryCode: 'D', + }, +] diff --git a/frontend/src/stores/vehicle.ts b/frontend/src/stores/vehicle.ts index 2008f4a..76b9d0c 100644 --- a/frontend/src/stores/vehicle.ts +++ b/frontend/src/stores/vehicle.ts @@ -3,7 +3,7 @@ import { ref } from 'vue' import api from '../api/axios' /** - * Vehicle data shape returned by GET /api/v1/assets/vehicles + * Vehicle data shape returned by GET /assets/vehicles * Based on the AssetResponse Pydantic schema from the backend. */ export interface Vehicle { @@ -41,6 +41,9 @@ export interface Vehicle { is_for_sale: boolean price: number | null currency: string + + // Primary vehicle flag (from backend individual_equipment JSONB) + is_primary: boolean } export const useVehicleStore = defineStore('vehicle', () => { @@ -53,14 +56,14 @@ export const useVehicleStore = defineStore('vehicle', () => { /** * Fetch all vehicles/assets for the current user or their organization. - * GET /api/v1/assets/vehicles + * GET /assets/vehicles */ async function fetchVehicles() { isLoading.value = true error.value = null try { - const res = await api.get('/api/v1/assets/vehicles') + const res = await api.get('/assets/vehicles') vehicles.value = res.data as Vehicle[] } catch (err: any) { const message = @@ -74,6 +77,97 @@ export const useVehicleStore = defineStore('vehicle', () => { } } + /** + * Create a new vehicle via POST /assets/vehicles. + * On success, adds the new vehicle to the local state and refreshes the list. + */ + async function createVehicle(vehicleData: Record): Promise { + isLoading.value = true + error.value = null + + try { + const res = await api.post('/assets/vehicles', vehicleData) + const newVehicle = res.data as Vehicle + + // Add the new vehicle to local state + vehicles.value.unshift(newVehicle) + + return newVehicle + } catch (err: any) { + // Log full error response for debugging (422 Validation Error details, etc.) + if (err.response) { + console.error('[VehicleStore] createVehicle error response:', { + status: err.response.status, + data: err.response.data, + headers: err.response.headers, + }) + } else { + console.error('[VehicleStore] createVehicle network error:', err.message) + } + const detail = err.response?.data?.detail + const message = Array.isArray(detail) + ? detail.map((d: any) => `${d.loc?.join('.')}: ${d.msg}`).join('; ') + : detail || err.response?.data?.message || 'Nem sikerült létrehozni a járművet.' + error.value = message + throw err + } finally { + isLoading.value = false + } + } + + /** + * Update an existing vehicle via PUT /assets/vehicles/{id}. + * On success, updates the vehicle in the local state. + */ + async function updateVehicle(id: string, vehicleData: Record): Promise { + isLoading.value = true + error.value = null + + try { + const res = await api.put(`/assets/vehicles/${id}`, vehicleData) + const updatedVehicle = res.data as Vehicle + + // Update the vehicle in local state + const index = vehicles.value.findIndex(v => v.id === id) + if (index !== -1) { + vehicles.value[index] = updatedVehicle + } + + return updatedVehicle + } catch (err: any) { + // Log full error response for debugging (422 Validation Error details, etc.) + if (err.response) { + console.error('[VehicleStore] updateVehicle error response:', { + status: err.response.status, + data: err.response.data, + headers: err.response.headers, + }) + } else { + console.error('[VehicleStore] updateVehicle network error:', err.message) + } + const detail = err.response?.data?.detail + const message = Array.isArray(detail) + ? detail.map((d: any) => `${d.loc?.join('.')}: ${d.msg}`).join('; ') + : detail || err.response?.data?.message || 'Nem sikerült frissíteni a járművet.' + error.value = message + throw err + } finally { + isLoading.value = false + } + } + + /** + * Optimistically set a vehicle as the primary vehicle. + * Unsets any other primary vehicle in the local state. + */ + function setPrimaryVehicle(vehicleId: string | number) { + vehicles.value = vehicles.value.map(v => ({ + ...v, + is_primary: v.id === vehicleId, + })) + console.log('[VehicleStore] setPrimaryVehicle:', vehicleId) + } + return { // State vehicles, @@ -82,5 +176,8 @@ export const useVehicleStore = defineStore('vehicle', () => { // Actions fetchVehicles, + createVehicle, + updateVehicle, + setPrimaryVehicle, } }) diff --git a/frontend/src/types/vehicle.ts b/frontend/src/types/vehicle.ts new file mode 100644 index 0000000..540ebb8 --- /dev/null +++ b/frontend/src/types/vehicle.ts @@ -0,0 +1,55 @@ +/** + * Unified VehicleData interface. + * Covers both API response (Vehicle from store) and mock data shapes. + * Used across all vehicle-related components for type safety. + * + * The backend AssetResponse uses `current_mileage` as the canonical field. + * Components should use `current_mileage` for display; `mileage` is kept + * as a backward-compatible alias for mock data. + */ +export interface VehicleData { + id: number | string + license_plate: string + brand: string + model: string + + // Display fields (unified) + year: number | null + /** @deprecated Use current_mileage instead */ + mileage?: number + /** Canonical mileage field from backend AssetResponse.current_mileage */ + current_mileage: number + engine: string + color: string + nextService?: string + image: string | null + nickname?: string + countryCode?: string + + // Extended API fields (optional — present in store Vehicle but not in mock) + vin?: string | null + year_of_manufacture?: number | null + fuel_type?: string + engine_capacity?: number | null + power_kw?: number | null + transmission_type?: string + drive_type?: string + trim_level?: string + vehicle_class?: string + status?: string + is_verified?: boolean + is_primary?: boolean + created_at?: string + updated_at?: string | null + is_for_sale?: boolean + price?: number | null + currency?: string + + // JSONB individual_equipment from backend (color, notes, engine_code, etc.) + individual_equipment?: { + color?: string + notes?: string + engine_code?: string + [key: string]: any + } +} diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index 8dafea0..7ad3d0c 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -3,22 +3,6 @@
- - -
- {{ t('menu.calendar') }} -
- - -
- SERVICE FINDER -
@@ -78,43 +62,21 @@
🚗 {{ t('dashboard.myVehicles') }} - {{ vehicleStore.vehicles.length }} {{ t('dashboard.vehicles') }} + {{ displayVehicles.length }} {{ t('dashboard.vehicles') }}
-
+
-
- -
-
- SF -
-
- - {{ vehicle.license_plate }} - -
-
- -

- {{ vehicle.brand || t('dashboard.statusUnknown') }} - {{ vehicle.model }} -

- -

- {{ vehicle.current_mileage?.toLocaleString() || 0 }} {{ t('dashboard.mileage') }} -

-
- + :vehicle="vehicle" + @click="openCard('vehicles')" + @plate-click="openCard('vehicles', vehicle.license_plate)" + /> +
@@ -124,10 +86,6 @@
- -
-
-
- - + +
+
+ + - -
-

- {{ t('dashboard.detailedView') }}: {{ activeCard }} -

-

- {{ t('dashboard.modalPlaceholder') }} -

+ +
+ + + + + +
-
+