frontend 2026-06-10 bontva a 2 felület
This commit is contained in:
139
plans/logic_spec_dashboard_header_restructure.md
Normal file
139
plans/logic_spec_dashboard_header_restructure.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 📐 Logic Spec: Dashboard Header Restructure
|
||||
|
||||
## 1. Cél és Masterbook 2 illeszkedés
|
||||
|
||||
**Cél:** A privát garázs dashboard fejlécének (`DashboardHeader.vue`) átalakítása a felhasználó specifikációja szerint:
|
||||
- **Bal:** 👋 Üdvözlés + keresztnév
|
||||
- **Közép:** `{firstName}_garázsa` (i18n kulcsból)
|
||||
- **Jobb:** Cégváltó dropdown → Nyelvváltó → Avatár menü (sorrendben)
|
||||
- **Layout struktúra:** Marad a `PrivateLayout` + `DashboardHeader` felosztás
|
||||
|
||||
**Masterbook 2.0 illeszkedés:** A feladat illeszkedik az Epic 11 (Public Frontend) és a B2C dashboard UX specifikációjához.
|
||||
|
||||
---
|
||||
|
||||
## 2. Jelenlegi Problémák (`DashboardHeader.vue` - 685 sor)
|
||||
|
||||
| Probléma | Leírás |
|
||||
|----------|--------|
|
||||
| P1 - Túl komplex | 7 funkció egy komponensben (WelcomeCard, Logo, Funkciók menu, Center label, Org dropdown, LanguageSwitcher, Avatar) |
|
||||
| P2 - Welcome Card | Lebegő elem, nem a header része - megtartható, de auditálni kell |
|
||||
| P3 - Funkciók menu | Nem illik a header-be, 100+ sor Bento dropdown - **ELTÁVOLÍTANDÓ** |
|
||||
| P4 - Logo + Brand | 2 sor + kép - **ELTÁVOLÍTANDÓ** a header-ből |
|
||||
| P5 - Center label | Duplikált logika a jobb oldali org dropdown-nal |
|
||||
| P6 - Jobb oldal sorrend | Jelenleg: Org dropdown → LanguageSwitcher → Avatar. **Megtartandó sorrendben** |
|
||||
|
||||
---
|
||||
|
||||
## 3. Új 3-Oszlopos Header Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ [BAL] [KÖZÉP] [JOBB] │
|
||||
│ 👋 Üdv, János! János_garázsa [🏢 Cégem ▼] [🌐] [👤] │
|
||||
│ ↓ ↓ ↓ │
|
||||
│ Org Dropdown Lang Avatar│
|
||||
│ (cég listával) Switcher│
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.1 Bal oldal: Welcome + keresztnév
|
||||
|
||||
```vue
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg">👋</span>
|
||||
<span class="text-sm font-medium text-white/80">
|
||||
{{ t('header.welcome') }}, <span class="font-semibold text-white">{{ displayName }}</span>!
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
**`displayName` computed** - már létezik, elsődlegesen `props.firstName`, fallback email prefix.
|
||||
|
||||
### 3.2 Középső: `{firstName}_garázsa`
|
||||
|
||||
```vue
|
||||
<div class="absolute left-1/2 -translate-x-1/2">
|
||||
<span class="text-sm font-semibold text-white/70 tracking-wide">
|
||||
{{ centerLabel }}
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
**`centerLabel` computed logika:**
|
||||
- Ha `isOnCompanyGarage` (szervezeti nézetben): `t('header.companyGarageLabel', { name: activeCompanyName })`
|
||||
- Ha `isCorporateMode` (vállalati módban): `t('header.companyGarageLabel', { name: activeCompanyName })`
|
||||
- Különben (személyes mód): `t('header.personalGarageLabel', { name: firstName })`
|
||||
|
||||
### 3.3 Jobb oldal (sorrendben: Cég → Nyelv → Avatár)
|
||||
|
||||
**Cég dropdown** - Meglévő funkció tisztítva:
|
||||
- Megtartani: org-dropdown-container (dropdown cég listával)
|
||||
- Eltávolítani: duplikált "Funkciók menüben lévő Organization Menu Item"
|
||||
- Állapotok: nincs cég → "➕ Új cég" (navigáció `/company/onboard`); van cég → "🏢 Cégem" (dropdown); corporate mód → "👤 Személyes Garázs" (visszaváltás)
|
||||
|
||||
**Nyelvváltó** - Változatlan, 6 nyelv támogatása.
|
||||
|
||||
**Avatár menü** - Változatlan (Profile, Logout).
|
||||
|
||||
---
|
||||
|
||||
## 4. Eltávolítandó elemek
|
||||
|
||||
| Elem | Sorok | Hova kerül? |
|
||||
|------|-------|-------------|
|
||||
| Logo + Brand | 30-43 | ELTÁVOLÍTVA |
|
||||
| Funkciók menu (Bento) | 46-154 | ELTÁVOLÍTVA, később `DashboardView.vue`-ba |
|
||||
| Funkciók-belüli Org Menu Item | 119-135 | ELTÁVOLÍTVA (duplikáció) |
|
||||
|
||||
**Megtartva:** Floating Welcome Card (2-23. sorok) - vizuálisan elkülönül.
|
||||
|
||||
---
|
||||
|
||||
## 5. Érintett fájlok
|
||||
|
||||
| Fájl | Művelet | Mérték |
|
||||
|------|---------|--------|
|
||||
| `frontend/src/components/DashboardHeader.vue` | **ÁTÍRÁS** ~685 → ~300 sor | Nagy |
|
||||
| `frontend/src/i18n/hu.ts` | Ellenőrzés (várhatóan nincs új kulcs) | Apró |
|
||||
| `frontend/src/i18n/en.ts` | Ellenőrzés (várhatóan nincs új kulcs) | Apró |
|
||||
|
||||
**Nem érintett:** `PrivateLayout.vue`, `OrganizationLayout.vue`, `LanguageSwitcher.vue`, `auth.ts`
|
||||
|
||||
---
|
||||
|
||||
## 6. AuthStore használt értékei
|
||||
|
||||
| Érték | Típus |
|
||||
|-------|-------|
|
||||
| `authStore.user?.first_name` | `string \| undefined` |
|
||||
| `authStore.user?.email` | `string \| undefined` |
|
||||
| `authStore.myOrganizations` | `OrganizationItem[]` |
|
||||
| `authStore.hasBusinessOrganization` | `boolean` (computed) |
|
||||
| `authStore.isCorporateMode` | `boolean` (computed) |
|
||||
| `authStore.businessOrganization` | `OrganizationItem \| undefined` (computed) |
|
||||
| `authStore.switchOrganization(id \| null)` | `function` (async) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Tesztelési terv
|
||||
|
||||
1. `vite build` hiba nélkül
|
||||
2. Vizuális: Bal 👋 Welcome; Közép `{name}_garázsa`; Jobb: Cég → 🌐 → Avatár
|
||||
3. Cég dropdown működik (list, select, back)
|
||||
4. LanguageSwitcher működik (6 nyelv)
|
||||
5. Avatár menü működik (Profile, Logout)
|
||||
|
||||
---
|
||||
|
||||
## 8. Módosítási sorrend
|
||||
|
||||
1. `DashboardHeader.vue` - Teljes átírás
|
||||
2. `hu.ts` / `en.ts` - Ellenőrzés
|
||||
3. `vite build` - Verifikáció
|
||||
|
||||
---
|
||||
|
||||
## ⏸️ Jóváhagyási pont
|
||||
|
||||
**Kérem a felhasználó jóváhagyását a fenti specifikációhoz!**
|
||||
232
plans/logic_spec_opten_integration.md
Normal file
232
plans/logic_spec_opten_integration.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# logic_spec_opten_integration.md — Opten REST API Integrációs Terv
|
||||
|
||||
## Modul Célja
|
||||
A jelenlegi mock `GET /lookup-tax/{tax_number}` végpont lecserélése valós [Opten REST API](https://www.opten.hu/) hívásra. A frontend [`CompanyOnboardingView.vue`](frontend/src/views/organization/CompanyOnboardingView.vue:654) `lookupTaxNumber()` függvénye már készen áll a valós adatok fogadására — csak a backend oldali integráció hiányzik.
|
||||
|
||||
## Masterbook 2.0 Illeszkedés
|
||||
- **DDD Séma:** `identity.organizations` — a cégadatok automatikus kitöltése csökkenti a hibalehetőséget.
|
||||
- **Geo-logika:** Csak magyar (HU) adószámok esetén aktív. Más országok esetén a mock marad, vagy külön integráció később.
|
||||
|
||||
---
|
||||
|
||||
## Érintett Fájlok és Műveletek
|
||||
|
||||
| # | Fájl | Művelet | Leírás |
|
||||
|---|------|---------|--------|
|
||||
| 1 | [`backend/app/core/config.py`](backend/app/core/config.py:11) | ✏️ Módosítás | `OPTEN_API_KEY`, `OPTEN_API_BASE_URL`, `OPTEN_CACHE_TTL` hozzáadása a `Settings` osztályhoz |
|
||||
| 2 | [`backend/app/services/opten_service.py`](backend/app/services/opten_service.py) | ➕ **ÚJ fájl** | `OptenService` osztály: `httpx.AsyncClient` + `tenacity` retry logika |
|
||||
| 3 | [`backend/app/schemas/organization.py`](backend/app/schemas/organization.py) | ➕ **ÚJ fájl** | `TaxLookupResponse` Pydantic séma |
|
||||
| 4 | [`backend/app/api/v1/endpoints/organizations.py`](backend/app/api/v1/endpoints/organizations.py:125) | ✏️ Módosítás | Mock végpont → valós `OptenService.lookup_by_tax_number()` hívás |
|
||||
| 5 | [`backend/requirements.txt`](backend/requirements.txt:17) | 🟢 **Nincs teendő** | `httpx` már szerepel. `tenacity` opcionális (retry logikához) |
|
||||
| 6 | [`frontend/src/views/organization/CompanyOnboardingView.vue`](frontend/src/views/organization/CompanyOnboardingView.vue:654) | 🟢 **Már kész** | `lookupTaxNumber()` teljes mértékben használható |
|
||||
|
||||
---
|
||||
|
||||
## Adatmodell — TaxLookupResponse
|
||||
|
||||
```python
|
||||
# backend/app/schemas/organization.py
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class TaxLookupResponse(BaseModel):
|
||||
full_name: str
|
||||
name: str
|
||||
display_name: Optional[str] = None
|
||||
address_zip: str
|
||||
address_city: str
|
||||
address_street_name: str
|
||||
address_street_type: Optional[str] = None
|
||||
address_house_number: str
|
||||
legal_form: Optional[str] = None # pl. "Kft.", "Bt.", "Zrt."
|
||||
status: Optional[str] = None # pl. "active", "dissolved"
|
||||
```
|
||||
|
||||
### Frontend Kompatibilitás
|
||||
A fenti mezők teljes mértékben lefedik a frontend [`CompanyOnboardingView.vue:654-684`](frontend/src/views/organization/CompanyOnboardingView.vue:654) által várt mezőket. Az új `legal_form` és `status` mezők a frontend `tax_lookup_result` objektumban tárolódhatnak későbbi felhasználásra.
|
||||
|
||||
---
|
||||
|
||||
## Service Réteg — OptenService
|
||||
|
||||
### Osztály Felépítése
|
||||
|
||||
```python
|
||||
# backend/app/services/opten_service.py
|
||||
import logging
|
||||
from typing import Optional
|
||||
import httpx
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OptenService:
|
||||
def __init__(self, api_key: str, base_url: str = "https://api.opten.hu/v2"):
|
||||
self.api_key = api_key
|
||||
self.base_url = base_url
|
||||
self.client = httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
headers={"X-Api-Key": self.api_key, "Accept": "application/json"},
|
||||
timeout=10.0
|
||||
)
|
||||
|
||||
@retry(stop=stop_after_attempt(2), wait=wait_exponential(multiplier=1, min=1, max=5))
|
||||
async def lookup_by_tax_number(self, tax_number: str) -> dict:
|
||||
"""Lekérdezi az Opten API-t adószám alapján."""
|
||||
normalized = self._normalize_tax_number(tax_number)
|
||||
response = await self.client.get(f"/company/{normalized}")
|
||||
response.raise_for_status()
|
||||
return self._map_opten_response(response.json())
|
||||
|
||||
def _normalize_tax_number(self, raw: str) -> str:
|
||||
"""Csak számjegyeket tart meg (8 vagy 10 vagy 11 karakter)."""
|
||||
digits = "".join(c for c in raw if c.isdigit())
|
||||
# HU adószám: 8 számjegy (egyéni) vagy 10-11 számjegy (cég, áfával)
|
||||
return digits[:11]
|
||||
|
||||
def _map_opten_response(self, data: dict) -> dict:
|
||||
"""Opten API válasz → TaxLookupResponse formátumba."""
|
||||
return {
|
||||
"full_name": data.get("cegnev", ""),
|
||||
"name": data.get("rovitett_cegnev", data.get("cegnev", "")),
|
||||
"display_name": data.get("rovitett_cegnev"),
|
||||
"address_zip": data.get("iranyitoszam", ""),
|
||||
"address_city": data.get("telepules", ""),
|
||||
"address_street_name": data.get("kozt_nev", ""),
|
||||
"address_street_type": data.get("kozt_jelleg", ""),
|
||||
"address_house_number": str(data.get("hazszam", "")),
|
||||
"legal_form": data.get("cegforma", ""),
|
||||
"status": data.get("ceg_statusz", ""),
|
||||
}
|
||||
|
||||
async def close(self):
|
||||
await self.client.aclose()
|
||||
```
|
||||
|
||||
### Gráciális Fallback
|
||||
Ha az `OPTEN_API_KEY` nincs beállítva (üres string), a végpont NEM dob hibát, hanem visszaadja a mock adatokat:
|
||||
|
||||
```python
|
||||
# backend/app/api/v1/endpoints/organizations.py
|
||||
@router.get("/lookup-tax/{tax_number}", response_model=TaxLookupResponse)
|
||||
async def lookup_tax_number(tax_number: str, db: AsyncSession = Depends(get_db)):
|
||||
settings = get_settings()
|
||||
if not settings.OPTEN_API_KEY:
|
||||
logger.warning("OPTEN_API_KEY not set — returning mock data")
|
||||
return await _mock_lookup(tax_number)
|
||||
service = OptenService(settings.OPTEN_API_KEY, settings.OPTEN_API_BASE_URL)
|
||||
try:
|
||||
result = await service.lookup_by_tax_number(tax_number)
|
||||
return result
|
||||
except httpx.HTTPStatusError as e:
|
||||
return await _handle_opten_error(e)
|
||||
finally:
|
||||
await service.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Geo-Logika
|
||||
|
||||
| Ország | Forrás | Státusz |
|
||||
|--------|--------|---------|
|
||||
| Magyarország (HU) | Opten REST API | ✅ Ezen terv |
|
||||
| Más EU országok | Mock (később: saját provider) | ⏳ Tervezett |
|
||||
|
||||
Csak magyar adószámok esetén aktív az Opten hívás. A `_normalize_tax_number()` metódus alapján: ha a szám nem felel meg a HU formátumnak (8-11 számjegy), a végpont mock adatokat ad vissza.
|
||||
|
||||
---
|
||||
|
||||
## Hibakezelési Mátrix
|
||||
|
||||
| Opten Hiba | HTTP Státusz | Felhasználói Üzenet |
|
||||
|------------|-------------|---------------------|
|
||||
| 404 — Nincs ilyen cég | 404 | "Nem található cég ezzel az adószámmal." |
|
||||
| 403 — Rossz API kulcs | 500 | "Belső szolgáltatói hiba. Kérjük, próbáld később." |
|
||||
| 429 — Rate limit | 429 | "Túl sok lekérdezés. Kérjük, várj egy percet." |
|
||||
| 422 — Érvénytelen adószám | 422 | "Érvénytelen adószám formátum." |
|
||||
| 504 — Gateway timeout | 504 | "A szolgáltató nem válaszol. Próbáld újra később." |
|
||||
| Ismeretlen hiba | 502 | "Váratlan hiba történt. Próbáld újra később." |
|
||||
|
||||
### `_handle_opten_error()` pszeudokód
|
||||
```python
|
||||
async def _handle_opten_error(e: httpx.HTTPStatusError) -> dict:
|
||||
status_map = {
|
||||
404: (404, "Nem található cég ezzel az adószámmal."),
|
||||
403: (500, "Belső szolgáltatói hiba."),
|
||||
429: (429, "Túl sok lekérdezés. Várj egy percet."),
|
||||
422: (422, "Érvénytelen adószám formátum."),
|
||||
504: (504, "A szolgáltató nem válaszol."),
|
||||
}
|
||||
http_code, msg = status_map.get(e.response.status_code, (502, "Váratlan hiba."))
|
||||
raise HTTPException(status_code=http_code, detail=msg)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Biztonsági Megfontolások
|
||||
|
||||
| # | Intézkedés | Leírás |
|
||||
|---|-----------|--------|
|
||||
| 1 | `.env`-ben tartás | `OPTEN_API_KEY` csak a `.env` fájlban, SOHOHA hardcode-olva |
|
||||
| 2 | Rate limiting | Nginx/API Gateway szintű korlátozás a `/lookup-tax/` végpontra |
|
||||
| 3 | Audit log | Minden Opten hívás naplózása: tax_number, timestamp, sikeresség |
|
||||
| 4 | Cache | `OPTEN_CACHE_TTL` (alapértelmezett: 3600s) — redis cache a gyakori ismétlődésekhez |
|
||||
| 5 | Adatvédelem | A visszaadott adatok csak a szervezet létrehozásához használhatók |
|
||||
|
||||
---
|
||||
|
||||
## Teszt Terv
|
||||
|
||||
### Unit tesztek
|
||||
| Teszt | Leírás |
|
||||
|-------|--------|
|
||||
| `test_normalize_tax_number` | 8, 10, 11 jegyű, kötőjeles, szóközös bemenetek |
|
||||
| `test_map_opten_response` | Opten JSON → TaxLookupResponse mezőtérkép |
|
||||
| `test_mock_fallback` | Ha nincs API kulcs, mock adat jön |
|
||||
| `test_error_mapping` | Minden hibaeset lefedése |
|
||||
|
||||
### Integrációs tesztek
|
||||
```bash
|
||||
# Teszt élő Opten API-val (ha van API kulcs)
|
||||
docker compose exec sf_api python3 -c "
|
||||
from app.services.opten_service import OptenService
|
||||
import asyncio
|
||||
async def test():
|
||||
s = OptenService('teszt_kulcs')
|
||||
result = await s.lookup_by_tax_number('12345678')
|
||||
print(result)
|
||||
await s.close()
|
||||
asyncio.run(test())
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Végrehajtási Terv (6 Kártya)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[1. Config: OPTEN_API_KEY] --> B[2. Séma: TaxLookupResponse]
|
||||
A --> C[3. Service: OptenService]
|
||||
B --> D[4. Endpoint: Mock → Valós]
|
||||
C --> D
|
||||
D --> E[5. Tesztek]
|
||||
E --> F[6. Dokumentáció]
|
||||
```
|
||||
|
||||
| # | Kártya Név | Függőség | Becsült Méret |
|
||||
|---|-----------|----------|---------------|
|
||||
| 1 | Config: `OPTEN_API_KEY` hozzáadása | Nincs | S (< 10 sor) |
|
||||
| 2 | Séma: `TaxLookupResponse` Pydantic | 1 | S (< 20 sor) |
|
||||
| 3 | Service: `OptenService` osztály | Nincs | M (~60 sor) |
|
||||
| 4 | Endpoint: Mock → valós hívás | 2, 3 | S (~20 sor) |
|
||||
| 5 | Tesztek (unit + smoke) | 4 | M (~50 sor) |
|
||||
| 6 | Dokumentáció frissítése | 5 | S (< 20 sor) |
|
||||
|
||||
---
|
||||
|
||||
## Kockázatok
|
||||
- **Opten API kulcs beszerzése** — a fejlesztés mock módban is végezhető.
|
||||
- **Opten API változás** — a `_map_opten_response()` metódus könnyen adaptálható.
|
||||
- **Éles rate limit** — szükség esetén Opten előfizetés frissítése.
|
||||
Reference in New Issue
Block a user