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:
File diff suppressed because it is too large
Load Diff
490
src/modules/reports/components/dashboard/cards/IndicatorItem.vue
Normal file
490
src/modules/reports/components/dashboard/cards/IndicatorItem.vue
Normal 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>
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user