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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,490 @@
<template>
<div class="indicator-item" :class="statusClass">
<!-- Label (top) -->
<div class="indicator-label">{{ label }}</div>
<!-- Description (optional) -->
<div v-if="description" class="indicator-description">{{ description }}</div>
<!-- Main content: Value centered + Status icon on right -->
<div class="indicator-main">
<div class="indicator-value" :class="statusClass">
{{ formattedValue }}{{ unit ? ` ${unit}` : '' }}
</div>
<div class="indicator-status-icon" :class="statusClass">
<i :class="statusIcon"></i>
</div>
</div>
<!-- Sparkline (bottom) -->
<div
v-if="hasSparklineData"
class="sparkline-container"
ref="sparklineContainer"
>
<svg
class="sparkline-svg"
:viewBox="`0 0 ${svgWidth} ${svgHeight}`"
preserveAspectRatio="none"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<!-- Sparkline polyline -->
<polyline
:points="sparklinePoints"
fill="none"
:stroke="strokeColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="sparkline-line"
/>
<!-- Hover point indicator -->
<circle
v-if="hoveredIndex !== null"
:cx="hoveredPoint.x"
:cy="hoveredPoint.y"
r="4"
:fill="strokeColor"
class="sparkline-point"
/>
</svg>
<!-- Tooltip -->
<div
v-if="hoveredIndex !== null"
class="sparkline-tooltip"
:style="tooltipStyle"
>
<div class="tooltip-label">{{ tooltipLabel }}</div>
<div class="tooltip-value">{{ tooltipValue }}</div>
</div>
</div>
<!-- Threshold info -->
<div v-if="thresholdText" class="indicator-threshold">
{{ thresholdText }}
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
const props = defineProps({
label: {
type: String,
required: true
},
value: {
type: Number,
default: null
},
unit: {
type: String,
default: ''
},
status: {
type: String,
default: 'neutral',
validator: (val) => ['good', 'warning', 'danger', 'safe', 'grey', 'distress', 'neutral'].includes(val)
},
sparklineData: {
type: Array,
default: () => []
},
sparklineLabels: {
type: Array,
default: () => []
},
thresholds: {
type: Object,
default: () => ({})
},
decimals: {
type: Number,
default: 2
},
description: {
type: String,
default: ''
}
})
// SVG dimensions
const svgWidth = 200
const svgHeight = 40
const padding = { top: 4, right: 4, bottom: 4, left: 4 }
// Refs
const sparklineContainer = ref(null)
const hoveredIndex = ref(null)
const mouseX = ref(0)
// Computed: Format the displayed value
const formattedValue = computed(() => {
if (props.value === null || props.value === undefined) return '-'
return Number(props.value).toFixed(props.decimals)
})
// Computed: Check if we have sparkline data
const hasSparklineData = computed(() => {
return props.sparklineData && props.sparklineData.length > 0
})
// Computed: Normalize status values (Z-Score uses safe/grey/distress)
const normalizedStatus = computed(() => {
const statusMap = {
'safe': 'good',
'grey': 'warning',
'distress': 'danger'
}
return statusMap[props.status] || props.status
})
// Computed: Status CSS class
const statusClass = computed(() => {
return `status-${normalizedStatus.value}`
})
// Computed: Status icon
const statusIcon = computed(() => {
const iconMap = {
'good': 'pi pi-check-circle',
'warning': 'pi pi-exclamation-circle',
'danger': 'pi pi-times-circle',
'neutral': 'pi pi-minus-circle'
}
return iconMap[normalizedStatus.value] || iconMap['neutral']
})
// Computed: Stroke color for sparkline - uses primary color that adapts to theme
const strokeColor = computed(() => {
// Use primary-color which automatically adapts to theme (light/dark mode)
return 'var(--primary-color)'
})
// Computed: Calculate sparkline points from data
const sparklinePoints = computed(() => {
if (!hasSparklineData.value) return ''
const data = props.sparklineData.filter(v => v !== null && v !== undefined)
if (data.length < 2) return ''
const min = Math.min(...data)
const max = Math.max(...data)
const range = max - min || 1 // Avoid division by zero
const chartWidth = svgWidth - padding.left - padding.right
const chartHeight = svgHeight - padding.top - padding.bottom
const points = data.map((value, index) => {
const x = padding.left + (index / (data.length - 1)) * chartWidth
const y = padding.top + chartHeight - ((value - min) / range) * chartHeight
return `${x},${y}`
})
return points.join(' ')
})
// Computed: Calculate point positions for hover
const pointPositions = computed(() => {
if (!hasSparklineData.value) return []
const data = props.sparklineData.filter(v => v !== null && v !== undefined)
if (data.length < 2) return []
const min = Math.min(...data)
const max = Math.max(...data)
const range = max - min || 1
const chartWidth = svgWidth - padding.left - padding.right
const chartHeight = svgHeight - padding.top - padding.bottom
return data.map((value, index) => ({
x: padding.left + (index / (data.length - 1)) * chartWidth,
y: padding.top + chartHeight - ((value - min) / range) * chartHeight,
value: value,
label: props.sparklineLabels?.[index] || `Luna ${index + 1}`
}))
})
// Computed: Current hovered point
const hoveredPoint = computed(() => {
if (hoveredIndex.value === null || !pointPositions.value[hoveredIndex.value]) {
return { x: 0, y: 0 }
}
return pointPositions.value[hoveredIndex.value]
})
// Computed: Tooltip label
const tooltipLabel = computed(() => {
if (hoveredIndex.value === null) return ''
return pointPositions.value[hoveredIndex.value]?.label || ''
})
// Computed: Tooltip value
const tooltipValue = computed(() => {
if (hoveredIndex.value === null) return ''
const value = pointPositions.value[hoveredIndex.value]?.value
if (value === null || value === undefined) return '-'
return `${Number(value).toFixed(props.decimals)}${props.unit ? ` ${props.unit}` : ''}`
})
// Computed: Tooltip position style
const tooltipStyle = computed(() => {
if (hoveredIndex.value === null || !sparklineContainer.value) return {}
const containerRect = sparklineContainer.value.getBoundingClientRect()
const point = hoveredPoint.value
// Convert SVG coordinates to container coordinates
const xRatio = point.x / svgWidth
const left = xRatio * containerRect.width
// Position tooltip above the point, centered horizontally
return {
left: `${left}px`,
transform: 'translateX(-50%)'
}
})
// Computed: Threshold display text
const thresholdText = computed(() => {
if (!props.thresholds) return ''
const parts = []
if (props.thresholds.threshold_min !== null && props.thresholds.threshold_min !== undefined) {
parts.push(`Min: ${props.thresholds.threshold_min}`)
}
if (props.thresholds.threshold_max !== null && props.thresholds.threshold_max !== undefined) {
parts.push(`Max: ${props.thresholds.threshold_max}`)
}
return parts.join(' | ')
})
// Methods
const handleMouseMove = (event) => {
if (!pointPositions.value.length || !sparklineContainer.value) return
const containerRect = sparklineContainer.value.getBoundingClientRect()
const relativeX = event.clientX - containerRect.left
const xRatio = relativeX / containerRect.width
// Find the closest point
const targetX = xRatio * svgWidth
let closestIndex = 0
let closestDistance = Infinity
pointPositions.value.forEach((point, index) => {
const distance = Math.abs(point.x - targetX)
if (distance < closestDistance) {
closestDistance = distance
closestIndex = index
}
})
hoveredIndex.value = closestIndex
mouseX.value = relativeX
}
const handleMouseLeave = () => {
hoveredIndex.value = null
}
</script>
<style scoped>
/* Indicator Item Container */
.indicator-item {
background: var(--surface-ground);
border: 1px solid var(--surface-border);
border-radius: var(--radius-sm);
padding: var(--space-md);
display: flex;
flex-direction: column;
gap: var(--space-xs);
position: relative;
transition: border-color var(--transition-fast);
}
.indicator-item:hover {
border-color: var(--surface-hover);
}
/* Label (top) */
.indicator-label {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-color-secondary);
text-align: center;
}
/* Description (optional subtitle) */
.indicator-description {
font-size: var(--text-xs);
color: var(--text-color-secondary);
text-align: center;
opacity: 0.8;
line-height: 1.3;
margin-top: calc(var(--space-xs) * -1);
}
/* Main section: Value + Status Icon */
.indicator-main {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
position: relative;
}
/* Value (large, centered) */
.indicator-value {
font-size: var(--text-2xl);
font-weight: var(--font-bold);
font-family: var(--font-mono);
text-align: center;
}
/* Status Icon (right side) */
.indicator-status-icon {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
font-size: var(--text-lg);
}
/* Status Colors - use 600 variants in light mode, 400 variants in dark mode for better contrast */
.status-good {
color: var(--green-600);
}
.status-warning {
color: var(--yellow-600);
}
.status-danger {
color: var(--red-600);
}
.status-neutral {
color: var(--text-color-secondary);
}
/* Dark mode: use lighter variants (400) for better visibility on dark backgrounds */
[data-theme="dark"] .status-good {
color: var(--green-400);
}
[data-theme="dark"] .status-warning {
color: var(--yellow-400);
}
[data-theme="dark"] .status-danger {
color: var(--red-400);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .status-good {
color: var(--green-400);
}
:root:not([data-theme]) .status-warning {
color: var(--yellow-400);
}
:root:not([data-theme]) .status-danger {
color: var(--red-400);
}
}
/* Sparkline Container */
.sparkline-container {
width: 100%;
height: 40px;
position: relative;
margin-top: var(--space-xs);
}
/* SVG Sparkline */
.sparkline-svg {
width: 100%;
height: 100%;
display: block;
}
.sparkline-line {
vector-effect: non-scaling-stroke;
}
.sparkline-point {
transition: r 0.15s ease;
}
/* Tooltip */
.sparkline-tooltip {
position: absolute;
bottom: calc(100% + 4px);
background: var(--surface-overlay);
border: 1px solid var(--surface-border);
border-radius: var(--radius-sm);
padding: var(--space-xs) var(--space-sm);
box-shadow: var(--shadow-md);
z-index: var(--z-tooltip);
pointer-events: none;
white-space: nowrap;
}
.tooltip-label {
font-size: var(--text-xs);
color: var(--text-color-secondary);
margin-bottom: 2px;
}
.tooltip-value {
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--text-color);
font-family: var(--font-mono);
}
/* Threshold info */
.indicator-threshold {
font-size: var(--text-xs);
color: var(--text-color-secondary);
text-align: center;
margin-top: var(--space-xs);
}
/* Responsive */
@media (max-width: 768px) {
.indicator-item {
padding: var(--space-sm);
}
.indicator-value {
font-size: var(--text-xl);
}
.indicator-status-icon {
font-size: var(--text-base);
}
.sparkline-container {
height: 32px;
}
}
/* Dark mode adjustments */
[data-theme="dark"] .sparkline-tooltip {
background: var(--surface-card);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .sparkline-tooltip {
background: var(--surface-card);
}
}
</style>

View File

@@ -1,5 +1,5 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { ref, computed } from "vue";
import api from "@reports/services/api";
export const useDashboardStore = defineStore("dashboard", () => {
@@ -15,6 +15,13 @@ export const useDashboardStore = defineStore("dashboard", () => {
const maturityData = ref({});
const currentPeriod = ref(null);
// State pentru financial indicators (US-013)
const financialIndicators = ref({
loading: false,
error: null,
data: null,
});
// State pentru detailed data pagination
const detailedDataTotal = ref(0);
@@ -474,6 +481,62 @@ export const useDashboardStore = defineStore("dashboard", () => {
}
};
/**
* Load financial indicators from API (US-013)
* GET /api/reports/dashboard/financial-indicators
*
* @param {number} companyId - Company ID (required)
* @param {number|null} luna - Accounting month (1-12, optional)
* @param {number|null} an - Accounting year (optional)
* @returns {Promise<{success: boolean, data?: object, error?: string}>}
*/
const loadFinancialIndicators = async (companyId, luna = null, an = null) => {
financialIndicators.value.loading = true;
financialIndicators.value.error = null;
try {
const params = { company: companyId };
if (luna !== null) params.luna = luna;
if (an !== null) params.an = an;
const response = await api.get("/dashboard/financial-indicators", {
params,
});
financialIndicators.value.data = response.data;
financialIndicators.value.loading = false;
return { success: true, data: response.data };
} catch (err) {
console.error("Failed to load financial indicators:", err);
// User-friendly error message
let errorMessage = "Nu s-au putut încărca indicatorii financiari.";
if (err.response?.status === 403) {
errorMessage = "Nu aveți acces la datele acestei firme.";
} else if (err.response?.status === 400) {
errorMessage =
err.response?.data?.detail || "Parametri invalizi pentru cerere.";
} else if (err.response?.data?.detail) {
errorMessage = err.response.data.detail;
}
financialIndicators.value.error = errorMessage;
financialIndicators.value.loading = false;
financialIndicators.value.data = null;
return { success: false, error: errorMessage };
}
};
// Computed getters for financial indicators (US-013)
const lichiditate = computed(() => financialIndicators.value.data?.lichiditate || null);
const eficienta = computed(() => financialIndicators.value.data?.eficienta || null);
const risc = computed(() => financialIndicators.value.data?.risc || null);
const cashFlow = computed(() => financialIndicators.value.data?.cash_flow || null);
const dinamica = computed(() => financialIndicators.value.data?.dinamica || null);
const altmanZScore = computed(() => financialIndicators.value.data?.altman_zscore || null);
// Clear cache
const clearCache = () => {
dataCache.clear();
@@ -489,6 +552,12 @@ export const useDashboardStore = defineStore("dashboard", () => {
cashflowData.value = {};
maturityData.value = {};
currentPeriod.value = null;
// Reset financial indicators (US-013)
financialIndicators.value = {
loading: false,
error: null,
data: null,
};
clearCache();
};
@@ -516,5 +585,16 @@ export const useDashboardStore = defineStore("dashboard", () => {
// Detailed data pagination
detailedDataTotal,
// Financial indicators (US-013)
financialIndicators,
loadFinancialIndicators,
// Computed getters for financial indicators
lichiditate,
eficienta,
risc,
cashFlow,
dinamica,
altmanZScore,
};
});

View File

@@ -40,8 +40,8 @@
<!-- Secțiune Carduri Noi - Adăugare -->
<div class="metrics-cards-section" v-if="!isLoading">
<!-- Mobile: Swipeable KPI Cards Carousel -->
<!-- US-2002: 5 pages - first page is 2x2 grid with solduri, pages 2-5 are original graph cards -->
<SwipeableCards v-if="isMobile" :totalCards="5" class="mobile-kpi-carousel">
<!-- US-2002: 6 pages - first page is 2x2 grid with solduri, pages 2-5 are original graph cards, page 6 is financial indicators -->
<SwipeableCards v-if="isMobile" :totalCards="6" class="mobile-kpi-carousel">
<!-- Page 1: Grid 2x2 cu Solduri Compacte -->
<template #card-0>
<div class="solduri-grid-2x2">
@@ -124,6 +124,17 @@
:breakdown="netBalanceData?.breakdown?.furnizori"
/>
</template>
<!-- Page 6: FinancialIndicatorsCard (US-015) -->
<template #card-5>
<FinancialIndicatorsCard
:loading="dashboardStore.financialIndicators.loading"
:error="dashboardStore.financialIndicators.error"
:data="dashboardStore.financialIndicators.data"
:initial-period="periodStore.selectedPeriod"
mobile
@period-change="handleFinancialIndicatorsPeriodChange"
/>
</template>
</SwipeableCards>
<!-- US-2004: Desktop Solduri Section (sus, fără titlu) -->
@@ -151,56 +162,95 @@
/>
</div>
<!-- Desktop: Grid layout (carduri grafice originale) -->
<!-- Desktop: Grid layout (carduri grafice originale) - collapsible by default -->
<div v-if="!isMobile" class="metrics-row">
<TreasuryDualCard
:casaTotal="treasuryData?.breakdown?.casa?.total || 0"
:bancaTotal="treasuryData?.breakdown?.banca?.total || 0"
:casaItems="treasuryData?.breakdown?.casa?.items || []"
:bancaItems="treasuryData?.breakdown?.banca?.items || []"
:casaTrend="casaTrend"
:bancaTrend="bancaTrend"
:casaSparklineData="casaSparkline"
:bancaSparklineData="bancaSparkline"
:casaPreviousSparklineData="casaPreviousSparkline"
:bancaPreviousSparklineData="bancaPreviousSparkline"
:sparklineLabels="sparklineLabels"
:previousSparklineLabels="previousSparklineLabels"
/>
<CashFlowMetricCard
:inflowsValue="monthlyInflows"
:outflowsValue="monthlyOutflows"
:inflowsTrend="inflowsTrend"
:outflowsTrend="outflowsTrend"
:inflowsSparkline="inflowsSparkline"
:outflowsSparkline="outflowsSparkline"
:inflowsPreviousSparkline="inflowsPreviousSparkline"
:outflowsPreviousSparkline="outflowsPreviousSparkline"
:sparklineLabels="sparklineLabels"
:previousSparklineLabels="previousSparklineLabels"
/>
<ClientiBalanceCard
:total="netBalanceData?.clienti_total || 0"
:trend="clientiTrend"
:sparklineData="clientiSparkline"
:previousSparklineData="clientiPreviousSparkline"
:sparklineLabels="sparklineLabels"
:previousSparklineLabels="previousSparklineLabels"
:breakdown="netBalanceData?.breakdown?.clienti"
/>
<FurnizoriBalanceCard
:total="netBalanceData?.furnizori_total || 0"
:trend="furnizoriTrend"
:sparklineData="furnizoriSparkline"
:previousSparklineData="furnizoriPreviousSparkline"
:sparklineLabels="sparklineLabels"
:previousSparklineLabels="previousSparklineLabels"
:breakdown="netBalanceData?.breakdown?.furnizori"
/>
<CollapsibleCard
label="Trezorerie"
:value="totalTrezorerie"
icon="pi pi-wallet"
:value-class="totalTrezorerie >= 0 ? 'positive' : 'negative'"
>
<TreasuryDualCard
:casaTotal="treasuryData?.breakdown?.casa?.total || 0"
:bancaTotal="treasuryData?.breakdown?.banca?.total || 0"
:casaItems="treasuryData?.breakdown?.casa?.items || []"
:bancaItems="treasuryData?.breakdown?.banca?.items || []"
:casaTrend="casaTrend"
:bancaTrend="bancaTrend"
:casaSparklineData="casaSparkline"
:bancaSparklineData="bancaSparkline"
:casaPreviousSparklineData="casaPreviousSparkline"
:bancaPreviousSparklineData="bancaPreviousSparkline"
:sparklineLabels="sparklineLabels"
:previousSparklineLabels="previousSparklineLabels"
/>
</CollapsibleCard>
<CollapsibleCard
label="Cash Flow"
:value="netCashFlow"
icon="pi pi-arrows-h"
:value-class="netCashFlow >= 0 ? 'positive' : 'negative'"
>
<CashFlowMetricCard
:inflowsValue="monthlyInflows"
:outflowsValue="monthlyOutflows"
:inflowsTrend="inflowsTrend"
:outflowsTrend="outflowsTrend"
:inflowsSparkline="inflowsSparkline"
:outflowsSparkline="outflowsSparkline"
:inflowsPreviousSparkline="inflowsPreviousSparkline"
:outflowsPreviousSparkline="outflowsPreviousSparkline"
:sparklineLabels="sparklineLabels"
:previousSparklineLabels="previousSparklineLabels"
/>
</CollapsibleCard>
<CollapsibleCard
label="Clienți"
:value="netBalanceData?.clienti_total || 0"
icon="pi pi-users"
:value-class="(netBalanceData?.clienti_total || 0) >= 0 ? 'positive' : 'negative'"
>
<ClientiBalanceCard
:total="netBalanceData?.clienti_total || 0"
:trend="clientiTrend"
:sparklineData="clientiSparkline"
:previousSparklineData="clientiPreviousSparkline"
:sparklineLabels="sparklineLabels"
:previousSparklineLabels="previousSparklineLabels"
:breakdown="netBalanceData?.breakdown?.clienti"
/>
</CollapsibleCard>
<CollapsibleCard
label="Furnizori"
:value="netBalanceData?.furnizori_total || 0"
icon="pi pi-truck"
:value-class="(netBalanceData?.furnizori_total || 0) <= 0 ? 'positive' : 'negative'"
>
<FurnizoriBalanceCard
:total="netBalanceData?.furnizori_total || 0"
:trend="furnizoriTrend"
:sparklineData="furnizoriSparkline"
:previousSparklineData="furnizoriPreviousSparkline"
:sparklineLabels="sparklineLabels"
:previousSparklineLabels="previousSparklineLabels"
:breakdown="netBalanceData?.breakdown?.furnizori"
/>
</CollapsibleCard>
</div>
</div>
<!-- Financial Indicators Section - Desktop Only (US-014) -->
<div v-if="!isMobile && !isLoading" class="financial-indicators-section">
<FinancialIndicatorsCard
:loading="dashboardStore.financialIndicators.loading"
:error="dashboardStore.financialIndicators.error"
:data="dashboardStore.financialIndicators.data"
:initial-period="periodStore.selectedPeriod"
@period-change="handleFinancialIndicatorsPeriodChange"
/>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="loading-state">
<div class="loading-spinner"></div>
@@ -225,6 +275,8 @@ import ClientiBalanceCard from "@reports/components/dashboard/cards/ClientiBalan
import FurnizoriBalanceCard from "@reports/components/dashboard/cards/FurnizoriBalanceCard.vue";
import TreasuryDualCard from "@reports/components/dashboard/cards/TreasuryDualCard.vue";
import SolduriCompactCard from "@reports/components/solduri/SolduriCompactCard.vue";
import FinancialIndicatorsCard from "@reports/components/dashboard/cards/FinancialIndicatorsCard.vue";
import CollapsibleCard from "@shared/components/CollapsibleCard.vue";
// Mobile components
import SwipeableCards from "@shared/components/mobile/SwipeableCards.vue";
import MobileTopBar from "@shared/components/mobile/MobileTopBar.vue";
@@ -469,6 +521,11 @@ const tvaTotal = computed(() => {
return dashboardStore.summary?.tva_sold || 0;
});
// Net Cash Flow for CollapsibleCard header
const netCashFlow = computed(() => {
return (monthlyInflows.value || 0) - (monthlyOutflows.value || 0);
});
// Casa and Bancă specific trends and sparklines
const casaTrend = computed(() => {
// Calculate trend based on Casa proportion of treasury
@@ -608,6 +665,16 @@ const handleRefresh = async () => {
await loadDashboardData();
};
// US-014: Handle period change from FinancialIndicatorsCard dropdown
const handleFinancialIndicatorsPeriodChange = async (period) => {
if (!companyStore.selectedCompany || !period) return;
await dashboardStore.loadFinancialIndicators(
companyStore.selectedCompany.id_firma,
period.luna,
period.an,
);
};
// Computed property pentru luna curentă - folosește perioada din period selector
const currentMonthLabel = computed(() => {
// Prioritate: period selector > dashboard current period > loading
@@ -977,6 +1044,12 @@ const loadDashboardData = async () => {
loadMonthlyFlows(),
loadTreasuryBreakdown(),
loadNetBalanceBreakdown(),
// US-014: Load financial indicators for desktop card
dashboardStore.loadFinancialIndicators(
companyStore.selectedCompany.id_firma,
luna,
an,
),
]);
} catch (error) {
console.error("Failed to load dashboard data:", error);
@@ -1399,6 +1472,11 @@ onUnmounted(() => {
margin-bottom: var(--space-lg);
}
/* US-014: Financial Indicators Section - Desktop Only */
.financial-indicators-section {
margin-top: var(--space-lg);
width: 100%;
}
/* Responsive - All breakpoints consolidated */
@media (max-width: 1200px) {