feat(financial-indicators): Complete Financial Indicators Dashboard Card

Implementare completă a card-ului Indicatori Financiari în Dashboard Solduri:

Backend:
- Model FinancialIndicators cu 22+ indicatori organizați pe categorii
- Service cu calcule din VBAL (Lichiditate, Eficiență, Risc, Cash Flow, Dinamică)
- Altman Z-Score cu toate componentele (X1-X4) și valori absolute
- Profitabilitate cu ROA, ROE, Cifra Afaceri, Cheltuieli separate (operaționale/financiare)
- Caching inteligent pe company_id, luna, an

Frontend:
- FinancialIndicatorsCard.vue cu 4 indicatori principali collapsed
- Expanded view grupat pe categorii (desktop + mobile BottomSheet)
- Subindicatori pentru verificare manuală în balanță
- Traduceri complete în română
- Dark mode support complet
- Sparklines cu tooltips
- Responsive design (desktop grid + mobile carousel)

Documentație:
- PRD complet cu specificații și formule
- Descrieri cu conturi din planul contabil român (OMFP 1802/2014)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-20 17:32:48 +00:00
parent 15327687f4
commit dd4b90f922
14 changed files with 6800 additions and 237 deletions

View File

@@ -9,7 +9,10 @@ import logging
logger = logging.getLogger(__name__)
from ..models.dashboard import DashboardSummary, TrendsResponse, TrendData
from ..models.financial_indicators import FinancialIndicatorsResponse
from ..services.dashboard_service import DashboardService
from ..services.financial_indicators_service import FinancialIndicatorsService
from ..cache.decorators import cached
router = APIRouter()
@@ -427,4 +430,153 @@ async def get_current_period(
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Eroare la obținerea perioadei curente: {str(e)}")
raise HTTPException(status_code=500, detail=f"Eroare la obținerea perioadei curente: {str(e)}")
raise HTTPException(status_code=500, detail=f"Eroare la obținerea perioadei curente: {str(e)}")
@router.get(
"/financial-indicators",
response_model=FinancialIndicatorsResponse,
tags=["dashboard"]
)
async def get_financial_indicators(
request: Request,
company: int = Query(..., description="ID-ul firmei (required)"),
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
include_sparklines: bool = Query(True, description="Include date istorice pentru sparklines (12 luni)"),
current_user: CurrentUser = Depends(get_current_user)
) -> FinancialIndicatorsResponse:
"""
Returnează toți indicatorii financiari calculați pentru firma selectată.
Acest endpoint agregă datele din:
- Lichiditate: Current Ratio, Quick Ratio, Cash Ratio
- Eficiență: DSO, DPO, Cash Conversion Cycle, rate încasare/plată
- Risc: creanțe/datorii restante, raport datorii/trezorerie
- Cash Flow: flux net lunar, YTD, YoY, acoperire
- Dinamică: creștere vânzări/achiziții YoY, marjă implicită
- Altman Z-Score: scor și componente X1-X4
Parametri:
- company (required): ID-ul firmei pentru care se calculează indicatorii
- luna (optional): Luna contabilă (1-12). Dacă nu este specificată,
se folosește ultima perioadă disponibilă.
- an (optional): Anul contabil (2000-2100). Dacă nu este specificat,
se folosește anul curent.
- include_sparklines (optional, default=true): Dacă să includă date istorice
pentru vizualizarea trendului pe ultimele 12 luni (sparkline_data și sparkline_labels
în fiecare indicator)
Cache:
- TTL: 30 minute pentru indicatori curenți (cache_type='financial_indicators')
- TTL: 1 oră pentru date istorice sparkline (cache_type='financial_indicators_historical')
- Se invalidează automat la schimbarea datelor din balanță
Necesită autentificare JWT și acces la firma specificată.
"""
try:
# Verifică dacă utilizatorul are acces la firma specificată
if str(company) not in current_user.companies:
raise HTTPException(
status_code=403,
detail=f"Nu aveți acces la firma {company}"
)
# Dacă luna/an nu sunt specificate, obținem perioada curentă
# Folosim variabile tipizate explicit pentru a evita erori de tip
resolved_luna: int
resolved_an: int
if luna is None or an is None:
try:
current_period = await DashboardService.get_current_period(company)
resolved_luna = luna if luna is not None else current_period.get('luna', 12)
resolved_an = an if an is not None else current_period.get('an', 2024)
except Exception as e:
logger.warning(f"Could not get current period: {e}, using defaults")
from datetime import datetime
resolved_luna = luna if luna is not None else datetime.now().month
resolved_an = an if an is not None else datetime.now().year
else:
resolved_luna = luna
resolved_an = an
# Dacă include_sparklines este True, folosim metoda care include datele istorice
if include_sparklines:
response = await FinancialIndicatorsService.get_indicators_with_sparklines(
company, resolved_luna, resolved_an, months=12
)
logger.info(
f"Financial indicators with sparklines for company {company}, "
f"luna={resolved_luna}, an={resolved_an}: "
f"Z-Score={response.altman_zscore.zscore.value} ({response.altman_zscore.zscore.status})"
)
return response
# Dacă include_sparklines este False, calculăm doar indicatorii curenți
import asyncio
# Apelăm serviciul pentru fiecare categorie de indicatori
lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
company, resolved_luna, resolved_an
)
eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
company, resolved_luna, resolved_an
)
risc_task = FinancialIndicatorsService.calculate_risk_indicators(
company, resolved_luna, resolved_an
)
cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
company, resolved_luna, resolved_an
)
dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
company, resolved_luna, resolved_an
)
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
company, resolved_luna, resolved_an
)
# Executăm toate calculele în paralel pentru performanță
(
lichiditate,
eficienta,
risc,
cash_flow,
dinamica,
altman_zscore
) = await asyncio.gather(
lichiditate_task,
eficienta_task,
risc_task,
cash_flow_task,
dinamica_task,
altman_task
)
# Construim răspunsul
response = FinancialIndicatorsResponse(
lichiditate=lichiditate,
eficienta=eficienta,
risc=risc,
cash_flow=cash_flow,
dinamica=dinamica,
altman_zscore=altman_zscore
)
logger.info(
f"Financial indicators for company {company}, luna={resolved_luna}, an={resolved_an}: "
f"Z-Score={altman_zscore.zscore.value} ({altman_zscore.zscore.status})"
)
return response
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Eroare la obținerea indicatorilor financiari: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Eroare la obținerea indicatorilor financiari: {str(e)}"
)