feat(dashboard): Complete dashboard desktop cleanup and improvements

User Stories Completed:
- US-001: Eliminare SolduriCompactCard de pe Desktop
- US-002: Eliminare Icoane din Header-ul CollapsibleCard
- US-003: Reorganizare TreasuryDualCard - Text Înainte de Grafice
- US-004: Reorganizare ClientiBalanceCard - Text Înainte de Grafice
- US-005: Reorganizare FurnizoriBalanceCard - Text Înainte de Grafice
- US-006: Grafice Colapsabile în TreasuryDualCard
- US-007: Grafice Colapsabile în ClientiBalanceCard
- US-008: Grafice Colapsabile în FurnizoriBalanceCard
- US-009: Grafice Colapsabile în CashFlowMetricCard

Additional Improvements:
- Add cache metadata display (CacheFooter component) for all dashboard cards
- Add @cached decorators to get_monthly_flows and get_indicators_with_sparklines
- Fix financial indicators calculations and sparkline sync
- Add state reset on company change to prevent stale data
- New shared components: CacheFooter.vue, authRedirect.js
- Enhanced FinancialIndicatorsCard with sparklines and period selection

Squashed from branch: ralph/dashboard-desktop-cleanup (11 commits)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-22 07:27:27 +00:00
parent 69683b2d65
commit 1b9ebf1d8f
23 changed files with 4034 additions and 1396 deletions

View File

@@ -89,6 +89,18 @@ class BalanceSheetAggregates(BaseModel):
default=Decimal('0'),
description="Cheltuieli financiare (Clasa 66 - dobânzi, diferențe curs)"
)
capital_social_strict: Decimal = Field(
default=Decimal('0'),
description="Capital Social strict (doar contul 101 - subscris și vărsat)"
)
cifra_afaceri: Decimal = Field(
default=Decimal('0'),
description="Cifra de afaceri (doar 70x FĂRĂ TVA, fără 71x-75x)"
)
achizitii_stocuri: Decimal = Field(
default=Decimal('0'),
description="Achiziții stocuri YTD (Clasa 3 TOTDEB, FĂRĂ TVA)"
)
# Computed properties pentru calculele ulterioare
@property
@@ -220,6 +232,12 @@ class LiquidityIndicators(BaseModel):
lichiditate_vedere: Cash Ratio = disponibilități / datorii_curente
- Măsoară capacitatea de plată imediată doar din numerar
- Good: >= 0.2, Warning: 0.1-0.2, Danger: < 0.1
Sub-indicatori pentru verificare:
- active_curente: Active Curente = Stocuri + Creanțe + Disponibilități
- disponibilitati: Disponibilități (bancă + casă)
- creante: Creanțe comerciale
- datorii_curente: Datorii pe termen scurt
"""
lichiditate_curenta: IndicatorResult = Field(
description="Current Ratio = active_curente / datorii_curente"
@@ -230,6 +248,23 @@ class LiquidityIndicators(BaseModel):
lichiditate_vedere: IndicatorResult = Field(
description="Cash Ratio = disponibilități / datorii_curente"
)
# Sub-indicatori pentru verificare manuală în balanță
active_curente: Optional[IndicatorResult] = Field(
default=None,
description="Active Curente = Stocuri + Creanțe + Disponibilități (RON)"
)
disponibilitati: Optional[IndicatorResult] = Field(
default=None,
description="Disponibilități = Bancă (512x) + Casă (531x) (RON)"
)
creante: Optional[IndicatorResult] = Field(
default=None,
description="Creanțe comerciale = Clienți (411x) + Debitori (461x) (RON)"
)
datorii_curente: Optional[IndicatorResult] = Field(
default=None,
description="Datorii pe termen scurt = Furnizori + TVA + Salarii etc. (RON)"
)
class Config:
json_schema_extra = {
@@ -301,6 +336,31 @@ class EfficiencyIndicators(BaseModel):
rata_plata: IndicatorResult = Field(
description="Rata de plată = plati / achizitii * 100"
)
# Sub-indicatori pentru verificare manuală
sold_clienti: Optional[IndicatorResult] = Field(
default=None,
description="Sold Clienți la final de lună (RON)"
)
facturari_lunare: Optional[IndicatorResult] = Field(
default=None,
description="Total facturări în luna curentă (RON)"
)
sold_furnizori: Optional[IndicatorResult] = Field(
default=None,
description="Sold Furnizori la final de lună (RON)"
)
achizitii_lunare: Optional[IndicatorResult] = Field(
default=None,
description="Total achiziții în luna curentă (RON)"
)
incasari_luna: Optional[IndicatorResult] = Field(
default=None,
description="Încasări efectuate în luna curentă (RON)"
)
plati_luna: Optional[IndicatorResult] = Field(
default=None,
description="Plăți efectuate în luna curentă (RON)"
)
class Config:
json_schema_extra = {
@@ -379,6 +439,31 @@ class RiskIndicators(BaseModel):
raport_datorii_trezorerie: IndicatorResult = Field(
description="Raport datorii/trezorerie = furnizori_sold_total / trezorerie"
)
# Sub-indicatori pentru verificare manuală
total_clienti: Optional[IndicatorResult] = Field(
default=None,
description="Sold total clienți (411x) (RON)"
)
clienti_restanti: Optional[IndicatorResult] = Field(
default=None,
description="Sold clienți cu facturi restante (RON)"
)
clienti_90plus: Optional[IndicatorResult] = Field(
default=None,
description="Sold clienți cu facturi >90 zile restant (RON)"
)
total_furnizori: Optional[IndicatorResult] = Field(
default=None,
description="Sold total furnizori (401x) (RON)"
)
furnizori_restanti: Optional[IndicatorResult] = Field(
default=None,
description="Sold furnizori cu facturi restante (RON)"
)
trezorerie: Optional[IndicatorResult] = Field(
default=None,
description="Disponibilități (512x + 531x) (RON)"
)
class Config:
json_schema_extra = {
@@ -447,6 +532,23 @@ class CashFlowIndicators(BaseModel):
acoperire_cash_flow: IndicatorResult = Field(
description="Acoperire datorii = cash_flow_ytd / datorii_restante"
)
# Sub-indicatori pentru verificare manuală
incasari_luna: Optional[IndicatorResult] = Field(
default=None,
description="Încasări luna curentă (RON)"
)
plati_luna: Optional[IndicatorResult] = Field(
default=None,
description="Plăți luna curentă (RON)"
)
cf_an_precedent: Optional[IndicatorResult] = Field(
default=None,
description="Cash Flow aceeași perioadă an precedent (RON)"
)
datorii_restante: Optional[IndicatorResult] = Field(
default=None,
description="Total datorii cu scadență depășită (RON)"
)
class Config:
json_schema_extra = {
@@ -488,19 +590,24 @@ class DynamicsIndicators(BaseModel):
Arată dacă afacerea crește sau scade prin comparație YoY (Year-over-Year).
SURSE DE DATE (toate FĂRĂ TVA):
- Vânzări: Cifra de Afaceri din VBAL (Clasa 7 - conturile 70x)
- Achiziții: Registru Jurnal ACT (stocuri 3x=4x + cheltuieli directe 6x=4x)
Attributes:
crestere_vanzari_yoy: Creșterea procentuală a vânzărilor față de anul anterior
- Formula: (facturari_curent - facturari_anterior) / facturari_anterior * 100
- Măsoară dinamica vânzărilor - creștere sau scădere
crestere_vanzari_yoy: Creșterea procentuală a Cifrei de Afaceri față de anul anterior
- Formula: (CA_curent - CA_anterior) / CA_anterior * 100
- Sursa: VBAL TOTCRED(70x) - TOTDEB(709)
- Good: > 5%, Warning: 0-5%, Danger: < 0%
crestere_achizitii_yoy: Creșterea procentuală a achizițiilor față de anul anterior
crestere_achizitii_yoy: Creșterea procentuală a achizițiilor totale față de anul anterior
- Formula: (achizitii_curent - achizitii_anterior) / achizitii_anterior * 100
- Sursa: ACT (stocuri 3x=4x + cheltuieli directe 6x=4x, fără TVA)
- Creșterea achizițiilor poate indica expansiune sau costuri mai mari
marja_implicita: Marja implicită din diferența facturări - achiziții
- Formula: (facturari - achizitii) / facturari * 100
- Arată ce procent din vânzări rămâne după achiziții
marja_implicita: Marja implicită din diferența CA - achiziții totale
- Formula: (CA - achizitii_totale) / CA * 100
- Arată ce procent din Cifra de Afaceri rămâne după achiziții
- Good: > 20%, Warning: 10-20%, Danger: < 10%
"""
crestere_vanzari_yoy: IndicatorResult = Field(
@@ -512,6 +619,23 @@ class DynamicsIndicators(BaseModel):
marja_implicita: IndicatorResult = Field(
description="Marja implicită = (facturari - achizitii) / facturari * 100"
)
# Sub-indicatori pentru verificare manuală
vanzari_an_curent: Optional[IndicatorResult] = Field(
default=None,
description="Total vânzări YTD an curent (RON)"
)
vanzari_an_precedent: Optional[IndicatorResult] = Field(
default=None,
description="Total vânzări YTD an precedent (RON)"
)
achizitii_an_curent: Optional[IndicatorResult] = Field(
default=None,
description="Total achiziții YTD an curent (stocuri + cheltuieli directe, fără TVA)"
)
achizitii_an_precedent: Optional[IndicatorResult] = Field(
default=None,
description="Total achiziții YTD an precedent (stocuri + cheltuieli directe, fără TVA)"
)
class Config:
json_schema_extra = {
@@ -713,6 +837,11 @@ class ProfitabilityIndicators(BaseModel):
profit_brut: IndicatorResult = Field(
description="Profit brut (EBIT) = Venituri - Cheltuieli operaționale"
)
# Sub-indicator pentru verificare EBIT
venituri: Optional[IndicatorResult] = Field(
default=None,
description="Total venituri (Clasa 7) - pentru verificare calcul EBIT (RON)"
)
marja_profit_brut: IndicatorResult = Field(
description="Marja de profit = Profit brut / Cifra afaceri * 100"
)
@@ -776,6 +905,91 @@ class ProfitabilityIndicators(BaseModel):
}
class SolvabilityIndicators(BaseModel):
"""
Indicatori de solvabilitate pentru evaluarea capacității firmei
de a-și acoperi datoriile pe termen lung.
Conform articolului UniversulFiscal despre Activul Net Contabil (ANC):
- ANC = Total Active - Total Datorii
- Implicații legale (din 1 ianuarie 2026): Sub 50% din capital social
→ restricții dividende, restituire împrumuturi, acordare împrumuturi noi
Attributes:
activ_net_contabil: Activul Net Contabil (ANC) în RON
- Formula: Total Active - Total Datorii
- Good: > 0 (firma are avere netă pozitivă)
- Danger: <= 0 (firma este insolvabilă tehnic)
rata_anc_capital: Rata ANC / Capital Social în %
- Formula: (ANC / Capital Social) × 100
- Good: >= 100% (ANC acoperă integral capitalul social)
- Warning: 50-100% (ANC sub capital, dar peste pragul legal)
- Danger: < 50% (sub pragul legal - restricții aplicabile)
total_active: Total Active - valoare de verificare
total_datorii: Total Datorii - valoare de verificare
capital_social: Capital Social - valoare de verificare
"""
activ_net_contabil: IndicatorResult = Field(
description="Activ Net Contabil = Total Active - Total Datorii (RON)"
)
rata_anc_capital: IndicatorResult = Field(
description="Rata ANC/Capital = (ANC / Capital Social) × 100 (%)"
)
# Valori de bază pentru verificare manuală în balanță
total_active: IndicatorResult = Field(
description="Total Active - bază calcul ANC"
)
total_datorii: IndicatorResult = Field(
description="Total Datorii - bază calcul ANC"
)
capital_social: IndicatorResult = Field(
description="Capital Social - bază calcul Rata ANC"
)
class Config:
json_schema_extra = {
"example": {
"activ_net_contabil": {
"value": 850000.00,
"status": "good",
"threshold_min": 0,
"threshold_max": None,
"message": "Activ net pozitiv - firma solvabilă"
},
"rata_anc_capital": {
"value": 125.5,
"status": "good",
"threshold_min": 100.0,
"threshold_max": None,
"message": "ANC peste capitalul social - situație sănătoasă"
},
"total_active": {
"value": 1800000.00,
"status": "good",
"threshold_min": None,
"threshold_max": None,
"message": "Active Imobilizate + Active Curente"
},
"total_datorii": {
"value": 950000.00,
"status": "good",
"threshold_min": None,
"threshold_max": None,
"message": "Datorii Curente + Datorii Termen Lung"
},
"capital_social": {
"value": 680000.00,
"status": "good",
"threshold_min": None,
"threshold_max": None,
"message": "Capital subscris și vărsat"
}
}
}
class FinancialIndicatorsResponse(BaseModel):
"""
Răspunsul complet al endpoint-ului /api/reports/dashboard/financial-indicators.
@@ -791,6 +1005,7 @@ class FinancialIndicatorsResponse(BaseModel):
dinamica: Indicatori de dinamică (creștere vânzări/achiziții YoY, marjă)
altman_zscore: Scorul Altman Z-Score și componentele X1-X4
profitabilitate: Indicatori de profitabilitate (ROA, ROE, marjă profit)
solvabilitate: Indicatori de solvabilitate (ANC, rata ANC/Capital Social)
Usage:
GET /api/reports/dashboard/financial-indicators?company=123&luna=12&an=2024
@@ -803,7 +1018,8 @@ class FinancialIndicatorsResponse(BaseModel):
"cash_flow": { ... },
"dinamica": { ... },
"altman_zscore": { ... },
"profitabilitate": { ... }
"profitabilitate": { ... },
"solvabilitate": { ... }
}
"""
lichiditate: LiquidityIndicators = Field(
@@ -827,6 +1043,9 @@ class FinancialIndicatorsResponse(BaseModel):
profitabilitate: ProfitabilityIndicators = Field(
description="Indicatori de profitabilitate: ROA, ROE, marja de profit"
)
solvabilitate: SolvabilityIndicators = Field(
description="Indicatori de solvabilitate: ANC, rata ANC/Capital Social"
)
class Config:
json_schema_extra = {
@@ -868,6 +1087,13 @@ class FinancialIndicatorsResponse(BaseModel):
"x4": {"value": 1.80, "status": "good"},
"working_capital": 450000.00,
"total_assets": 1800000.00
},
"solvabilitate": {
"activ_net_contabil": {"value": 850000.00, "status": "good", "threshold_min": 0},
"rata_anc_capital": {"value": 125.5, "status": "good", "threshold_min": 100.0},
"total_active": {"value": 1800000.00, "status": "good"},
"total_datorii": {"value": 950000.00, "status": "good"},
"capital_social": {"value": 680000.00, "status": "good"}
}
}
}

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from typing import Optional
# import sys # Removed - no longer needed
import os
from shared.auth.dependencies import get_current_user
@@ -290,6 +289,7 @@ async def get_maturity_analysis(
@router.get("/monthly-flows")
async def get_monthly_flows(
request: Request,
company: int = Query(..., description="ID-ul firmei"),
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"),
@@ -301,15 +301,31 @@ async def get_monthly_flows(
- Necesită autentificare JWT
- Returnează date pentru analiza fluxurilor lunare
- luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă)
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
"""
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}")
result = await DashboardService.get_monthly_flows(company, luna=luna, an=an)
return result
# Apelăm serviciul cu request pentru cache metadata
result = await DashboardService.get_monthly_flows(company, luna=luna, an=an, request=request)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
# Add cache metadata if requested (for Telegram Bot / Dashboard)
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
if include_metadata:
cache_hit = getattr(request.state, 'cache_hit', False)
response_time = getattr(request.state, 'response_time_ms', 0)
cache_source = getattr(request.state, 'cache_source', None)
result_dict['cache_hit'] = cache_hit
result_dict['response_time_ms'] = response_time
result_dict['cache_source'] = cache_source
return result_dict
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
@@ -435,7 +451,6 @@ async def get_current_period(
@router.get(
"/financial-indicators",
response_model=FinancialIndicatorsResponse,
tags=["dashboard"]
)
async def get_financial_indicators(
@@ -445,7 +460,7 @@ async def get_financial_indicators(
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ă.
@@ -504,15 +519,25 @@ async def get_financial_indicators(
# 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
company, resolved_luna, resolved_an, months=12, request=request
)
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})"
f"Z-Score={response.altman_zscore.zscore.value} ({response.altman_zscore.zscore.status}), "
f"cache_hit={getattr(request.state, 'cache_hit', False)}, "
f"response_time={getattr(request.state, 'response_time_ms', 0):.1f}ms"
)
# Add cache metadata if requested (for Telegram Bot / Dashboard)
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
if include_metadata:
result_dict = response.dict() if hasattr(response, 'dict') else response
result_dict['cache_hit'] = getattr(request.state, 'cache_hit', False)
result_dict['response_time_ms'] = getattr(request.state, 'response_time_ms', 0)
result_dict['cache_source'] = getattr(request.state, 'cache_source', None)
return result_dict
return response
# Dacă include_sparklines este False, calculăm doar indicatorii curenți
@@ -537,6 +562,12 @@ async def get_financial_indicators(
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
company, resolved_luna, resolved_an
)
profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
company, resolved_luna, resolved_an
)
solvabilitate_task = FinancialIndicatorsService.calculate_solvability_indicators(
company, resolved_luna, resolved_an
)
# Executăm toate calculele în paralel pentru performanță
(
@@ -545,14 +576,18 @@ async def get_financial_indicators(
risc,
cash_flow,
dinamica,
altman_zscore
altman_zscore,
profitabilitate,
solvabilitate
) = await asyncio.gather(
lichiditate_task,
eficienta_task,
risc_task,
cash_flow_task,
dinamica_task,
altman_task
altman_task,
profitabilitate_task,
solvabilitate_task
)
# Construim răspunsul
@@ -562,7 +597,9 @@ async def get_financial_indicators(
risc=risc,
cash_flow=cash_flow,
dinamica=dinamica,
altman_zscore=altman_zscore
altman_zscore=altman_zscore,
profitabilitate=profitabilitate,
solvabilitate=solvabilitate
)
logger.info(
@@ -570,6 +607,14 @@ async def get_financial_indicators(
f"Z-Score={altman_zscore.zscore.value} ({altman_zscore.zscore.status})"
)
# Add cache metadata if requested (for Telegram Bot / Dashboard)
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
if include_metadata:
result_dict = response.dict() if hasattr(response, 'dict') else response
result_dict['cache_hit'] = getattr(request.state, 'cache_hit', False)
result_dict['response_time_ms'] = getattr(request.state, 'response_time_ms', 0)
result_dict['cache_source'] = getattr(request.state, 'cache_source', None)
return result_dict
return response
except ValueError as e:

View File

@@ -1546,14 +1546,16 @@ class DashboardService:
raise
@staticmethod
async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None) -> Dict[str, Any]:
@cached(cache_type='monthly_flows', key_params=['company', 'luna', 'an'])
async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
"""
Obține fluxurile lunare de intrare și ieșire pentru luna curentă
Obține fluxurile lunare de intrare și ieșire pentru luna curentă (CACHED 30 min)
Args:
company: ID-ul firmei
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
request: Request object pentru cache metadata
"""
try:
async with oracle_pool.get_connection() as connection: