feat(reports,mobile): add FinancialIndicators page and refactor dashboard layout
- Add dedicated FinancialIndicatorsView with its own route - Refactor FinancialIndicatorsCard: simplified collapsible card (no period picker) - Dashboard mobile: reduce swipeable pages from 2 to 1, embed indicators in card-0 - Dashboard desktop: wrap indicators in CollapsibleCard - Update MobileBottomNav defaults: reports-centric nav (Facturi/Banca/Casa/Indicatori) - BankView/CashView: date format DD/MM/YY, remove amount color classes, CSS tweaks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -156,7 +156,7 @@
|
||||
@media (max-width: 768px) {
|
||||
.app-container,
|
||||
.page-container {
|
||||
padding: var(--space-md);
|
||||
padding: var(--space-xs);
|
||||
}
|
||||
|
||||
/* US-705: Keep main-content padding minimal - app-container handles content spacing */
|
||||
@@ -171,7 +171,7 @@
|
||||
|
||||
.dashboard-container {
|
||||
gap: var(--space-lg);
|
||||
padding: var(--space-md);
|
||||
padding: var(--space-xs);
|
||||
}
|
||||
|
||||
.card-container {
|
||||
@@ -195,7 +195,7 @@
|
||||
.app-container,
|
||||
.page-container,
|
||||
.dashboard-container {
|
||||
padding: var(--space-sm);
|
||||
padding: var(--space-xs);
|
||||
}
|
||||
|
||||
/* US-705: Keep main-content padding minimal on small screens too */
|
||||
|
||||
19
src/assets/css/vendor/primevue-overrides.css
vendored
19
src/assets/css/vendor/primevue-overrides.css
vendored
@@ -956,3 +956,22 @@
|
||||
color: var(--text-color-secondary, #9ca3af) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Mobile Layout: Flat Cards (no borders/shadows) ===== */
|
||||
/* Applied when parent has .mobile-layout class (isMobile === true).
|
||||
Removes PrimeVue Card visual frame so content flows as flat list,
|
||||
consistent with Material Design 3 edge-to-edge pattern. */
|
||||
.mobile-layout .p-card {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.mobile-layout .p-card .p-card-body {
|
||||
padding: var(--space-sm) !important;
|
||||
}
|
||||
|
||||
.mobile-layout .p-card .p-card-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,92 +1,77 @@
|
||||
<template>
|
||||
<div class="indicator-item" :class="statusClass">
|
||||
<!-- Label (top) cu toggle pentru descriere -->
|
||||
<div class="indicator-label">
|
||||
{{ label }}
|
||||
<i
|
||||
v-if="description"
|
||||
class="pi desc-toggle"
|
||||
:class="descExpanded ? 'pi-chevron-up' : 'pi-chevron-down'"
|
||||
@click.stop="toggleDescription"
|
||||
title="Toggle descriere"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<!-- Description (collapsible) -->
|
||||
<div v-if="description && descExpanded" class="indicator-description slide-down">
|
||||
{{ 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">
|
||||
<div class="indicator-item-flat" @click="expanded = !expanded">
|
||||
<!-- Collapsed flat row: Label + Value + Status icon -->
|
||||
<div class="indicator-row-flat">
|
||||
<span class="indicator-label">{{ label }}</span>
|
||||
<span class="indicator-value">{{ formattedValue }}{{ unit ? ` ${unit}` : '' }}</span>
|
||||
<span class="indicator-status-icon" :class="statusClass">
|
||||
<i :class="statusIcon"></i>
|
||||
</div>
|
||||
</span>
|
||||
</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"
|
||||
/>
|
||||
<!-- Expanded content (tap to reveal) -->
|
||||
<div v-if="expanded" class="indicator-expand-flat slide-down">
|
||||
<!-- Description -->
|
||||
<div v-if="description" class="indicator-description">
|
||||
{{ description }}
|
||||
</div>
|
||||
|
||||
<!-- Hover point indicator -->
|
||||
<circle
|
||||
v-if="hoveredIndex !== null"
|
||||
:cx="hoveredPoint.x"
|
||||
:cy="hoveredPoint.y"
|
||||
r="4"
|
||||
:fill="strokeColor"
|
||||
class="sparkline-point"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<!-- Sparkline -->
|
||||
<div
|
||||
v-if="hoveredIndex !== null"
|
||||
class="sparkline-tooltip"
|
||||
:style="tooltipStyle"
|
||||
v-if="hasSparklineData"
|
||||
class="sparkline-container"
|
||||
ref="sparklineContainer"
|
||||
>
|
||||
<div class="tooltip-label">{{ tooltipLabel }}</div>
|
||||
<div class="tooltip-value">{{ tooltipValue }}</div>
|
||||
<svg
|
||||
class="sparkline-svg"
|
||||
:viewBox="`0 0 ${svgWidth} ${svgHeight}`"
|
||||
preserveAspectRatio="none"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<polyline
|
||||
:points="sparklinePoints"
|
||||
fill="none"
|
||||
:stroke="strokeColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="sparkline-line"
|
||||
/>
|
||||
<circle
|
||||
v-if="hoveredIndex !== null"
|
||||
:cx="hoveredPoint.x"
|
||||
:cy="hoveredPoint.y"
|
||||
r="4"
|
||||
:fill="strokeColor"
|
||||
class="sparkline-point"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
v-if="hoveredIndex !== null"
|
||||
class="sparkline-tooltip"
|
||||
:style="tooltipStyle"
|
||||
>
|
||||
<div class="tooltip-label">{{ tooltipLabel }}</div>
|
||||
<div class="tooltip-value">{{ tooltipValue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- YoY Trend indicator (shows variation from first to last sparkline value) -->
|
||||
<div
|
||||
v-if="hasSparklineData && trendInfo.text !== '-'"
|
||||
class="yoy-trend"
|
||||
:class="trendInfo.class"
|
||||
>
|
||||
<i :class="trendInfo.icon"></i>
|
||||
<span class="trend-value">{{ trendInfo.text }}</span>
|
||||
<span class="trend-label">vs 12 luni</span>
|
||||
</div>
|
||||
<!-- YoY Trend -->
|
||||
<div
|
||||
v-if="hasSparklineData && trendInfo.text !== '-'"
|
||||
class="yoy-trend"
|
||||
:class="trendInfo.class"
|
||||
>
|
||||
<i :class="trendInfo.icon"></i>
|
||||
<span class="trend-value">{{ trendInfo.text }}</span>
|
||||
<span class="trend-label">vs 12 luni</span>
|
||||
</div>
|
||||
|
||||
<!-- Threshold info -->
|
||||
<div v-if="thresholdText" class="indicator-threshold">
|
||||
{{ thresholdText }}
|
||||
<!-- Threshold info -->
|
||||
<div v-if="thresholdText" class="indicator-threshold">
|
||||
{{ thresholdText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -94,12 +79,8 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Description toggle state
|
||||
const descExpanded = ref(false)
|
||||
|
||||
const toggleDescription = () => {
|
||||
descExpanded.value = !descExpanded.value
|
||||
}
|
||||
// Expand/collapse state (tap to expand)
|
||||
const expanded = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
@@ -151,10 +132,13 @@ const sparklineContainer = ref(null)
|
||||
const hoveredIndex = ref(null)
|
||||
const mouseX = ref(0)
|
||||
|
||||
// Computed: Format the displayed value
|
||||
// Computed: Format the displayed value (locale-aware with thousands separators)
|
||||
const formattedValue = computed(() => {
|
||||
if (props.value === null || props.value === undefined) return '-'
|
||||
return Number(props.value).toFixed(props.decimals)
|
||||
return Number(props.value).toLocaleString('ro-RO', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: props.decimals
|
||||
})
|
||||
})
|
||||
|
||||
// Computed: Check if we have sparkline data
|
||||
@@ -258,7 +242,7 @@ 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}` : ''}`
|
||||
return `${Number(value).toLocaleString('ro-RO', { minimumFractionDigits: 0, maximumFractionDigits: props.decimals })}${props.unit ? ` ${props.unit}` : ''}`
|
||||
})
|
||||
|
||||
// Computed: Tooltip position style
|
||||
@@ -357,60 +341,51 @@ const handleMouseLeave = () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Indicator Item Container */
|
||||
.indicator-item {
|
||||
background: transparent;
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--space-md);
|
||||
/* Indicator Item - Flat row design */
|
||||
.indicator-item-flat {
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
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) with toggle */
|
||||
.indicator-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-color-secondary);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
/* Description toggle icon */
|
||||
.desc-toggle {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
opacity: 0.6;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.desc-toggle:hover {
|
||||
color: var(--primary-color);
|
||||
opacity: 1;
|
||||
.indicator-item-flat:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.indicator-item-flat:active {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* Description (collapsible) */
|
||||
/* Collapsed flat row */
|
||||
.indicator-row-flat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-sm) 0;
|
||||
min-height: 48px;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
/* Label (left, flex-grow) */
|
||||
.indicator-label {
|
||||
flex: 1;
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Expanded content area */
|
||||
.indicator-expand-flat {
|
||||
padding: 0 0 var(--space-sm);
|
||||
}
|
||||
|
||||
/* Description */
|
||||
.indicator-description {
|
||||
font-size: var(--text-xs);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-color-secondary);
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
line-height: 1.3;
|
||||
padding: var(--space-xs);
|
||||
line-height: 1.4;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--surface-hover);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: var(--space-xs);
|
||||
@@ -432,30 +407,19 @@ const handleMouseLeave = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* Main section: Value + Status Icon */
|
||||
.indicator-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Value (large, centered) */
|
||||
/* Value (inline, right of label) */
|
||||
.indicator-value {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-bold);
|
||||
font-family: var(--font-mono);
|
||||
text-align: center;
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-semibold);
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Status Icon (right side) */
|
||||
/* Status Icon (rightmost) - only element that gets status color */
|
||||
.indicator-status-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: var(--text-lg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Status Colors - use 600 variants in light mode, 400 variants in dark mode for better contrast */
|
||||
@@ -620,7 +584,7 @@ const handleMouseLeave = () => {
|
||||
|
||||
/* Threshold info */
|
||||
.indicator-threshold {
|
||||
font-size: var(--text-xs);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-color-secondary);
|
||||
text-align: center;
|
||||
margin-top: var(--space-xs);
|
||||
@@ -628,18 +592,6 @@ const handleMouseLeave = () => {
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -249,13 +249,11 @@
|
||||
<span class="compact-partner">{{ reg.nume || 'Fără partener' }}</span>
|
||||
<span
|
||||
class="compact-amount"
|
||||
:class="reg.incasari > 0 ? 'positive' : (reg.plati > 0 ? 'negative' : '')"
|
||||
>
|
||||
<template v-if="reg.incasari > 0">+{{ formatNumber(reg.incasari) }}</template>
|
||||
<template v-else-if="reg.plati > 0">-{{ formatNumber(reg.plati) }}</template>
|
||||
<template v-else>0</template>
|
||||
</span>
|
||||
<i :class="expandedMeta.has(index) ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="meta-chevron"></i>
|
||||
</div>
|
||||
<div v-if="expandedMeta.has(index)" class="compact-meta">
|
||||
<span v-if="reg.nume_cont_bancar" class="meta-item">{{ reg.nume_cont_bancar }}</span>
|
||||
@@ -552,11 +550,12 @@ const formatDate = (dateString) => {
|
||||
return format(new Date(dateString), "dd.MM.yyyy");
|
||||
};
|
||||
|
||||
// Short date format for mobile cards (DD/MM)
|
||||
// Short date format for mobile cards (DD/MM/YY)
|
||||
const formatDateShort = (dateString) => {
|
||||
if (!dateString) return "";
|
||||
const date = new Date(dateString);
|
||||
return `${String(date.getDate()).padStart(2, "0")}/${String(date.getMonth() + 1).padStart(2, "0")}`;
|
||||
const year = String(date.getFullYear()).slice(-2);
|
||||
return `${String(date.getDate()).padStart(2, "0")}/${String(date.getMonth() + 1).padStart(2, "0")}/${year}`;
|
||||
};
|
||||
|
||||
// Compact number format
|
||||
@@ -971,8 +970,8 @@ watch(
|
||||
.mobile-layout .register-view {
|
||||
padding-top: calc(56px + var(--space-md));
|
||||
padding-bottom: calc(56px + var(--space-md));
|
||||
padding-left: var(--space-md);
|
||||
padding-right: var(--space-md);
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Card Spacing */
|
||||
@@ -1005,10 +1004,9 @@ watch(
|
||||
|
||||
.mobile-totals-bar {
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--surface-border);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding: var(--space-sm) var(--space-xs);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.mobile-totals-grid {
|
||||
@@ -1054,7 +1052,7 @@ watch(
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
padding: var(--space-sm) 0;
|
||||
gap: var(--space-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1064,9 +1062,9 @@ watch(
|
||||
}
|
||||
|
||||
.compact-date {
|
||||
font-size: var(--text-xs);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-color-secondary);
|
||||
min-width: 40px;
|
||||
min-width: 54px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1083,20 +1081,7 @@ watch(
|
||||
font-weight: var(--font-semibold);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: var(--text-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.compact-amount.positive {
|
||||
color: var(--green-600);
|
||||
}
|
||||
|
||||
.compact-amount.negative {
|
||||
color: var(--red-600);
|
||||
}
|
||||
|
||||
.meta-chevron {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1104,9 +1089,9 @@ watch(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-md) var(--space-sm) calc(40px + var(--space-md) + var(--space-sm));
|
||||
padding: var(--space-xs) var(--space-xs) var(--space-sm) calc(54px + var(--space-sm));
|
||||
background: var(--surface-hover);
|
||||
font-size: var(--text-xs);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
@@ -1156,24 +1141,8 @@ watch(
|
||||
Dark Mode Support
|
||||
================================================ */
|
||||
|
||||
[data-theme="dark"] .compact-amount.positive {
|
||||
color: var(--green-400);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .compact-amount.negative {
|
||||
color: var(--red-400);
|
||||
}
|
||||
|
||||
/* Auto dark mode */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .compact-amount.positive {
|
||||
color: var(--green-400);
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .compact-amount.negative {
|
||||
color: var(--red-400);
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .numeric-value.negative {
|
||||
color: var(--red-400);
|
||||
}
|
||||
@@ -1189,7 +1158,7 @@ watch(
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.register-view {
|
||||
padding: var(--space-md);
|
||||
padding: var(--space-xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -249,13 +249,11 @@
|
||||
<span class="compact-partner">{{ reg.nume || 'Fără partener' }}</span>
|
||||
<span
|
||||
class="compact-amount"
|
||||
:class="reg.incasari > 0 ? 'positive' : (reg.plati > 0 ? 'negative' : '')"
|
||||
>
|
||||
<template v-if="reg.incasari > 0">+{{ formatNumber(reg.incasari) }}</template>
|
||||
<template v-else-if="reg.plati > 0">-{{ formatNumber(reg.plati) }}</template>
|
||||
<template v-else>0</template>
|
||||
</span>
|
||||
<i :class="expandedMeta.has(index) ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="meta-chevron"></i>
|
||||
</div>
|
||||
<div v-if="expandedMeta.has(index)" class="compact-meta">
|
||||
<span v-if="reg.nume_cont_bancar" class="meta-item">{{ reg.nume_cont_bancar }}</span>
|
||||
@@ -552,11 +550,12 @@ const formatDate = (dateString) => {
|
||||
return format(new Date(dateString), "dd.MM.yyyy");
|
||||
};
|
||||
|
||||
// Short date format for mobile cards (DD/MM)
|
||||
// Short date format for mobile cards (DD/MM/YY)
|
||||
const formatDateShort = (dateString) => {
|
||||
if (!dateString) return "";
|
||||
const date = new Date(dateString);
|
||||
return `${String(date.getDate()).padStart(2, "0")}/${String(date.getMonth() + 1).padStart(2, "0")}`;
|
||||
const year = String(date.getFullYear()).slice(-2);
|
||||
return `${String(date.getDate()).padStart(2, "0")}/${String(date.getMonth() + 1).padStart(2, "0")}/${year}`;
|
||||
};
|
||||
|
||||
// Compact number format
|
||||
@@ -971,8 +970,8 @@ watch(
|
||||
.mobile-layout .register-view {
|
||||
padding-top: calc(56px + var(--space-md));
|
||||
padding-bottom: calc(56px + var(--space-md));
|
||||
padding-left: var(--space-md);
|
||||
padding-right: var(--space-md);
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Card Spacing */
|
||||
@@ -1005,10 +1004,9 @@ watch(
|
||||
|
||||
.mobile-totals-bar {
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--surface-border);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding: var(--space-sm) var(--space-xs);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.mobile-totals-grid {
|
||||
@@ -1054,7 +1052,7 @@ watch(
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
padding: var(--space-sm) 0;
|
||||
gap: var(--space-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1064,9 +1062,9 @@ watch(
|
||||
}
|
||||
|
||||
.compact-date {
|
||||
font-size: var(--text-xs);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-color-secondary);
|
||||
min-width: 40px;
|
||||
min-width: 54px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1083,20 +1081,7 @@ watch(
|
||||
font-weight: var(--font-semibold);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: var(--text-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.compact-amount.positive {
|
||||
color: var(--green-600);
|
||||
}
|
||||
|
||||
.compact-amount.negative {
|
||||
color: var(--red-600);
|
||||
}
|
||||
|
||||
.meta-chevron {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1104,9 +1089,9 @@ watch(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-md) var(--space-sm) calc(40px + var(--space-md) + var(--space-sm));
|
||||
padding: var(--space-xs) var(--space-xs) var(--space-sm) calc(54px + var(--space-sm));
|
||||
background: var(--surface-hover);
|
||||
font-size: var(--text-xs);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
@@ -1156,24 +1141,8 @@ watch(
|
||||
Dark Mode Support
|
||||
================================================ */
|
||||
|
||||
[data-theme="dark"] .compact-amount.positive {
|
||||
color: var(--green-400);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .compact-amount.negative {
|
||||
color: var(--red-400);
|
||||
}
|
||||
|
||||
/* Auto dark mode */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .compact-amount.positive {
|
||||
color: var(--green-400);
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .compact-amount.negative {
|
||||
color: var(--red-400);
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .numeric-value.negative {
|
||||
color: var(--red-400);
|
||||
}
|
||||
@@ -1189,7 +1158,7 @@ watch(
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.register-view {
|
||||
padding: var(--space-md);
|
||||
padding: var(--space-xs);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<div class="metrics-cards-section">
|
||||
<!-- Mobile: Swipeable KPI Cards Carousel -->
|
||||
<!-- 2 pages: enriched compact cards with embedded charts + financial indicators -->
|
||||
<SwipeableCards v-if="isMobile" :totalCards="2" :fixed-dots="true" :fill-height="true" class="mobile-kpi-carousel">
|
||||
<SwipeableCards v-if="isMobile" :totalCards="1" :fixed-dots="false" :fill-height="true" class="mobile-kpi-carousel">
|
||||
<!-- Page 1: Compact cards with embedded sparkline charts -->
|
||||
<template #card-0>
|
||||
<div class="solduri-grid-2x2">
|
||||
@@ -126,20 +126,15 @@
|
||||
:sold-total="budgetDebtSold"
|
||||
:breakdown="tvaBreakdown"
|
||||
/>
|
||||
<FinancialIndicatorsCard
|
||||
:loading="dashboardStore.financialIndicators.loading"
|
||||
:error="dashboardStore.financialIndicators.error"
|
||||
:data="dashboardStore.financialIndicators.data"
|
||||
:period="previousPeriodForIndicators"
|
||||
mobile
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Page 2: FinancialIndicatorsCard (US-015) -->
|
||||
<template #card-1>
|
||||
<FinancialIndicatorsCard
|
||||
:loading="dashboardStore.financialIndicators.loading"
|
||||
:error="dashboardStore.financialIndicators.error"
|
||||
:data="dashboardStore.financialIndicators.data"
|
||||
:initial-period="previousPeriodForIndicators"
|
||||
:cache-info="dashboardStore.financialIndicators.cacheInfo"
|
||||
mobile
|
||||
@period-change="handleFinancialIndicatorsPeriodChange"
|
||||
/>
|
||||
</template>
|
||||
</SwipeableCards>
|
||||
|
||||
<!-- Desktop: Grid layout (carduri grafice originale) - collapsible by default -->
|
||||
@@ -291,22 +286,22 @@
|
||||
</template>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
<CollapsibleCard
|
||||
label="Indicatori"
|
||||
:value="indicatorsSummaryLabel"
|
||||
value-class=""
|
||||
>
|
||||
<FinancialIndicatorsCard
|
||||
:loading="dashboardStore.financialIndicators.loading"
|
||||
:error="dashboardStore.financialIndicators.error"
|
||||
:data="dashboardStore.financialIndicators.data"
|
||||
:period="previousPeriodForIndicators"
|
||||
/>
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Financial Indicators Section - Desktop Only (US-014) -->
|
||||
<div v-if="!isMobile" class="financial-indicators-section">
|
||||
<FinancialIndicatorsCard
|
||||
:loading="dashboardStore.financialIndicators.loading"
|
||||
:error="dashboardStore.financialIndicators.error"
|
||||
:data="dashboardStore.financialIndicators.data"
|
||||
:initial-period="previousPeriodForIndicators"
|
||||
:cache-info="dashboardStore.financialIndicators.cacheInfo"
|
||||
@period-change="handleFinancialIndicatorsPeriodChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -826,16 +821,6 @@ 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
|
||||
@@ -856,6 +841,15 @@ const currentMonthLabel = computed(() => {
|
||||
|
||||
// Computed property pentru luna anterioară - pentru indicatorii financiari
|
||||
// Luna curentă e în lucru, deci folosim luna anterioară pentru date finale
|
||||
// Summary label for Indicators CollapsibleCard header (desktop)
|
||||
const indicatorsSummaryLabel = computed(() => {
|
||||
const d = dashboardStore.financialIndicators.data;
|
||||
if (!d) return '';
|
||||
const marja = d?.profitabilitate?.marja_profit_brut?.value;
|
||||
if (marja === null || marja === undefined) return '';
|
||||
return `${Number(marja).toLocaleString('ro-RO', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}% Marjă`;
|
||||
});
|
||||
|
||||
const previousPeriodForIndicators = computed(() => {
|
||||
if (!periodStore.selectedPeriod) return null;
|
||||
|
||||
@@ -1705,10 +1699,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
/* US-014: Financial Indicators Section - Desktop Only */
|
||||
.financial-indicators-section {
|
||||
margin-top: var(--space-lg);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* Mobile Budget Card wrapper (card-6 in SwipeableCards) */
|
||||
.mobile-budget-card {
|
||||
@@ -1988,11 +1979,7 @@ onUnmounted(() => {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mobile-kpi-carousel :deep(.financial-indicators-card) {
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
/* FinancialIndicatorsCard now has its own card styling - do NOT strip it */
|
||||
|
||||
.mobile-kpi-carousel .mobile-budget-card {
|
||||
border: none;
|
||||
@@ -2031,4 +2018,5 @@ onUnmounted(() => {
|
||||
.solduri-grid-2x2 > * {
|
||||
/* Height auto - only as tall as content needs */
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -352,7 +352,6 @@
|
||||
<span class="partner-sold" :class="{ overdue: group.hasRestant }">
|
||||
{{ formatCurrency(group.totalSold) }}
|
||||
</span>
|
||||
<i :class="isGroupExpanded(group.name) ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="partner-chevron"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isGroupExpanded(group.name)" class="partner-invoices">
|
||||
@@ -590,7 +589,7 @@ const formatDate = (value) => {
|
||||
return date.toLocaleDateString('ro-RO', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
year: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1455,21 +1454,19 @@ onUnmounted(() => {
|
||||
.mobile-partner-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.mobile-partner-row {
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.partner-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
padding: var(--space-sm) var(--space-xs);
|
||||
cursor: pointer;
|
||||
gap: var(--space-sm);
|
||||
min-height: 44px;
|
||||
@@ -1516,25 +1513,15 @@ onUnmounted(() => {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.partner-sold.overdue {
|
||||
color: var(--red-600);
|
||||
}
|
||||
|
||||
.partner-chevron {
|
||||
font-size: 12px;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.partner-invoices {
|
||||
border-top: 1px solid var(--surface-border);
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.invoice-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
padding: var(--space-xs) var(--space-xs);
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
@@ -1544,24 +1531,20 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.invoice-ref {
|
||||
font-size: var(--text-xs);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-color-secondary);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.invoice-sold {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-semibold);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.invoice-sold.overdue {
|
||||
color: var(--red-600);
|
||||
}
|
||||
|
||||
/* Empty data state */
|
||||
.empty-data {
|
||||
display: flex;
|
||||
@@ -1657,11 +1640,6 @@ onUnmounted(() => {
|
||||
background: var(--primary-800);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .partner-sold.overdue,
|
||||
[data-theme="dark"] .invoice-sold.overdue {
|
||||
color: var(--red-400);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .filter-chip {
|
||||
background: var(--surface-100);
|
||||
@@ -1670,10 +1648,5 @@ onUnmounted(() => {
|
||||
:root:not([data-theme]) .filter-chip.active {
|
||||
background: var(--primary-800);
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .partner-sold.overdue,
|
||||
:root:not([data-theme]) .invoice-sold.overdue {
|
||||
color: var(--red-400);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
1085
src/modules/reports/views/FinancialIndicatorsView.vue
Normal file
1085
src/modules/reports/views/FinancialIndicatorsView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -632,17 +632,17 @@ const formatCompact = (amount) => {
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "";
|
||||
try {
|
||||
return format(new Date(dateString), "dd/MM/yyyy", { locale: ro });
|
||||
return format(new Date(dateString), "dd/MM/yy", { locale: ro });
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
// Short date format for mobile (DD/MM only)
|
||||
// Short date format for mobile (DD/MM/YY with year)
|
||||
const formatDateShort = (dateString) => {
|
||||
if (!dateString) return "";
|
||||
try {
|
||||
return format(new Date(dateString), "dd/MM", { locale: ro });
|
||||
return format(new Date(dateString), "dd/MM/yy", { locale: ro });
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
@@ -1025,8 +1025,8 @@ watch(
|
||||
.mobile-layout .invoices {
|
||||
padding-top: calc(56px + 48px + var(--space-md)); /* Account for MobileTopBar + tabs */
|
||||
padding-bottom: calc(56px + var(--space-md)); /* Account for fixed MobileBottomNav */
|
||||
padding-left: var(--space-md);
|
||||
padding-right: var(--space-md);
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
@@ -1154,9 +1154,8 @@ watch(
|
||||
.mobile-totals-bar {
|
||||
background: var(--surface-card);
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-sm) var(--space-xs);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.mobile-totals-content {
|
||||
@@ -1193,7 +1192,7 @@ watch(
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
min-height: 44px;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
padding: var(--space-sm) 0;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
@@ -1346,7 +1345,7 @@ watch(
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.invoices {
|
||||
padding: var(--space-md);
|
||||
padding: var(--space-xs);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
|
||||
@@ -1057,8 +1057,8 @@ watch(
|
||||
.mobile-layout .trial-balance {
|
||||
padding-top: calc(56px + var(--space-md)); /* Account for fixed MobileTopBar */
|
||||
padding-bottom: calc(56px + var(--space-md)); /* Account for fixed MobileBottomNav */
|
||||
padding-left: var(--space-md);
|
||||
padding-right: var(--space-md);
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Card Spacing */
|
||||
@@ -1088,9 +1088,8 @@ watch(
|
||||
.mobile-totals-bar {
|
||||
background: var(--surface-card);
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-sm) var(--space-xs);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.mobile-totals-content {
|
||||
@@ -1142,7 +1141,7 @@ watch(
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
min-height: 44px;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
padding: var(--space-sm) 0;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,12 @@ const routes = [
|
||||
component: () => import('@reports/views/MaturityAnalysisView.vue'),
|
||||
meta: { requiresAuth: true, title: 'Analiză Scadențe - ROA2WEB' }
|
||||
},
|
||||
{
|
||||
path: 'financial-indicators',
|
||||
name: 'FinancialIndicators',
|
||||
component: () => import('@reports/views/FinancialIndicatorsView.vue'),
|
||||
meta: { requiresAuth: true, title: 'Indicatori Financiari - ROA2WEB' }
|
||||
},
|
||||
{
|
||||
// US-603: Single route for Detailed Invoices with tabs (Clienți/Furnizori)
|
||||
path: 'detailed-invoices',
|
||||
|
||||
@@ -41,11 +41,12 @@
|
||||
* Events:
|
||||
* - item-click: Emitted when a button item (without `to`) is clicked
|
||||
*
|
||||
* Default items (4 links):
|
||||
* Default items (5 links):
|
||||
* - Dashboard (/dashboard)
|
||||
* - Bonuri (/data-entry)
|
||||
* - Detalii (/reports/detailed-invoices)
|
||||
* - Setări (/settings)
|
||||
* - Facturi (/reports/detailed-invoices)
|
||||
* - Banca (/reports/bank)
|
||||
* - Casa (/reports/cash)
|
||||
* - Indicatori (/reports/financial-indicators)
|
||||
*/
|
||||
|
||||
defineProps({
|
||||
@@ -57,9 +58,10 @@ defineProps({
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ to: '/dashboard', icon: 'pi pi-home', label: 'Dashboard' },
|
||||
{ to: '/data-entry', icon: 'pi pi-shopping-bag', label: 'Bonuri' },
|
||||
{ to: '/reports/detailed-invoices', icon: 'pi pi-file-edit', label: 'Detalii' },
|
||||
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări' }
|
||||
{ to: '/reports/detailed-invoices', icon: 'pi pi-file-edit', label: 'Facturi' },
|
||||
{ to: '/reports/bank', icon: 'pi pi-building', label: 'Banca' },
|
||||
{ to: '/reports/cash', icon: 'pi pi-wallet', label: 'Casa' },
|
||||
{ to: '/reports/financial-indicators', icon: 'pi pi-chart-bar', label: 'Indicatori' }
|
||||
],
|
||||
validator: (items) => {
|
||||
return Array.isArray(items) && items.every(
|
||||
|
||||
@@ -724,9 +724,10 @@ const rapoarteItems = [
|
||||
{ to: '/reports/bank', icon: 'pi pi-building', label: 'Bancă', exactMatch: true }
|
||||
]
|
||||
|
||||
// ANALIZE: Scadențe
|
||||
// ANALIZE: Scadențe, Indicatori Financiari
|
||||
const analizeItems = [
|
||||
{ to: '/reports/maturity-analysis', icon: 'pi pi-clock', label: 'Scadențe', exactMatch: true }
|
||||
{ to: '/reports/maturity-analysis', icon: 'pi pi-clock', label: 'Scadențe', exactMatch: true },
|
||||
{ to: '/reports/financial-indicators', icon: 'pi pi-chart-bar', label: 'Indicatori', exactMatch: true }
|
||||
]
|
||||
|
||||
// ADMINISTRARE: Setări
|
||||
|
||||
Reference in New Issue
Block a user