feat: Add JWT auth and nomenclature sync to data-entry-app

Integrate shared JWT authentication into data-entry-app:
- Add Oracle pool initialization for auth service
- Add AuthenticationMiddleware to protect API routes
- Update all receipt endpoints to use CurrentUser from JWT
- Add shared auth router (/api/auth/login, /api/auth/refresh)

Add nomenclature synchronization feature:
- Create SQLite models for synced suppliers, local suppliers, and cash registers
- Add nomenclature router with sync triggers and CRUD endpoints
- Add sync service for Oracle → SQLite nomenclature data
- Update nomenclature_service to use synced SQLite data with fallbacks

Create shared frontend components:
- Add shared/frontend/ with LoginView.vue, auth store factory, login.css
- Integrate shared login and auth into data-entry-app frontend
- Add axios-based API service with token refresh interceptor

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-14 18:36:24 +02:00
parent 682a4b64b9
commit c5fde510a8
37 changed files with 28907 additions and 903 deletions

View File

@@ -0,0 +1,346 @@
# Plan: Implementare Auth SSO + Nomenclatoare Sync
> **Plan Handover Document** - Salvat pentru continuare în altă sesiune
> **Data**: 2025-12-13 | **Branch**: `feature/data-entry-receipts`
## Obiectiv
Integrare autentificare SSO și sincronizare nomenclatoare Oracle în data-entry-app conform `IMPLEMENTATION_PLAN_AUTH_UNITAR.md`.
---
## Instrucțiuni Implementare
### Metodologie
1. **Execută fazele în paralel** unde e posibil (Faza 1+2 pot rula simultan, Faza 3+4 pot rula simultan)
2. **Folosește agenți Task** pentru viteza - lansează agenți în paralel pentru task-uri independente
3. **Testează după fiecare fază** - nu trece la următoarea fără validare
4. **Urmărește progresul** în acest fișier - marchează task-urile completate cu ✅
### Comenzi de Start
```bash
# Asigură-te că SSH tunnel rulează (pentru Oracle)
./ssh_tunnel.sh start
# Backend reports (pentru auth API - port 8001)
cd reports-app/backend && uvicorn app.main:app --reload --port 8001
# Backend data-entry (port 8003)
cd data-entry-app/backend && uvicorn app.main:app --reload --port 8003
# Frontend data-entry (port 3010)
cd data-entry-app/frontend && npm run dev
```
### Progres Implementare
- [x] **FAZA 1**: Auth Backend - ✅ 6/6 task-uri COMPLETE
- [x] **FAZA 2**: Auth Frontend - ✅ 6/6 task-uri COMPLETE
- [x] **FAZA 3**: Nomenclatoare Sync - ✅ 6/6 task-uri COMPLETE
- [x] **FAZA 4**: OCR + Supplier Search - ✅ 2/2 task-uri COMPLETE
> **Status**: ✅ **IMPLEMENTARE COMPLETĂ** - 2025-12-13
---
## Stare Curentă (IMPLEMENTAT)
### Backend Data-Entry ✅
- ✅ Models: Receipt, ReceiptAttachment, AccountingEntry - complete
- ✅ CRUD operations - complete
- ✅ API Routers: receipts.py, ocr.py, **nomenclature.py**
- ✅ Services: receipt_service, ocr_service, **sync_service**
- ✅ Workflow: DRAFT → PENDING → APPROVED/REJECTED
-**Auth**: Integrare shared/auth (middleware + CurrentUser)
-**Nomenclatoare**: SQLite sync (SyncedSupplier, LocalSupplier, SyncedCashRegister)
-`sys.path.insert` pentru shared/ în main.py
### Frontend Data-Entry ✅
- ✅ Views: List, Create, Detail, Approval, **LoginView**
- ✅ Components: OCR components + **Create Supplier Dialog**
- ✅ Store: receiptsStore.js + **auth.js**
- ✅ Router: routes + **auth guards + /login**
-**Auth Store**: `src/stores/auth.js` - creat
-**Login View**: `src/views/LoginView.vue` - creat
-**Router Guards**: beforeEach cu requiresAuth
-**API Service**: `src/services/api.js` - creat cu interceptors
### Shared Auth (disponibil pentru integrare)
-`shared/auth/routes.py` - `create_auth_router()` (linia 39-430)
-`shared/auth/middleware.py` - `AuthenticationMiddleware`
-`shared/auth/dependencies.py` - `get_current_user`
-`shared/auth/models.py` - `CurrentUser`, `TokenResponse`
### Referință Reports-App (pentru copiere)
- `reports-app/frontend/src/stores/auth.js` - 119 linii
- `reports-app/frontend/src/services/api.js` - 141 linii
- `reports-app/frontend/src/views/LoginView.vue` - 367 linii
- `reports-app/frontend/src/router/index.js` - auth guard la liniile 96-114
---
## Faze Implementare
### FAZA 1: Auth Backend (6 task-uri)
#### Task 1.1: Adaugă AuthenticationMiddleware în main.py
**Fișier**: `data-entry-app/backend/app/main.py`
**Acțiune**: După CORS middleware (linia 75), adaugă:
```python
from auth.middleware import AuthenticationMiddleware
app.add_middleware(
AuthenticationMiddleware,
excluded_paths=["/docs", "/redoc", "/openapi.json", "/health", "/", "/api/auth/login", "/api/auth/refresh"]
)
```
#### Task 1.2: Adaugă Auth Router în main.py
**Fișier**: `data-entry-app/backend/app/main.py`
**Acțiune**: După include_router pentru ocr (linia 98), adaugă:
```python
from auth.routes import create_auth_router
auth_router = create_auth_router()
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
```
#### Task 1.3: Înlocuiește get_current_user în receipts.py
**Fișier**: `data-entry-app/backend/app/routers/receipts.py`
**Acțiune**: Șterge liniile 38-59 și înlocuiește cu:
```python
from auth.dependencies import get_current_user
from auth.models import CurrentUser
```
Apoi actualizează type hints: `current_user: str``current_user: CurrentUser`
Și accesează `current_user.username` în loc de `current_user`
#### Task 1.4: Înlocuiește get_current_user în ocr.py
**Fișier**: `data-entry-app/backend/app/routers/ocr.py`
**Acțiune**: Similar cu receipts.py, adaugă importurile auth și folosește `CurrentUser`
#### Task 1.5: Actualizează type hints în toate endpoint-urile
Actualizează toate funcțiile care folosesc `current_user: str` să folosească `current_user: CurrentUser`
#### Task 1.6: Testare backend auth
```bash
cd data-entry-app/backend
uvicorn app.main:app --reload --port 8003
# Test: curl http://localhost:8003/api/receipts/ → 401 Unauthorized
```
---
### FAZA 2: Auth Frontend (6 task-uri)
#### Task 2.1: Crează API service
**Fișier NOU**: `data-entry-app/frontend/src/services/api.js`
**Acțiune**: Copiază din `reports-app/frontend/src/services/api.js` cu modificări:
- Schimbă BASE_URL pentru a funcționa cu proxy-ul
- Modifică refresh token URL
#### Task 2.2: Crează Auth Store
**Fișier NOU**: `data-entry-app/frontend/src/stores/auth.js`
**Acțiune**: Copiază din `reports-app/frontend/src/stores/auth.js`
- Modifică import apiService din `../services/api`
#### Task 2.3: Crează LoginView
**Fișier NOU**: `data-entry-app/frontend/src/views/LoginView.vue`
**Acțiune**: Copiază din `reports-app/frontend/src/views/LoginView.vue`
- Schimbă titlul: "ROA Reports" → "Data Entry"
- Schimbă subtitle: "Rapoarte ERP" → "Introducere Bonuri Fiscale"
- Schimbă redirect după login: "/dashboard" → "/"
#### Task 2.4: Actualizează Router cu auth guards
**Fișier**: `data-entry-app/frontend/src/router/index.js`
**Acțiune**: Adaugă auth guard similar cu reports-app (liniile 96-114)
```javascript
import { useAuthStore } from '@/stores/auth'
// Adaugă rută login
// Adaugă meta: { requiresAuth: true } la rutele protejate
// Adaugă beforeEach guard
```
#### Task 2.5: Actualizează vite.config.js pentru auth proxy
**Fișier**: `data-entry-app/frontend/vite.config.js`
**Acțiune**: Adaugă proxy pentru auth:
```javascript
'/api/auth': {
target: 'http://localhost:8001',
changeOrigin: true,
}
```
#### Task 2.6: Testare frontend auth
```bash
cd data-entry-app/frontend
npm run dev
# Test: Accesează http://localhost:3010 → Redirect la /login
# Login cu credențiale Oracle → Redirect la /
```
---
### FAZA 3: Nomenclatoare Oracle→SQLite (6 task-uri)
#### Task 3.1: Crează modele SQLModel
**Fișier NOU**: `data-entry-app/backend/app/db/models/nomenclature.py`
- `SyncedSupplier` - furnizori sincronizați din Oracle
- `LocalSupplier` - furnizori creați local (din OCR)
- `SyncedCashRegister` - case/bănci sincronizate
#### Task 3.2: Crează Alembic migration
```bash
cd data-entry-app/backend
alembic revision --autogenerate -m "add nomenclature tables"
alembic upgrade head
```
#### Task 3.3: Crează Sync Service
**Fișier NOU**: `data-entry-app/backend/app/services/sync_service.py`
- `sync_suppliers(company_id, schema)` - sync furnizori Oracle→SQLite
- `sync_cash_registers(company_id, schema)` - sync case/bănci
- `get_schema_for_company(company_id)` - lookup schema
#### Task 3.4: Crează Nomenclature Router
**Fișier NOU**: `data-entry-app/backend/app/routers/nomenclature.py`
- `GET /suppliers/search` - căutare furnizor (SQLite + Oracle live)
- `POST /suppliers/local` - creare furnizor local
- `POST /sync/suppliers` - trigger manual sync
#### Task 3.5: Înregistrează router în main.py
```python
from app.routers import nomenclature
app.include_router(nomenclature.router, prefix="/api/nomenclature", tags=["nomenclature"])
```
#### Task 3.6: Actualizare nomenclature_service.py existent
Înlocuiește mock data cu query-uri din tabelele SQLite sincronizate
---
### FAZA 4: Integrare OCR + Supplier Search (2 task-uri)
#### Task 4.1: Actualizare ReceiptCreateView.vue
**Fișier**: `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue`
**Acțiune**: După OCR result, caută automat furnizor după CUI:
```javascript
async function handleOCRResult(ocrData) {
if (ocrData.cui) {
const result = await receiptsStore.searchSupplier(ocrData.cui);
if (result.found) {
form.partner_id = result.supplier.id;
form.partner_name = result.supplier.name;
} else {
showCreateSupplierDialog(ocrData);
}
}
}
```
#### Task 4.2: Adaugă supplier search în receiptsStore.js
**Fișier**: `data-entry-app/frontend/src/stores/receiptsStore.js`
**Acțiune**: Adaugă action `searchSupplier(fiscalCode)` și `createLocalSupplier(data)`
---
## Sumar Fișiere
### De Modificat
| Fișier | Faza |
|--------|------|
| `data-entry-app/backend/app/main.py` | 1, 3 |
| `data-entry-app/backend/app/routers/receipts.py` | 1 |
| `data-entry-app/backend/app/routers/ocr.py` | 1 |
| `data-entry-app/frontend/src/router/index.js` | 2 |
| `data-entry-app/frontend/vite.config.js` | 2 |
| `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue` | 4 |
| `data-entry-app/frontend/src/stores/receiptsStore.js` | 4 |
### De Creat (NOU)
| Fișier | Faza |
|--------|------|
| `data-entry-app/frontend/src/services/api.js` | 2 |
| `data-entry-app/frontend/src/stores/auth.js` | 2 |
| `data-entry-app/frontend/src/views/LoginView.vue` | 2 |
| `data-entry-app/backend/app/db/models/nomenclature.py` | 3 |
| `data-entry-app/backend/app/services/sync_service.py` | 3 |
| `data-entry-app/backend/app/routers/nomenclature.py` | 3 |
| `migrations/versions/xxx_nomenclature.py` | 3 |
---
## Ordine Execuție
**Faza 1 + 2 (Auth)****Faza 3 + 4 (Nomenclatoare)**
Fazele 1-2 sunt blocante pentru funcționalitatea completă, dar Faza 3-4 poate fi amânată dacă e nevoie (nomenclatoarele rămân mock data temporar).
---
## Strategie Execuție cu Agenți
### Agenți Paraleli Recomandați
**Round 1 - Auth (Backend + Frontend simultan):**
```
Agent A: Faza 1 - Task 1.1-1.5 (Backend auth)
Agent B: Faza 2 - Task 2.1-2.3 (Frontend auth files)
```
După Round 1, testare manuală auth flow.
**Round 2 - Finalizare Auth + Start Nomenclatoare:**
```
Agent A: Faza 2 - Task 2.4-2.5 (Router guards, vite config)
Agent B: Faza 3 - Task 3.1-3.2 (Modele SQLModel + migration)
```
**Round 3 - Nomenclatoare + Integration:**
```
Agent A: Faza 3 - Task 3.3-3.6 (Sync service + router)
Agent B: Faza 4 - Task 4.1-4.2 (Frontend OCR supplier)
```
### Validare După Fiecare Fază
**După Faza 1:**
```bash
curl http://localhost:8003/api/receipts/
# Expected: 401 Unauthorized
```
**După Faza 2:**
```bash
# Browser: http://localhost:3010
# Expected: Redirect to /login
# Login cu credențiale Oracle → Redirect la /
```
**După Faza 3:**
```bash
curl http://localhost:8003/api/nomenclature/suppliers/search?fiscal_code=RO12345678
# Expected: Search result sau sugestie creare local
```
**După Faza 4:**
```
# Browser: Crează bon nou → Upload poză → OCR
# Expected: Furnizor găsit automat sau dialog creare
```
---
## Context pentru Sesiune Următoare
### Fișiere Cheie de Citit
1. Acest plan: `/home/marius/.claude/plans/unified-orbiting-sonnet.md`
2. CLAUDE.md principal: `/mnt/e/proiecte/roa2web/CLAUDE.md`
3. CLAUDE.md data-entry: `/mnt/e/proiecte/roa2web/data-entry-app/CLAUDE.md`
### Comenzi Quick Start
```bash
cd /mnt/e/proiecte/roa2web
git status # Verifică branch feature/data-entry-receipts
./ssh_tunnel.sh start # SSH tunnel pentru Oracle
```
### Dependențe Servicii
- **reports-backend:8001** - NECESAR pentru auth API (login, refresh)
- **data-entry-backend:8003** - Backend principal
- **Oracle DB** - Via SSH tunnel, necesar pentru auth + nomenclatoare

View File

@@ -1,254 +0,0 @@
# Plan: OCR Inteligent cu Early Exit
> **Context Handover Document** - Plan de implementare pentru următoarea sesiune
## Obiectiv
Optimizare proces OCR - dacă PaddleOCR pe light preprocessing dă rezultate bune, să NU mai ruleze heavy preprocessing și Tesseract.
---
## Criterii Early Exit (TOATE trebuie îndeplinite)
**Continuă cu alte încercări DACĂ:**
- Confidență < **85%** SAU
- Lipsește ORICARE din câmpurile critice:
- Număr bon (`receipt_number`)
- Dată (`receipt_date`)
- Valoare totală (`amount`)
- Valoare TVA (`tva_total` sau `tva_entries`)
- Cod fiscal (`cui`)
**Early exit DOAR când:**
- Confidență >= 85% **ȘI**
- TOATE 5 câmpurile sunt extrase
---
## Flow Propus: Adaptive OCR Pipeline
```
1. PaddleOCR + Light Preprocessing (cel mai rapid, cel mai bun pentru PDF-uri clare)
Verifică: conf >= 85% AND toate 5 câmpurile extrase?
├─ DA → STOP, returnează rezultat
└─ NU → continuă la pasul 2
2. PaddleOCR + Heavy Preprocessing (pentru bonuri termice șterse)
Combină cu rezultatul anterior (merge)
Verifică: toate câmpurile extrase acum?
├─ DA → STOP
└─ NU → continuă la pasul 3
3. Tesseract + Light (fallback pentru cazuri dificile)
Combină toate rezultatele
Returnează cel mai bun rezultat combinat
```
---
## Beneficii Estimate
| Tip document | OCR calls | Timp estimat |
|--------------|-----------|--------------|
| PDF clar (Kineterra) | 1 | ~2-3 sec |
| PDF mediu | 2 | ~5 sec |
| Bon termic șters | 3 | ~8-10 sec |
**Comparație cu acum:** Totdeauna 4 calls → maxim 3, de obicei 1-2
---
## Fișier de Modificat
**`data-entry-app/backend/app/services/ocr_service.py`**
### Înlocuire completă `_process_sync()`:
```python
def _process_sync(
self,
image_path: Path,
mime_type: str
) -> Tuple[bool, str, Optional[ExtractionResult]]:
"""Synchronous processing with ADAPTIVE OCR pipeline."""
logger.info(f"[OCR Service] Starting processing: {image_path}, mime: {mime_type}")
# Load image
if mime_type == 'application/pdf':
try:
images = self.preprocessor.pdf_to_images(image_path)
if not images:
return False, "Failed to extract images from PDF", None
image = images[0]
except RuntimeError as e:
return False, str(e), None
else:
try:
image = self.preprocessor.load_image(image_path)
except ValueError as e:
return False, str(e), None
raw_texts = []
extraction = None
# ══════════════════════════════════════════════════════════════
# STEP 1: PaddleOCR + Light (fastest, best for clear PDFs)
# ══════════════════════════════════════════════════════════════
logger.info("[OCR] Step 1: PaddleOCR + Light preprocessing")
light_img = self.preprocessor.preprocess_light(image)
try:
paddle_light = self.ocr_engine._paddle_recognize(light_img)
if paddle_light and paddle_light.text:
extraction = self.extractor.extract(paddle_light.text)
extraction.ocr_engine = "paddle-light"
raw_texts.append(f"═══ PaddleOCR (light, conf: {paddle_light.confidence:.0%}) ═══\n{paddle_light.text}")
# Early exit if complete
if self._is_extraction_complete(extraction):
extraction.raw_text = "\n\n".join(raw_texts)
logger.info("[OCR] ✓ Early exit: complete extraction from paddle-light")
return True, "OCR complete (fast mode)", extraction
except Exception as e:
logger.warning(f"[OCR] PaddleOCR light failed: {e}")
extraction = ExtractionResult()
# ══════════════════════════════════════════════════════════════
# STEP 2: PaddleOCR + Heavy (for faded thermal receipts)
# ══════════════════════════════════════════════════════════════
logger.info("[OCR] Step 2: PaddleOCR + Heavy preprocessing")
heavy_img = self.preprocessor.preprocess_heavy(image)
try:
paddle_heavy = self.ocr_engine._paddle_recognize(heavy_img)
if paddle_heavy and paddle_heavy.text:
extraction_heavy = self.extractor.extract(paddle_heavy.text)
extraction_heavy.ocr_engine = "paddle-heavy"
raw_texts.append(f"═══ PaddleOCR (heavy, conf: {paddle_heavy.confidence:.0%}) ═══\n{paddle_heavy.text}")
# Merge with previous
extraction = self._merge_extractions(extraction, extraction_heavy)
if self._is_extraction_complete(extraction):
extraction.raw_text = "\n\n".join(raw_texts)
extraction.ocr_engine = "paddle-adaptive"
logger.info("[OCR] ✓ Early exit: complete extraction after paddle-heavy")
return True, "OCR complete (paddle dual)", extraction
except Exception as e:
logger.warning(f"[OCR] PaddleOCR heavy failed: {e}")
# ══════════════════════════════════════════════════════════════
# STEP 3: Tesseract fallback
# ══════════════════════════════════════════════════════════════
logger.info("[OCR] Step 3: Tesseract fallback")
try:
tesseract_result = self.ocr_engine._tesseract_recognize(light_img)
if tesseract_result and tesseract_result.text:
extraction_tess = self.extractor.extract(tesseract_result.text)
extraction_tess.ocr_engine = "tesseract"
raw_texts.append(f"═══ Tesseract (conf: {tesseract_result.confidence:.0%}) ═══\n{tesseract_result.text}")
extraction = self._merge_extractions(extraction, extraction_tess)
except Exception as e:
logger.warning(f"[OCR] Tesseract failed: {e}")
# Final result
if extraction is None:
return False, "No text detected", None
extraction.raw_text = "\n\n".join(raw_texts)
extraction.ocr_engine = "adaptive-full"
# Build result message
fields_found = []
if extraction.amount: fields_found.append("amount")
if extraction.receipt_date: fields_found.append("date")
if extraction.receipt_number: fields_found.append("number")
if extraction.cui: fields_found.append("CUI")
if extraction.tva_total or extraction.tva_entries: fields_found.append("TVA")
message = f"OCR complete (full pipeline). Found: {', '.join(fields_found) or 'no fields'}"
logger.info(f"[OCR] Final result: {message}")
return True, message, extraction
```
### Adăugare metodă `_is_extraction_complete()`:
```python
def _is_extraction_complete(self, ext: ExtractionResult, min_confidence: float = 0.85) -> bool:
"""
Check if extraction has ALL required fields to skip further processing.
Required for early exit (ALL must be true):
- Overall confidence >= 85%
- ALL 5 critical fields present: number, date, amount, TVA, CUI
"""
# Must have high confidence
if ext.overall_confidence < min_confidence:
logger.info(f"[OCR] Confidence {ext.overall_confidence:.0%} < {min_confidence:.0%} - continuing")
return False
# Check all required fields
has_number = bool(ext.receipt_number)
has_date = bool(ext.receipt_date)
has_amount = bool(ext.amount)
has_tva = bool(ext.tva_total) or bool(ext.tva_entries)
has_cui = bool(ext.cui)
missing = []
if not has_number: missing.append("number")
if not has_date: missing.append("date")
if not has_amount: missing.append("amount")
if not has_tva: missing.append("TVA")
if not has_cui: missing.append("CUI")
if missing:
logger.info(f"[OCR] Missing: {', '.join(missing)} - continuing")
return False
logger.info(f"[OCR] ✓ All 5 fields found with {ext.overall_confidence:.0%} confidence")
return True
```
---
## Cod de Șters
După implementare, poți șterge:
- `_merge_all_extractions()` - înlocuită de flow secvențial
- `_format_dual_raw_text()` - nefolosită
- Bucla `for i, processed in enumerate(variants):` - înlocuită complet
---
## Context: Rezultate OCR Kineterra
Din testele anterioare, **PaddleOCR + Light** a dat cele mai bune rezultate:
| Variantă | Conf | CUI | Adresa |
|----------|------|-----|--------|
| **PaddleOCR Light** | **83%** | **31180432** ✓ | MUN.CONSTANTA ✓ |
| PaddleOCR Heavy | 83% | 31189432 ✗ | CONSTANTN ✗ |
| Tesseract Light | 50% | 31100400 ✗ | corupt |
| Tesseract Heavy | 42% | - | corupt |
---
## Testare
După implementare, testează cu toate PDF-urile:
1. **`abonament kineterra.pdf`** - ar trebui să facă early exit la Step 1
2. **`benzina 27 octombrie.pdf`** - verifică extracție completă
3. **`igiena 11 octombrie.pdf`** - verifică extracție completă
4. **`benzina 14 august.pdf`** - verifică extracție completă
---
*Generat: 2024-12-12*
*Pentru continuare în următoarea sesiune Claude*