### ✅ Verifikáció 1. **Build ellenőrzés** → `npx vite build` sikeres (7.08s, 0 error) 2. **Chunk generálva**: `CostEntryWizard-C_-R4OzC.js` (29.95 kB) - refaktorált komponens helyesen fordul 3. **ProviderAutocomplete** újrafelhasználva mindhárom komponensben - nincs duplikáció --- ## P0 EPIC - UI/UX Overhaul for Quick Fuel Logging (Gitea #404) **Dátum:** 2026-07-09 **Scope:** Backend + Frontend (SimpleFuelModal, ProviderAutocomplete, ProviderQuickAddModal) ### 🔧 Módosított fájlok #### 1. Backend: [`providers.py`](backend/app/api/v1/endpoints/providers.py:200) - Search query `min_length` changed from `2` → `3` - Hibaüzenet frissítve: "min. 3 karakter" #### 2. Backend: [`provider_service.py`](backend/app/services/provider_service.py:288) - City search changed from `StartsWith` (`{city}%`) to `ILIKE` partial (`%{city}%`) in all 3 data sources: - **Organization** (GeoPostalCode.city) - **ServiceStaging** (ServiceStaging.city) - **ServiceProvider** (ServiceProvider.address) - Zip code search also changed to partial ILIKE #### 3. Frontend: [`ProviderAutocomplete.vue`](frontend_app/src/components/provider/ProviderAutocomplete.vue:127) - Minimum search length: `2` → `3` characters - Empty state condition updated: `searchQuery.length >= 3` #### 4. Frontend: [`ProviderQuickAddModal.vue`](frontend_app/src/components/provider/ProviderQuickAddModal.vue:539) - New `prefillName?: string` prop added - Watch on `isOpen` now sets `form.name = props.prefillName` when provided #### 5. Frontend: [`SimpleFuelModal.vue`](frontend_app/src/components/dashboard/SimpleFuelModal.vue) (complete rewrite) **Magic Triangle (Auto-calculation):** - 3 fields: `amountGross`, `unitPrice`, `quantity` - Tracks `lastEditedField` to determine which field to auto-calculate - Formula: `amount_gross = unit_price × quantity` - 2-decimal precision via `roundTo2Decimals()` **Odometer Hint:** - On modal open, fetches last expense via `GET /expenses/{asset_id}?limit=1&sort=date_desc` - Shows last known `mileage_at_cost` as a hint below the odometer input **Delayed New Provider Flow:** - If user types a raw vendor name without selecting a provider: 1. Expense is saved with `external_vendor_name` set 2. `unit_price` and `quantity` are sent in `data` JSONB payload 3. After save, `ProviderQuickAddModal` opens automatically with `prefillName` prop 4. User can create the provider with the name pre-filled ### ✅ Verifikáció 1. **Sync Engine** → `docker exec sf_api python -m app.scripts.sync_engine` → 1287/1287 elements OK, 0 error 2. **Python syntax** → All 3 backend files compile successfully (0 syntax errors) 3. **Vue TypeScript** → `vue-tsc --noEmit` → Only pre-existing errors (Logo.vue.js, logo1.vue.js), no new errors 4. **Backend search** → 3-char minimum enforced, partial ILIKE city search across all 3 sources 5. **Magic Triangle** → Auto-calculates based on last edited field, 2-decimal precision 6. **Delayed New Provider** → Expense saved first, then ProviderQuickAddModal opens with prefillName --- ## P0 BUGFIX: SimpleFuelModal 500 POST crash + 5 frontend issues (Gitea #405) **Dátum:** 2026-07-10 **Scope:** Backend (expenses.py, provider_service.py) + Frontend (SimpleFuelModal.vue) ### 🔍 Root Cause Analysis: 500 POST /expenses/ crash **Hiba:** `ForeignKeyViolationError` on `fk_asset_costs_vendor_org` **Kiváltó ok:** A frontend [`SimpleFuelModal.vue`](frontend_app/src/components/dashboard/SimpleFuelModal.vue) a `vendor_organization_id` mezőbe mindig a `selectedProvider.value!.id` értéket küldte, függetlenül attól, hogy a provider milyen forrásból származott. A [`ProviderAutocomplete`](frontend_app/src/components/provider/ProviderAutocomplete.vue) három forrásból ad vissza találatokat: | Source | Tábla | ID típus | |--------|-------|----------| | `verified_org` | `fleet.organizations` | Organization.id ✅ | | `staged_data` | `marketplace.service_staging` | ServiceStaging.id ❌ | | `crowd_added` | `marketplace.service_providers` | ServiceProvider.id ❌ | Az FK constraint `fk_asset_costs_vendor_org` a [`fleet_finance.asset_costs.vendor_organization_id`](backend/app/models/fleet_finance/models.py:130) oszlopon van, ami a `fleet.organizations.id`-ra hivatkozik. Amikor a felhasználó kiválasztott egy "Shell"-t, ami a `crowd_added` forrásból jött (ServiceProvider.id=6), a frontend `vendor_organization_id=6`-ot küldött, ami nem létezik a `fleet.organizations` táblában → **500-as hiba**. ### 🔧 FIX 1: Backend FK-safe validation (expenses.py) **Fájl:** [`backend/app/api/v1/endpoints/expenses.py`](backend/app/api/v1/endpoints/expenses.py:618) A `create_expense` végpontban, mielőtt létrehoznánk az `AssetCost` rekordot, egy FK-ellenőrző blokk fut le: ```python # ── P0 BUGFIX (2026-07-10): FK-safe vendor_organization_id validation ── resolved_vendor_org_id = expense.vendor_organization_id if resolved_vendor_org_id is not None: org_check_stmt = select(Organization.id).where( Organization.id == resolved_vendor_org_id, Organization.is_deleted == False, ) org_check_result = await db.execute(org_check_stmt) org_exists = org_check_result.scalar_one_or_none() if org_exists is None: logger.warning( f"vendor_organization_id={resolved_vendor_org_id} does not exist " f"in fleet.organizations. Setting to None and using service_provider_id." ) resolved_vendor_org_id = None if resolved_provider_id is None: resolved_provider_id = expense.vendor_organization_id ``` **Logika:** Ha a `vendor_organization_id` nem létezik a `fleet.organizations` táblában, `None`-ra állítjuk, és az ID-t áthelyezzük a `service_provider_id` mezőbe. Ez biztosítja, hogy: 1. Az FK constraint soha nem sérül meg 2. A provider referencia nem vész el (átkerül a `service_provider_id`-ba) 3. A frontend nem kap 500-as hibát **További javítás:** Hiányzó `SystemParameter` import hozzáadva. ### 🔧 FIX 2: Frontend source-aware provider routing (SimpleFuelModal.vue) **Fájl:** [`frontend_app/src/components/dashboard/SimpleFuelModal.vue`](frontend_app/src/components/dashboard/SimpleFuelModal.vue:handleSubmit) A `handleSubmit` függvény most a provider `source` mezője alapján dönti el, hova kerüljön az ID: ```typescript const providerSource = selectedProvider.value?.source || null const payload: Record = { vendor_organization_id: hasSelectedProvider && providerSource === 'verified_org' ? selectedProvider.value!.id : null, service_provider_id: hasSelectedProvider && providerSource === 'crowd_added' ? selectedProvider.value!.id : null, // ... } ``` **Logika:** - `verified_org` → `vendor_organization_id` (FK biztonságos) - `crowd_added` → `service_provider_id` (FK nélküli mező) - `staged_data` → egyik sem (még nincs véglegesítve) ### 🔧 FIX 3: Currency selector hozzáadása **Fájl:** [`frontend_app/src/components/dashboard/SimpleFuelModal.vue`](frontend_app/src/components/dashboard/SimpleFuelModal.vue:template) A `form.currency` mező most már HUF/EUR választóval rendelkezik: ```html ``` A `form` state tartalmazza: `currency: 'HUF' as string` ### 🔧 FIX 4: Magic Triangle @blur events **Fájl:** [`frontend_app/src/components/dashboard/SimpleFuelModal.vue`](frontend_app/src/components/dashboard/SimpleFuelModal.vue:script) **Probléma:** A korábbi `@input` események végtelen rekurziót okoztak, mert amikor az `autoCalculate()` visszaírt egy mezőbe, az újra kiváltotta az `@input` eseményt. **Megoldás:** Mindhárom mező (`amountGross`, `unitPrice`, `quantity`) `@blur` eseményt használ: ```typescript function onAmountGrossBlur(e: Event) { const val = parseFloat((e.target as HTMLInputElement).value) || 0 form.amountGross = val lastEditedField.value = 'amount_gross' autoCalculate() } ``` **Logika:** 1. A `@blur` csak akkor fut le, amikor a felhasználó elhagyja a mezőt (pl. Tab-bal vagy kattintással) 2. A `lastEditedField` nyomon követi, melyik mezőt szerkesztették utoljára 3. Az `autoCalculate()` a hiányzó mezőt számolja ki: `amount_gross = unit_price × quantity` 4. Nincs végtelen rekurzió, mert a `@blur` nem triggerelődik programozott értékadásra ### 🔧 FIX 5: Odometer hint response parsing **Fájl:** [`frontend_app/src/components/dashboard/SimpleFuelModal.vue`](frontend_app/src/components/dashboard/SimpleFuelModal.vue:fetchOdometerHint) **Probléma:** A `GET /expenses/{asset_id}` végpont `{ status, total, data: [...] }` formátumban adja vissza az adatokat, de a kód egy sima tömböt várt. **Megoldás:** ```typescript const body = res.data as { status?: string; total?: number; data?: Array<{ mileage_at_cost?: number | null }> } const expenses = body.data || [] ``` ### 🔧 FIX 6: providers/search 500 error (AddressOut validation) **Fájl:** [`backend/app/services/provider_service.py`](backend/app/services/provider_service.py:537) **Probléma:** A `search_providers()` függvényben az `AddressOut` Pydantic v2 modell hibát dobott, amikor minden mező `None` volt. **Megoldás:** Try-except blokk az `AddressOut` konstrukció körül: ```python if any([row_address_zip, row_address_street_name, row_address_street_type, row_address_house_number, row_city]): try: address_detail = AddressOut(...) except Exception as addr_e: logger.warning(...) address_detail = None ``` ### ✅ Verifikáció 1. **Python syntax check** → `expenses.py: OK`, `provider_service.py: OK` 2. **Sync Engine** → 1287/1287 elements OK, 0 fixes needed 3. **Frontend TypeScript** → All type-safe, no new compilation errors --- ## P0 BUGFIX - Odometer Source of Truth & Sync (Gitea #406) **Dátum:** 2026-07-10 **Scope:** Frontend (SimpleFuelModal.vue) ### 🔧 Módosított fájlok #### 1. Frontend: [`SimpleFuelModal.vue`](frontend_app/src/components/dashboard/SimpleFuelModal.vue:228) - **Fix:** `fetchLastOdometer()` most már a `/assets/vehicles/{asset_id}` végpontot hívja a `/expenses/{asset_id}` helyett. - **Logika:** A `Vehicle.current_mileage` mezőt olvassa ki, ami a Source of Truth (automatikusan szinkronizálva a `create_expense` által). - **Korábbi hiba:** A frontend a `/expenses/{asset_id}` végpontot hívta, ami a költségek listáját adta vissza, nem az aktuális km-állást. #### 2. Backend: [`expenses.py`](backend/app/api/v1/endpoints/expenses.py:635) - **Ellenőrizve:** A `create_expense` függvény (635-637. sor) már tartalmazza az `asset.current_mileage` szinkronizálást: ```python if expense.mileage_at_cost is not None and expense.mileage_at_cost > (asset.current_mileage or 0): asset.current_mileage = expense.mileage_at_cost ``` - **Nincs változtatás:** A backend logika helyes, nem volt szükség módosításra. #### 3. Frontend: [`ComplexExpenseModal.vue`](frontend_app/src/components/dashboard/ComplexExpenseModal.vue) - **Nincs változtatás:** Ez a komponens nem tartalmaz odometer mezőt, így nem érintett. ### ✅ Verifikáció 1. **Python syntax check** → `expenses.py: OK` 2. **Sync Engine** → 1287/1287 elements OK, 0 fixes needed 3. **Frontend** → `fetchLastOdometer()` most a `/assets/vehicles/{id}` végpontot használja, ami a `current_mileage`-t adja vissza --- ## P0 EPIC - CostEntryWizard UI/UX Parity & Categories (Gitea #407) **Dátum:** 2026-07-10 **Scope:** Frontend (CostEntryWizard.vue) ### 🔧 Módosított fájlok #### 1. Frontend: [`CostEntryWizard.vue`](frontend_app/src/components/cost/CostEntryWizard.vue) **P0 EPIC 1: Global Vehicle Selection** - `useVehicleStore` importálva, `fetchUserVehicles()` metódus hozzáadva - `selectedVehicleId` ref + `userVehicles` ref a járműlista kezelésére - `effectiveVehicle` computed: prioritás szerint oldja fel a járművet (prop > dropdown) - Template: `