belépés jó, rgisztráción kicsit faragni kell még
This commit is contained in:
@@ -294,9 +294,15 @@ async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_d
|
||||
return {"status": "success", "message": "Fiók aktiválva."}
|
||||
|
||||
@router.get("/me")
|
||||
async def get_current_user_profile(current_user: User = Depends(get_current_user)):
|
||||
async def get_current_user_profile(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Return current user profile (alias for /users/me).
|
||||
Uses _build_user_response() to ensure proper address field mapping.
|
||||
"""
|
||||
from app.schemas.user import UserResponse
|
||||
return UserResponse.model_validate(current_user)
|
||||
from app.api.v1.endpoints.users import _build_user_response
|
||||
response_data = _build_user_response(current_user)
|
||||
return UserResponse.model_validate(response_data)
|
||||
@@ -68,15 +68,15 @@ def _build_user_response(user: User, active_org_id: Optional[int] = None) -> dic
|
||||
address_data = None
|
||||
if person.address:
|
||||
address_data = {
|
||||
"zip": getattr(person.address, 'zip', None) or getattr(getattr(person.address, 'postal_code', None), 'zip_code', None),
|
||||
"city": getattr(person.address, 'city', None) or getattr(getattr(person.address, 'postal_code', None), 'city', None),
|
||||
"street_name": person.address.street_name,
|
||||
"street_type": person.address.street_type,
|
||||
"house_number": person.address.house_number,
|
||||
"stairwell": person.address.stairwell,
|
||||
"floor": person.address.floor,
|
||||
"door": person.address.door,
|
||||
"parcel_id": person.address.parcel_id,
|
||||
"address_zip": getattr(person.address, 'zip', None) or getattr(getattr(person.address, 'postal_code', None), 'zip_code', None),
|
||||
"address_city": getattr(person.address, 'city', None) or getattr(getattr(person.address, 'postal_code', None), 'city', None),
|
||||
"address_street_name": person.address.street_name,
|
||||
"address_street_type": person.address.street_type,
|
||||
"address_house_number": person.address.house_number,
|
||||
"address_stairwell": person.address.stairwell,
|
||||
"address_floor": person.address.floor,
|
||||
"address_door": person.address.door,
|
||||
"address_hrsz": person.address.parcel_id,
|
||||
"full_address_text": person.address.full_address_text,
|
||||
"latitude": person.address.latitude,
|
||||
"longitude": person.address.longitude,
|
||||
@@ -249,13 +249,12 @@ async def update_my_person(
|
||||
)
|
||||
if addr_id:
|
||||
person.address_id = addr_id
|
||||
# EXPLICIT betöltjük az új címet a memóriába, hogy felülírjuk a régit!
|
||||
person.address = await db.get(Address, addr_id)
|
||||
|
||||
try:
|
||||
await db.commit()
|
||||
# Refresh to get the latest state
|
||||
await db.refresh(person)
|
||||
if person.address:
|
||||
await db.refresh(person.address)
|
||||
except SQLAlchemyError as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||
|
||||
@@ -6,16 +6,17 @@ from datetime import date, datetime
|
||||
|
||||
|
||||
class AddressResponse(BaseModel):
|
||||
"""Beágyazott cím adatok a PersonResponse számára."""
|
||||
zip: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
street_name: Optional[str] = None
|
||||
street_type: Optional[str] = None
|
||||
house_number: Optional[str] = None
|
||||
stairwell: Optional[str] = None
|
||||
floor: Optional[str] = None
|
||||
door: Optional[str] = None
|
||||
parcel_id: Optional[str] = None
|
||||
"""Beágyazott cím adatok a PersonResponse számára.
|
||||
A mezőnevek address_ előtaggal egyeznek a frontend által várt PersonUpdate formátummal."""
|
||||
address_zip: Optional[str] = None
|
||||
address_city: Optional[str] = None
|
||||
address_street_name: Optional[str] = None
|
||||
address_street_type: Optional[str] = None
|
||||
address_house_number: Optional[str] = None
|
||||
address_stairwell: Optional[str] = None
|
||||
address_floor: Optional[str] = None
|
||||
address_door: Optional[str] = None
|
||||
address_hrsz: Optional[str] = None
|
||||
full_address_text: Optional[str] = None
|
||||
latitude: Optional[float] = None
|
||||
longitude: Optional[float] = None
|
||||
|
||||
251
docs/vehicle_api_frontend_architecture.md
Normal file
251
docs/vehicle_api_frontend_architecture.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# 🚗 Service Finder - Jármű API és Frontend Architektúra Elemzés
|
||||
|
||||
> **Verzió:** 1.0
|
||||
> **Dátum:** 2026-06-05
|
||||
> **Szerző:** Rendszer-Architect
|
||||
> **Cél:** A backend jármű API teljes áttekintése és frontend csoportosítási javaslat
|
||||
|
||||
---
|
||||
|
||||
## 1. 📊 Backend API Réteg - Teljes Végpont Térkép
|
||||
|
||||
A FastAPI routerek a `backend/app/api/v1/api.py` fájlban vannak összefogva. Az összes járművel kapcsolatos végpont az alábbi prefixek alatt fut:
|
||||
|
||||
### 1.1 `/assets` prefix - Jármű (Asset) Menedzsment
|
||||
|
||||
**Router:** `backend/app/api/v1/endpoints/assets.py`
|
||||
|
||||
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|
||||
|---------|---------|-----|---------------------|
|
||||
| GET | /assets/vehicles | Felhasználó járműveinek listázása (Garázs). Paginált. Támogatja: személyes mód (owner/operator) és vállalati mód (szervezet garázsai/Branch-ek). | Asset - vehicle.assets tábla |
|
||||
| POST | /assets/vehicles | Új jármű hozzáadása vagy meglévő igénylése. Ellenőrzi a járműlimitet, kezeli a VIN duplikációt, XP jutalmat ad. | Asset |
|
||||
| GET | /assets/{asset_id} | Jármű részletes adatai (katalógus adatokkal együtt). Jogosultság ellenőrzés (tulajdonos/szervezet). | Asset + AssetCatalog |
|
||||
| GET | /assets/{asset_id}/financial-summary | Pénzügyi riport - kategóriákra bontott összesítés (Local/EUR). | AssetCost |
|
||||
| GET | /assets/{asset_id}/costs | Tételes költséglista lapozással. | AssetCost |
|
||||
| GET | /assets/{asset_id}/maintenance | Szerviznapló - karbantartási költségek listája. | AssetCost - cost_category='maintenance' |
|
||||
| POST | /assets/{asset_id}/maintenance | Szervizbejegyzés hozzáadása (date, odometer, description, cost). Létrehoz AssetEvent-et is. | AssetCost + AssetEvent |
|
||||
|
||||
### 1.2 `/catalog` prefix - Jármű Katalógus
|
||||
|
||||
**Router:** `backend/app/api/v1/endpoints/catalog.py`
|
||||
|
||||
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|
||||
|---------|---------|-----|---------------------|
|
||||
| GET | /catalog/makes | Márkák listázása (opcionálisan vehicle_class szerint szűrve). | AssetCatalog |
|
||||
| GET | /catalog/models | Típusok listázása adott márkához. | AssetCatalog |
|
||||
| GET | /catalog/generations | Generációk/Évjáratok adott márka+típushoz. | AssetCatalog |
|
||||
| GET | /catalog/engines | Motorváltozatok és technikai specifikációk (ID, engine_code, fuel_type, factory_data). | AssetCatalog |
|
||||
|
||||
### 1.3 `/vehicles` prefix - Jármű Értékelések
|
||||
|
||||
**Router:** `backend/app/api/v1/endpoints/vehicles.py`
|
||||
|
||||
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|
||||
|---------|---------|-----|---------------------|
|
||||
| POST | /vehicles/{vehicle_id}/ratings | Jármű értékelése 4 dimenzióban (driving_experience, reliability, comfort, consumption_satisfaction 1-10). Egy user csak egyszer értékelhet. | VehicleUserRating |
|
||||
| GET | /vehicles/{vehicle_id}/ratings | Értékelések lekérése adott járműhöz. | VehicleUserRating |
|
||||
|
||||
### 1.4 `/expenses` prefix - Flotta Költségek (TCO)
|
||||
|
||||
**Router:** `backend/app/api/v1/endpoints/expenses.py`
|
||||
|
||||
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|
||||
|---------|---------|-----|---------------------|
|
||||
| POST | /expenses/ | Költség rögzítése (üzemanyag, szerviz, adó, biztosítás). Dynamic Gatekeeper: draft jármű limit ellenőrzés. | AssetCost + SystemParameter |
|
||||
|
||||
### 1.5 `/analytics` prefix - Jármű Analitika
|
||||
|
||||
**Router:** `backend/app/api/v1/endpoints/analytics.py`
|
||||
|
||||
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|
||||
|---------|---------|-----|---------------------|
|
||||
| GET | /analytics/{vehicle_id}/summary | TCO összesítő járművenként (user costs, lifetime costs, benchmark). | AssetCost, VehicleModelDefinition |
|
||||
| GET | /analytics/dashboard | Dashboard analitika (mock data - havi költségek, üzemanyag-hatékonyság, cost/km trendek). | Jelenleg mock |
|
||||
|
||||
### 1.6 További kapcsolódó végpontok
|
||||
|
||||
| Metódus | Végpont | Cél | Router helye |
|
||||
|---------|---------|-----|-------------|
|
||||
| POST | /documents/upload/{parent_type}/{parent_id} | Dokumentumfeltöltés járműhöz | documents.py |
|
||||
| POST | /evidence/scan-registration | OCR regisztrációs dokumentum szkennelés | evidence.py |
|
||||
| GET | /search/match | Geo-keresés PostGIS segítségével | search.py |
|
||||
| POST | /services/hunt | Szerviz vadászat indítása | services.py |
|
||||
| GET | /reports/summary/{vehicle_id} | Jármű összesítő jelentés | reports.py |
|
||||
| GET | /reports/trends/{vehicle_id} | Jármű trend jelentés | reports.py |
|
||||
|
||||
---
|
||||
|
||||
## 2. 🗄️ Adatbázis Modell Réteg - Jármű Domain
|
||||
|
||||
### 2.1 Core Asset Model - A "Digital Twin"
|
||||
|
||||
Az Asset modell (backend/app/models/vehicle/asset.py) a központi entitás:
|
||||
|
||||
```
|
||||
Asset ──┬── AssetCatalog (katalógus kapcsolat)
|
||||
├── AssetFinancials (pénzügyi adatok)
|
||||
├── AssetCost (költségnapló)
|
||||
├── AssetEvent (eseménynapló - Digitális Szervizkönyv)
|
||||
├── VehicleLogbook (útnyilvántartás)
|
||||
├── AssetInspection (ellenőrzési napló)
|
||||
├── AssetReview (értékelések)
|
||||
├── AssetTelemetry (telemetria)
|
||||
├── AssetAssignment (szervezeti hozzárendelés)
|
||||
├── VehicleOwnership (tulajdonosváltás)
|
||||
└── ServiceRequest (szervizkérések)
|
||||
```
|
||||
|
||||
### 2.2 Főbb Modellek Leírása
|
||||
|
||||
| Modell | Tábla (séma) | Cél |
|
||||
|--------|-------------|-----|
|
||||
| Asset | vehicle.assets | Fizikai eszköz (Digital Twin) - Minden adat itt fut össze |
|
||||
| AssetCatalog | vehicle.vehicle_catalog | Jármű katalógus mesteradatok |
|
||||
| VehicleModelDefinition | vehicle.vehicle_model_definitions | Multi-Tier MDM Master - technikai igazságforrás |
|
||||
| VehicleType | vehicle.vehicle_types | Jármű kategóriák |
|
||||
| CostCategory | vehicle.cost_categories | Standardizált költségkategória fa |
|
||||
| AssetCost | vehicle.asset_costs | Üzemeltetési költség (TCO) |
|
||||
| AssetEvent | vehicle.asset_events | Digitális Szervizkönyv események |
|
||||
| VehicleOdometerState | vehicle.vehicle_odometer_states | Km óra állapot és becslés |
|
||||
| VehicleUserRating | vehicle.vehicle_user_ratings | Jármű értékelés 4 dimenzióban |
|
||||
|
||||
---
|
||||
|
||||
## 3. 🔌 Jelenlegi Frontend Implementáció
|
||||
|
||||
### 3.1 Garázs Nézet (DashboardView.vue)
|
||||
|
||||
A frontend egy egységes garázs nézetet használ, ahol az összes jármű egy grid-ben jelenik meg:
|
||||
|
||||
- Adatforrás: GET /api/v1/assets/vehicles -> vehicleStore.fetchVehicles()
|
||||
- Megjelenítés: Minden jármű egy kártyán, amely tartalmazza az EU-s rendszámtáblát, márka+modell nevet, gyártási évet, állapot score-t, futott km-t, státusz badge-t
|
||||
- UI élmény: Zen mód (UI elrejtése), 3D téreffektusok
|
||||
|
||||
### 3.2 Hiányzó Frontend Funkciók
|
||||
|
||||
A backend által biztosított API-k közül a frontend CSAK a járműlista lekérést használja. Az alábbi funkciók NINCSENEK implementálva:
|
||||
|
||||
- Jármű hozzáadása (POST /assets/vehicles)
|
||||
- Jármű részletes adatlapja (GET /assets/{id})
|
||||
- Költségnapló megjelenítése/rögzítése
|
||||
- Szerviznapló (karbantartási rekordok)
|
||||
- Katalógus böngésző (márka -> modell -> generáció -> motor)
|
||||
- TCO analitika és dashboard
|
||||
- Jármű értékelés
|
||||
- Dokumentumfeltöltés
|
||||
- Útnyilvántartás (logbook)
|
||||
|
||||
---
|
||||
|
||||
## 4. 🎯 Frontend Csoportosítási Javaslat
|
||||
|
||||
### 4.1 Javasolt Menü Szerkezet
|
||||
|
||||
Az alábbi csoportosítás a DDD (Domain-Driven Design) elveket követi, és a felhasználói élményt (UX) helyezi előtérbe:
|
||||
|
||||
```
|
||||
GARÁZS (fő nézet)
|
||||
│
|
||||
├── 🔧 JÁRMŰVEK
|
||||
│ ├── Garázs áttekintés <- Jelenlegi DashboardView
|
||||
│ ├── Jármű hozzáadása <- POST /assets/vehicles
|
||||
│ ├── Katalógus böngésző <- GET /catalog/makes -> models -> engines
|
||||
│ └── Jármű részletek <- GET /assets/{id}
|
||||
│
|
||||
├── 📊 PÉNZÜGYEK & TCO
|
||||
│ ├── Költségnapló <- GET /assets/{id}/costs, POST /expenses
|
||||
│ ├── Pénzügyi riport <- GET /assets/{id}/financial-summary
|
||||
│ ├── TCO Analitika <- GET /analytics/{id}/summary
|
||||
│ └── Dashboard analitika <- GET /analytics/dashboard
|
||||
│
|
||||
├── 🔧 KARBANTARTÁS
|
||||
│ ├── Szerviznapló <- GET /assets/{id}/maintenance
|
||||
│ ├── Új szerviz bejegyzés <- POST /assets/{id}/maintenance
|
||||
│ ├── Eseménynapló <- AssetEvent adatok
|
||||
│ └── Km óra állapot <- GET /admin/odometer/{id}
|
||||
│
|
||||
├── 📄 DOKUMENTUMOK
|
||||
│ ├── Feltöltés <- POST /documents/upload/vehicle/{id}
|
||||
│ └── OCR szkennelés <- POST /evidence/scan-registration
|
||||
│
|
||||
└── ⭐ KÖZÖSSÉG
|
||||
├── Értékelés <- POST /vehicles/{id}/ratings
|
||||
└── Vélemények <- GET /vehicles/{id}/ratings
|
||||
```
|
||||
|
||||
### 4.2 Javasolt Vue Router Struktúra
|
||||
|
||||
```
|
||||
/dashboard Garázs áttekintés (DashboardView)
|
||||
/vehicles/add Jármű hozzáadása
|
||||
/vehicles/catalog Katalógus böngésző
|
||||
/vehicles/{id} Jármű részletes nézet
|
||||
/vehicles/{id}/costs Költségek
|
||||
/vehicles/{id}/maintenance Szerviznapló
|
||||
/vehicles/{id}/analytics TCO Analitika
|
||||
/vehicles/{id}/documents Dokumentumok
|
||||
/vehicles/{id}/ratings Értékelések
|
||||
```
|
||||
|
||||
### 4.3 Javasolt Store Bővítés
|
||||
|
||||
A jelenlegi vehicleStore bővítendő az alábbiakkal:
|
||||
|
||||
```
|
||||
ÚJ state-ek:
|
||||
- selectedVehicle Aktuálisan kiválasztott jármű
|
||||
- vehicleCosts Költségnapló tételek
|
||||
- maintenanceRecords Szerviz rekordok
|
||||
- financialSummary Pénzügyi összesítő
|
||||
|
||||
ÚJ katalógus state-ek:
|
||||
- catalogMakes Márkák listája
|
||||
- catalogModels Modellek listája
|
||||
- catalogGenerations Generációk listája
|
||||
- catalogEngines Motorváltozatok listája
|
||||
|
||||
ÚJ action-ök:
|
||||
- fetchVehicleDetail(id)
|
||||
- createVehicle(payload)
|
||||
- fetchCosts(assetId)
|
||||
- createExpense(payload)
|
||||
- fetchMaintenance(assetId)
|
||||
- fetchFinancialSummary(assetId)
|
||||
- fetchAnalytics(assetId)
|
||||
- fetchMakes(vehicleClass?)
|
||||
- fetchModels(make)
|
||||
- fetchGenerations(make, model)
|
||||
- fetchEngines(make, model, gen)
|
||||
```
|
||||
|
||||
### 4.4 UX/UI Javaslatok
|
||||
|
||||
1. **Garázs Szűrők:** A járművek szűrhetők legyenek járműosztály (personal, motorcycle, commercial), státusz (active, draft, maintenance) és márka szerint.
|
||||
|
||||
2. **Jármű Kártya Információs Rétegek:**
|
||||
- 1. réteg (alap): Rendszám, márka, modell, státusz - látszik a grid-ben
|
||||
- 2. réteg (hover): Gyors infók (km, állapot, utolsó szerviz)
|
||||
- 3. réteg (kattintás): Teljes részletes nézet
|
||||
|
||||
3. **TCO Dashboard:** A garázs nézet tetején egy összesítő sáv (teljes flotta km, havi költség, átlagos állapot). Minden járműkártyán egy "TCO" gomb.
|
||||
|
||||
4. **Katalógus Böngésző:** Lépcsőzetes dropdown-ok (márka -> modell -> generáció -> motor). Gyors hozzáadás: "Add this to my garage" gomb.
|
||||
|
||||
---
|
||||
|
||||
## 5. 📝 Összefoglalás
|
||||
|
||||
A backend rendelkezésre álló API végpontok lefedik a járműkezelés teljes életciklusát:
|
||||
|
||||
| Fázis | API Végpontok | Állapot |
|
||||
|-------|--------------|---------|
|
||||
| Felfedezés (Katalógus) | /catalog/makes, /catalog/models, /catalog/generations, /catalog/engines | Backend kész |
|
||||
| Létrehozás | POST /assets/vehicles | Backend kész |
|
||||
| Megtekintés (Garázs) | GET /assets/vehicles, GET /assets/{id} | Backend kész, Frontend részleges |
|
||||
| Költségkövetés (TCO) | POST /expenses/, GET /assets/{id}/costs, GET /assets/{id}/financial-summary | Backend kész, Frontend hiányzik |
|
||||
| Karbantartás | GET/POST /assets/{id}/maintenance | Backend kész, Frontend hiányzik |
|
||||
| Analitika | GET /analytics/{id}/summary, GET /analytics/dashboard | Backend kész (részben mock), Frontend hiányzik |
|
||||
| Értékelés | GET/POST /vehicles/{id}/ratings | Backend kész, Frontend hiányzik |
|
||||
| Dokumentumok | POST /documents/upload/{parent_type}/{parent_id} | Backend kész, Frontend hiányzik |
|
||||
|
||||
**Következtetés:** A backend erős alapokon áll a járműkezeléshez. A frontenden a garázs nézet (DashboardView) csak a járművek listázását valósítja meg. A javasolt csoportosítás (Járművek -> Pénzügyek -> Karbantartás -> Dokumentumok -> Közösség) egy logikus, felhasználó-központú navigációs struktúrát adna a teljes jármű életciklus kezeléséhez.
|
||||
@@ -3,6 +3,12 @@ import { ref, computed } from 'vue'
|
||||
import router from '../router'
|
||||
import api from '../api/axios'
|
||||
|
||||
/**
|
||||
* Backend now sends address fields directly on the person object
|
||||
* with address_ prefix (e.g. address_city, address_zip, address_floor).
|
||||
* This interface is kept for backward compatibility but the actual
|
||||
* data comes as flat fields on PersonData.
|
||||
*/
|
||||
export interface AddressData {
|
||||
zip: string | null
|
||||
city: string | null
|
||||
@@ -28,7 +34,20 @@ export interface PersonData {
|
||||
identity_docs: Record<string, any> | null
|
||||
ice_contact: Record<string, any> | null
|
||||
is_active: boolean
|
||||
/** @deprecated Backend now sends address_* fields flat on person */
|
||||
address: AddressData | null
|
||||
// Flat address fields (new backend format)
|
||||
address_zip: string | null
|
||||
address_city: string | null
|
||||
address_street_name: string | null
|
||||
address_street_type: string | null
|
||||
address_house_number: string | null
|
||||
address_stairwell: string | null
|
||||
address_floor: string | null
|
||||
address_door: string | null
|
||||
address_hrsz: string | null
|
||||
address_parcel_id: string | null
|
||||
address_full_text: string | null
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
|
||||
@@ -403,13 +403,13 @@
|
||||
|
||||
<div v-else class="text-sm space-y-1">
|
||||
<p class="text-white font-medium">
|
||||
<template v-if="authStore.user?.person?.address?.zip || authStore.user?.person?.address?.city">
|
||||
{{ authStore.user?.person?.address?.zip }} {{ authStore.user?.person?.address?.city }},
|
||||
<template v-if="authStore.user?.person?.address?.address_zip || authStore.user?.person?.address?.address_city">
|
||||
{{ authStore.user?.person?.address?.address_zip }} {{ authStore.user?.person?.address?.address_city }},
|
||||
</template>
|
||||
{{ authStore.user?.person?.address?.street_name }} {{ authStore.user?.person?.address?.street_type }} {{ authStore.user?.person?.address?.house_number }}
|
||||
{{ authStore.user?.person?.address?.address_street_name }} {{ authStore.user?.person?.address?.address_street_type }} {{ authStore.user?.person?.address?.address_house_number }}
|
||||
</p>
|
||||
<p v-if="authStore.user?.person?.address?.stairwell || authStore.user?.person?.address?.floor || authStore.user?.person?.address?.door" class="text-white font-medium">
|
||||
Lépcsőház: {{ authStore.user?.person?.address?.stairwell || '—' }}, Emelet: {{ authStore.user?.person?.address?.floor || '—' }}, Ajtó: {{ authStore.user?.person?.address?.door || '—' }}
|
||||
<p v-if="authStore.user?.person?.address?.address_stairwell || authStore.user?.person?.address?.address_floor || authStore.user?.person?.address?.address_door" class="text-white font-medium">
|
||||
Lépcsőház: {{ authStore.user?.person?.address?.address_stairwell || '—' }}, Emelet: {{ authStore.user?.person?.address?.address_floor || '—' }}, Ajtó: {{ authStore.user?.person?.address?.address_door || '—' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,8 +427,8 @@
|
||||
<!-- ── 🩻 DIAGNOSTIC JSON DUMP ──────────────────────────────────── -->
|
||||
<div class="rounded-2xl border border-white/10 bg-black/40 p-4 mt-6 backdrop-blur-xl shadow-xl shadow-black/20">
|
||||
<details>
|
||||
<summary class="text-xs font-semibold text-[#00E5A0] cursor-pointer select-none mb-2">🩻 RÖNTGEN: authStore.user?.person?.address</summary>
|
||||
<pre class="text-xs text-green-400 bg-black p-4 mt-2 overflow-auto rounded-lg max-h-64">{{ JSON.stringify(authStore.user?.person?.address, null, 2) }}</pre>
|
||||
<summary class="text-xs font-semibold text-[#00E5A0] cursor-pointer select-none mb-2">🩻 RÖNTGEN: person (address_* fields)</summary>
|
||||
<pre class="text-xs text-green-400 bg-black p-4 mt-2 overflow-auto rounded-lg max-h-64">{{ JSON.stringify(authStore.user?.person, null, 2) }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -575,7 +575,6 @@ const isPageLoading = ref(true)
|
||||
|
||||
// ── Computed helpers ──────────────────────────────────────────────────
|
||||
const person = computed(() => authStore.user?.person ?? null)
|
||||
const address = computed(() => person.value?.address ?? null)
|
||||
const identityDocs = computed(() => person.value?.identity_docs ?? null)
|
||||
const isCityLoading = ref(false)
|
||||
|
||||
@@ -641,7 +640,6 @@ function formatDate(dateVal: string | null | undefined): string {
|
||||
|
||||
function startEditing() {
|
||||
const p = person.value
|
||||
const a = address.value
|
||||
const docs = identityDocs.value
|
||||
|
||||
editForm.first_name = p?.first_name || ''
|
||||
@@ -659,15 +657,16 @@ function startEditing() {
|
||||
editForm.license_expiry = formatDate(docs?.LICENSE?.expiry_date) || ''
|
||||
editForm.license_categories = docs?.LICENSE?.categories || ''
|
||||
|
||||
// Address
|
||||
editForm.address_zip = a?.zip || ''
|
||||
editForm.address_city = a?.city || ''
|
||||
editForm.address_street_name = a?.street_name || ''
|
||||
editForm.address_street_type = a?.street_type || ''
|
||||
editForm.address_house_number = a?.house_number || ''
|
||||
editForm.address_stairwell = a?.stairwell || ''
|
||||
editForm.address_floor = a?.floor || ''
|
||||
editForm.address_door = a?.door || ''
|
||||
// Address — backend sends address_* fields nested inside person.address
|
||||
const addr = p?.address
|
||||
editForm.address_zip = addr?.address_zip || ''
|
||||
editForm.address_city = addr?.address_city || ''
|
||||
editForm.address_street_name = addr?.address_street_name || ''
|
||||
editForm.address_street_type = addr?.address_street_type || ''
|
||||
editForm.address_house_number = addr?.address_house_number || ''
|
||||
editForm.address_stairwell = addr?.address_stairwell || ''
|
||||
editForm.address_floor = addr?.address_floor || ''
|
||||
editForm.address_door = addr?.address_door || ''
|
||||
|
||||
saveMessage.value = ''
|
||||
isEditing.value = true
|
||||
|
||||
253
tests/active/e2e_profile_address.py_old
Normal file
253
tests/active/e2e_profile_address.py_old
Normal file
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Playwright E2E Test: Profile Address Persistence (Stairwell / Floor / Door)
|
||||
|
||||
This test:
|
||||
1. Logs in as the configured test user (via LoginModal on the landing page).
|
||||
2. Navigates to the Profile page (/profile).
|
||||
3. Enters edit mode, fills every address field (city, zip, street, house number,
|
||||
stairwell, floor, door), and saves.
|
||||
4. Reloads the page and verifies that the stairwell, floor, and door values
|
||||
persisted after the page refresh.
|
||||
|
||||
Requirements:
|
||||
- playwright (Python) + dotenv
|
||||
- .env.test file at the project root with TEST_USER_EMAIL and TEST_USER_PASS
|
||||
|
||||
Usage:
|
||||
python tests/active/e2e_profile_address.py
|
||||
|
||||
To see the browser visually, change HEADLESS=False below.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from playwright.sync_api import sync_playwright, TimeoutError as PwTimeout
|
||||
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────
|
||||
BASE_URL = "https://app.servicefinder.hu"
|
||||
HEADLESS = True # Set to False for visual debugging
|
||||
SLOW_MO = 0 # ms delay between actions (useful for debugging)
|
||||
ACTION_TIMEOUT = 15_000 # ms – default timeout for element lookups
|
||||
|
||||
# ── Load .env.test from project root ──────────────────────────────────────
|
||||
ENV_PATH = Path(__file__).resolve().parents[1] / ".env.test"
|
||||
if not ENV_PATH.exists():
|
||||
raise FileNotFoundError(
|
||||
f".env.test not found at {ENV_PATH}. "
|
||||
"Create it with TEST_USER_EMAIL and TEST_USER_PASS."
|
||||
)
|
||||
|
||||
load_dotenv(dotenv_path=ENV_PATH, override=True)
|
||||
|
||||
TEST_EMAIL = os.getenv("TEST_USER_EMAIL")
|
||||
TEST_PASS = os.getenv("TEST_USER_PASS")
|
||||
|
||||
if not TEST_EMAIL or not TEST_PASS:
|
||||
raise ValueError(
|
||||
"TEST_USER_EMAIL and/or TEST_USER_PASS are missing from .env.test. "
|
||||
f"Loaded from: {ENV_PATH}"
|
||||
)
|
||||
|
||||
# ── Test data ──────────────────────────────────────────────────────────────
|
||||
ADDRESS_DATA = {
|
||||
"address_city": "Dunakeszi",
|
||||
"address_zip": "2120",
|
||||
"address_street_name": "Határ",
|
||||
"address_street_type": "utca",
|
||||
"address_house_number": "8",
|
||||
"address_stairwell": "A",
|
||||
"address_floor": "3",
|
||||
"address_door": "5",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Helper functions
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def log(msg: str) -> None:
|
||||
"""Simple stdout logger."""
|
||||
print(f"[E2E] {msg}")
|
||||
|
||||
|
||||
def fill_input(page, placeholder: str, value: str) -> None:
|
||||
"""Locate an <input> by its placeholder and fill it."""
|
||||
locator = page.locator(f'input[placeholder="{placeholder}"]')
|
||||
locator.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||||
locator.fill(value)
|
||||
log(f" Filled '{placeholder}' → '{value}'")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Main test flow
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def main() -> None:
|
||||
with sync_playwright() as pw:
|
||||
# ── 1. Launch browser (local or remote via BROWSERLESS_URL) ──
|
||||
browserless_url = os.getenv("BROWSERLESS_URL")
|
||||
if browserless_url:
|
||||
# Connect to a remote Browserless / Chrome container
|
||||
browser = pw.chromium.connect_over_cdp(browserless_url)
|
||||
log(f"Connected to remote browser at {browserless_url}")
|
||||
else:
|
||||
# Local fallback
|
||||
browser = pw.chromium.launch(
|
||||
headless=os.getenv("HEADLESS", "True").lower() == "true",
|
||||
slow_mo=SLOW_MO,
|
||||
)
|
||||
log(f"Browser launched locally (headless={os.getenv('HEADLESS', 'True')})")
|
||||
context = browser.new_context(
|
||||
viewport={"width": 1280, "height": 800},
|
||||
locale="hu-HU",
|
||||
)
|
||||
page = context.new_page()
|
||||
log(f"Browser launched (headless={HEADLESS})")
|
||||
|
||||
# ── 2. Open landing page & login ─────────────────────────────
|
||||
log("Navigating to landing page …")
|
||||
page.goto(BASE_URL, wait_until="networkidle", timeout=30_000)
|
||||
|
||||
# Click the "Garázs Nyitása" button to open the LoginModal
|
||||
login_trigger = page.locator("button:has-text('Garázs Nyitása')")
|
||||
login_trigger.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||||
login_trigger.click()
|
||||
log("Login modal opened")
|
||||
|
||||
# Fill email & password
|
||||
page.fill("#login-email", TEST_EMAIL)
|
||||
page.fill("#login-password", TEST_PASS)
|
||||
log("Login credentials entered")
|
||||
|
||||
# Click the submit button (the first .btn-premium inside the visible form)
|
||||
submit_btn = page.locator("form button[type='submit']").first
|
||||
submit_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||||
submit_btn.click()
|
||||
log("Login submitted – waiting for navigation …")
|
||||
|
||||
# Wait for redirect away from landing page (to /dashboard or /complete-kyc)
|
||||
try:
|
||||
page.wait_for_url(
|
||||
lambda url: "/dashboard" in url or "/complete-kyc" in url,
|
||||
timeout=15_000,
|
||||
)
|
||||
except PwTimeout:
|
||||
# If still on landing, maybe login failed – dump state
|
||||
log(f"URL after login attempt: {page.url}")
|
||||
page.screenshot(path="/tmp/e2e_login_fail.png")
|
||||
raise AssertionError(
|
||||
"Login failed – did not redirect to dashboard. "
|
||||
"Check credentials or UI state."
|
||||
)
|
||||
|
||||
log(f"Login successful – current URL: {page.url}")
|
||||
|
||||
# ── 3. Navigate to Profile page ──────────────────────────────
|
||||
log("Navigating to /profile …")
|
||||
page.goto(f"{BASE_URL}/profile", wait_until="networkidle", timeout=20_000)
|
||||
|
||||
# Wait for the profile page to finish loading (spinner disappears)
|
||||
page.wait_for_selector("text=Profil beállítások", timeout=ACTION_TIMEOUT)
|
||||
# Also wait until the loading spinner is gone
|
||||
page.wait_for_function(
|
||||
"() => !document.querySelector('.animate-spin')",
|
||||
timeout=15_000,
|
||||
)
|
||||
log("Profile page loaded")
|
||||
|
||||
# ── 4. Enter edit mode ───────────────────────────────────────
|
||||
edit_btn = page.locator("button:has-text('Szerkesztés')")
|
||||
edit_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||||
edit_btn.click()
|
||||
log("Edit mode activated")
|
||||
|
||||
# ── 5. Fill in all address fields ────────────────────────────
|
||||
log("Filling address form …")
|
||||
fill_input(page, "Irányítószám", ADDRESS_DATA["address_zip"])
|
||||
fill_input(page, "Város", ADDRESS_DATA["address_city"])
|
||||
fill_input(page, "Utca", ADDRESS_DATA["address_street_name"])
|
||||
fill_input(page, "Közterület jellege", ADDRESS_DATA["address_street_type"])
|
||||
fill_input(page, "Házszám", ADDRESS_DATA["address_house_number"])
|
||||
fill_input(page, "Lépcsőház", ADDRESS_DATA["address_stairwell"])
|
||||
fill_input(page, "Emelet", ADDRESS_DATA["address_floor"])
|
||||
fill_input(page, "Ajtó", ADDRESS_DATA["address_door"])
|
||||
|
||||
# ── 6. Save ──────────────────────────────────────────────────
|
||||
save_btn = page.locator("button:has-text('Mentés')")
|
||||
save_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
|
||||
save_btn.click()
|
||||
log("Save clicked – waiting for confirmation …")
|
||||
|
||||
# Wait for the success message to appear
|
||||
success_locator = page.locator("text=A profil adatok és okmányok sikeresen frissítve!")
|
||||
try:
|
||||
success_locator.wait_for(state="visible", timeout=10_000)
|
||||
log("Save succeeded – success message visible")
|
||||
except PwTimeout:
|
||||
# Check for error message
|
||||
error_locator = page.locator(".text-red-400")
|
||||
if error_locator.is_visible():
|
||||
error_text = error_locator.text_content()
|
||||
log(f"Save FAILED with error: {error_text}")
|
||||
page.screenshot(path="/tmp/e2e_save_fail.png")
|
||||
raise AssertionError(f"Save failed: {error_text}")
|
||||
|
||||
page.screenshot(path="/tmp/e2e_save_timeout.png")
|
||||
raise AssertionError(
|
||||
"Save timeout – neither success nor error message appeared."
|
||||
)
|
||||
|
||||
# ── 7. Reload and verify persistence ─────────────────────────
|
||||
log("Reloading page to verify persistence …")
|
||||
page.reload(wait_until="networkidle")
|
||||
page.wait_for_selector("text=Profil beállítások", timeout=ACTION_TIMEOUT)
|
||||
page.wait_for_function(
|
||||
"() => !document.querySelector('.animate-spin')",
|
||||
timeout=15_000,
|
||||
)
|
||||
log("Page reloaded")
|
||||
|
||||
# Read the display text in the read-only address section
|
||||
# The read-only view shows: Lépcsőház: …, Emelet: …, Ajtó: …
|
||||
page_body = page.locator("body").inner_text()
|
||||
log("── Read-only address block ──")
|
||||
|
||||
# Use a regex to extract values
|
||||
stairwell_match = re.search(r"Lépcsőház:\s*(\S+)", page_body)
|
||||
floor_match = re.search(r"Emelet:\s*(\S+)", page_body)
|
||||
door_match = re.search(r"Ajtó:\s*(\S+)", page_body)
|
||||
|
||||
stairwell_val = stairwell_match.group(1) if stairwell_match else ""
|
||||
floor_val = floor_match.group(1) if floor_match else ""
|
||||
door_val = door_match.group(1) if door_match else ""
|
||||
|
||||
log(f" Stairwell (Lépcsőház): '{stairwell_val}'")
|
||||
log(f" Floor (Emelet): '{floor_val}'")
|
||||
log(f" Door (Ajtó): '{door_val}'")
|
||||
|
||||
# ── 8. Assertion: all three values must be present ───────────
|
||||
errors = []
|
||||
if not stairwell_val or stairwell_val in ("—", "null", "None"):
|
||||
errors.append(f"stairwell (expected '{ADDRESS_DATA['address_stairwell']}', got '{stairwell_val}')")
|
||||
if not floor_val or floor_val in ("—", "null", "None"):
|
||||
errors.append(f"floor (expected '{ADDRESS_DATA['address_floor']}', got '{floor_val}')")
|
||||
if not door_val or door_val in ("—", "null", "None"):
|
||||
errors.append(f"door (expected '{ADDRESS_DATA['address_door']}', got '{door_val}')")
|
||||
|
||||
if errors:
|
||||
page.screenshot(path="/tmp/e2e_persistence_fail.png")
|
||||
raise AssertionError(
|
||||
"Hibás backend mentés! Az emelet/ajtó adatok elvesztek az oldal "
|
||||
f"frissítése után.\n Missing/empty fields: {', '.join(errors)}"
|
||||
)
|
||||
|
||||
log("Sikeres E2E teszt: Az adatok megmaradtak a backend frissítés után is!")
|
||||
log("ALL CHECKS PASSED.\n")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user