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:
Claude Agent
2026-03-01 08:55:11 +00:00
parent 06cbf8fb9d
commit 74dff2d17d
14 changed files with 1624 additions and 2811 deletions

View File

@@ -156,7 +156,7 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.app-container, .app-container,
.page-container { .page-container {
padding: var(--space-md); padding: var(--space-xs);
} }
/* US-705: Keep main-content padding minimal - app-container handles content spacing */ /* US-705: Keep main-content padding minimal - app-container handles content spacing */
@@ -171,7 +171,7 @@
.dashboard-container { .dashboard-container {
gap: var(--space-lg); gap: var(--space-lg);
padding: var(--space-md); padding: var(--space-xs);
} }
.card-container { .card-container {
@@ -195,7 +195,7 @@
.app-container, .app-container,
.page-container, .page-container,
.dashboard-container { .dashboard-container {
padding: var(--space-sm); padding: var(--space-xs);
} }
/* US-705: Keep main-content padding minimal on small screens too */ /* US-705: Keep main-content padding minimal on small screens too */

View File

@@ -956,3 +956,22 @@
color: var(--text-color-secondary, #9ca3af) !important; 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;
}

View File

@@ -1,92 +1,77 @@
<template> <template>
<div class="indicator-item" :class="statusClass"> <div class="indicator-item-flat" @click="expanded = !expanded">
<!-- Label (top) cu toggle pentru descriere --> <!-- Collapsed flat row: Label + Value + Status icon -->
<div class="indicator-label"> <div class="indicator-row-flat">
{{ label }} <span class="indicator-label">{{ label }}</span>
<i <span class="indicator-value">{{ formattedValue }}{{ unit ? ` ${unit}` : '' }}</span>
v-if="description" <span class="indicator-status-icon" :class="statusClass">
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">
<i :class="statusIcon"></i> <i :class="statusIcon"></i>
</div> </span>
</div> </div>
<!-- Sparkline (bottom) --> <!-- Expanded content (tap to reveal) -->
<div <div v-if="expanded" class="indicator-expand-flat slide-down">
v-if="hasSparklineData" <!-- Description -->
class="sparkline-container" <div v-if="description" class="indicator-description">
ref="sparklineContainer" {{ description }}
> </div>
<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 --> <!-- Sparkline -->
<circle
v-if="hoveredIndex !== null"
:cx="hoveredPoint.x"
:cy="hoveredPoint.y"
r="4"
:fill="strokeColor"
class="sparkline-point"
/>
</svg>
<!-- Tooltip -->
<div <div
v-if="hoveredIndex !== null" v-if="hasSparklineData"
class="sparkline-tooltip" class="sparkline-container"
:style="tooltipStyle" ref="sparklineContainer"
> >
<div class="tooltip-label">{{ tooltipLabel }}</div> <svg
<div class="tooltip-value">{{ tooltipValue }}</div> 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>
</div>
<!-- YoY Trend indicator (shows variation from first to last sparkline value) --> <!-- YoY Trend -->
<div <div
v-if="hasSparklineData && trendInfo.text !== '-'" v-if="hasSparklineData && trendInfo.text !== '-'"
class="yoy-trend" class="yoy-trend"
:class="trendInfo.class" :class="trendInfo.class"
> >
<i :class="trendInfo.icon"></i> <i :class="trendInfo.icon"></i>
<span class="trend-value">{{ trendInfo.text }}</span> <span class="trend-value">{{ trendInfo.text }}</span>
<span class="trend-label">vs 12 luni</span> <span class="trend-label">vs 12 luni</span>
</div> </div>
<!-- Threshold info --> <!-- Threshold info -->
<div v-if="thresholdText" class="indicator-threshold"> <div v-if="thresholdText" class="indicator-threshold">
{{ thresholdText }} {{ thresholdText }}
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -94,12 +79,8 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
// Description toggle state // Expand/collapse state (tap to expand)
const descExpanded = ref(false) const expanded = ref(false)
const toggleDescription = () => {
descExpanded.value = !descExpanded.value
}
const props = defineProps({ const props = defineProps({
label: { label: {
@@ -151,10 +132,13 @@ const sparklineContainer = ref(null)
const hoveredIndex = ref(null) const hoveredIndex = ref(null)
const mouseX = ref(0) const mouseX = ref(0)
// Computed: Format the displayed value // Computed: Format the displayed value (locale-aware with thousands separators)
const formattedValue = computed(() => { const formattedValue = computed(() => {
if (props.value === null || props.value === undefined) return '-' 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 // Computed: Check if we have sparkline data
@@ -258,7 +242,7 @@ const tooltipValue = computed(() => {
if (hoveredIndex.value === null) return '' if (hoveredIndex.value === null) return ''
const value = pointPositions.value[hoveredIndex.value]?.value const value = pointPositions.value[hoveredIndex.value]?.value
if (value === null || value === undefined) return '-' 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 // Computed: Tooltip position style
@@ -357,60 +341,51 @@ const handleMouseLeave = () => {
</script> </script>
<style scoped> <style scoped>
/* Indicator Item Container */ /* Indicator Item - Flat row design */
.indicator-item { .indicator-item-flat {
background: transparent; border-bottom: 1px solid var(--surface-border);
border: 1px solid var(--surface-border);
border-radius: var(--radius-sm);
padding: var(--space-md);
display: flex; display: flex;
flex-direction: column; 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; cursor: pointer;
padding: 2px; transition: background var(--transition-fast);
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
opacity: 0.6;
} }
.desc-toggle:hover { .indicator-item-flat:last-child {
color: var(--primary-color); border-bottom: none;
opacity: 1; }
.indicator-item-flat:active {
background: var(--surface-hover); 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 { .indicator-description {
font-size: var(--text-xs); font-size: var(--text-sm);
color: var(--text-color-secondary); color: var(--text-color-secondary);
text-align: center; line-height: 1.4;
opacity: 0.8; padding: var(--space-xs) var(--space-sm);
line-height: 1.3;
padding: var(--space-xs);
background: var(--surface-hover); background: var(--surface-hover);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
margin-bottom: var(--space-xs); margin-bottom: var(--space-xs);
@@ -432,30 +407,19 @@ const handleMouseLeave = () => {
} }
} }
/* Main section: Value + Status Icon */ /* Value (inline, right of label) */
.indicator-main {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
position: relative;
}
/* Value (large, centered) */
.indicator-value { .indicator-value {
font-size: var(--text-2xl); font-size: var(--text-base);
font-weight: var(--font-bold); font-weight: var(--font-semibold);
font-family: var(--font-mono); font-variant-numeric: tabular-nums;
text-align: center; color: var(--text-color);
white-space: nowrap;
} }
/* Status Icon (right side) */ /* Status Icon (rightmost) - only element that gets status color */
.indicator-status-icon { .indicator-status-icon {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
font-size: var(--text-lg); font-size: var(--text-lg);
flex-shrink: 0;
} }
/* Status Colors - use 600 variants in light mode, 400 variants in dark mode for better contrast */ /* Status Colors - use 600 variants in light mode, 400 variants in dark mode for better contrast */
@@ -620,7 +584,7 @@ const handleMouseLeave = () => {
/* Threshold info */ /* Threshold info */
.indicator-threshold { .indicator-threshold {
font-size: var(--text-xs); font-size: var(--text-sm);
color: var(--text-color-secondary); color: var(--text-color-secondary);
text-align: center; text-align: center;
margin-top: var(--space-xs); margin-top: var(--space-xs);
@@ -628,18 +592,6 @@ const handleMouseLeave = () => {
/* Responsive */ /* Responsive */
@media (max-width: 768px) { @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 { .sparkline-container {
height: 32px; height: 32px;
} }

View File

@@ -249,13 +249,11 @@
<span class="compact-partner">{{ reg.nume || 'Fără partener' }}</span> <span class="compact-partner">{{ reg.nume || 'Fără partener' }}</span>
<span <span
class="compact-amount" class="compact-amount"
:class="reg.incasari > 0 ? 'positive' : (reg.plati > 0 ? 'negative' : '')"
> >
<template v-if="reg.incasari > 0">+{{ formatNumber(reg.incasari) }}</template> <template v-if="reg.incasari > 0">+{{ formatNumber(reg.incasari) }}</template>
<template v-else-if="reg.plati > 0">-{{ formatNumber(reg.plati) }}</template> <template v-else-if="reg.plati > 0">-{{ formatNumber(reg.plati) }}</template>
<template v-else>0</template> <template v-else>0</template>
</span> </span>
<i :class="expandedMeta.has(index) ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="meta-chevron"></i>
</div> </div>
<div v-if="expandedMeta.has(index)" class="compact-meta"> <div v-if="expandedMeta.has(index)" class="compact-meta">
<span v-if="reg.nume_cont_bancar" class="meta-item">{{ reg.nume_cont_bancar }}</span> <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"); 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) => { const formatDateShort = (dateString) => {
if (!dateString) return ""; if (!dateString) return "";
const date = new Date(dateString); 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 // Compact number format
@@ -971,8 +970,8 @@ watch(
.mobile-layout .register-view { .mobile-layout .register-view {
padding-top: calc(56px + var(--space-md)); padding-top: calc(56px + var(--space-md));
padding-bottom: calc(56px + var(--space-md)); padding-bottom: calc(56px + var(--space-md));
padding-left: var(--space-md); padding-left: 0;
padding-right: var(--space-md); padding-right: 0;
} }
/* Card Spacing */ /* Card Spacing */
@@ -1005,10 +1004,9 @@ watch(
.mobile-totals-bar { .mobile-totals-bar {
background: var(--surface-card); background: var(--surface-card);
border: 1px solid var(--surface-border); border-bottom: 1px solid var(--surface-border);
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) var(--space-xs);
margin-bottom: var(--space-md); margin-bottom: var(--space-sm);
border-radius: var(--radius-md);
} }
.mobile-totals-grid { .mobile-totals-grid {
@@ -1054,7 +1052,7 @@ watch(
display: flex; display: flex;
align-items: center; align-items: center;
min-height: 44px; min-height: 44px;
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) 0;
gap: var(--space-sm); gap: var(--space-sm);
cursor: pointer; cursor: pointer;
} }
@@ -1064,9 +1062,9 @@ watch(
} }
.compact-date { .compact-date {
font-size: var(--text-xs); font-size: var(--text-sm);
color: var(--text-color-secondary); color: var(--text-color-secondary);
min-width: 40px; min-width: 54px;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -1083,20 +1081,7 @@ watch(
font-weight: var(--font-semibold); font-weight: var(--font-semibold);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: var(--text-sm); font-size: var(--text-sm);
flex-shrink: 0; color: var(--text-color);
}
.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);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -1104,9 +1089,9 @@ watch(
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-xs); 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); background: var(--surface-hover);
font-size: var(--text-xs); font-size: var(--text-sm);
color: var(--text-color-secondary); color: var(--text-color-secondary);
} }
@@ -1156,24 +1141,8 @@ watch(
Dark Mode Support 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 */ /* Auto dark mode */
@media (prefers-color-scheme: dark) { @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 { :root:not([data-theme]) .numeric-value.negative {
color: var(--red-400); color: var(--red-400);
} }
@@ -1189,7 +1158,7 @@ watch(
@media (max-width: 768px) { @media (max-width: 768px) {
.register-view { .register-view {
padding: var(--space-md); padding: var(--space-xs);
} }
} }
</style> </style>

View File

@@ -249,13 +249,11 @@
<span class="compact-partner">{{ reg.nume || 'Fără partener' }}</span> <span class="compact-partner">{{ reg.nume || 'Fără partener' }}</span>
<span <span
class="compact-amount" class="compact-amount"
:class="reg.incasari > 0 ? 'positive' : (reg.plati > 0 ? 'negative' : '')"
> >
<template v-if="reg.incasari > 0">+{{ formatNumber(reg.incasari) }}</template> <template v-if="reg.incasari > 0">+{{ formatNumber(reg.incasari) }}</template>
<template v-else-if="reg.plati > 0">-{{ formatNumber(reg.plati) }}</template> <template v-else-if="reg.plati > 0">-{{ formatNumber(reg.plati) }}</template>
<template v-else>0</template> <template v-else>0</template>
</span> </span>
<i :class="expandedMeta.has(index) ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="meta-chevron"></i>
</div> </div>
<div v-if="expandedMeta.has(index)" class="compact-meta"> <div v-if="expandedMeta.has(index)" class="compact-meta">
<span v-if="reg.nume_cont_bancar" class="meta-item">{{ reg.nume_cont_bancar }}</span> <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"); 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) => { const formatDateShort = (dateString) => {
if (!dateString) return ""; if (!dateString) return "";
const date = new Date(dateString); 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 // Compact number format
@@ -971,8 +970,8 @@ watch(
.mobile-layout .register-view { .mobile-layout .register-view {
padding-top: calc(56px + var(--space-md)); padding-top: calc(56px + var(--space-md));
padding-bottom: calc(56px + var(--space-md)); padding-bottom: calc(56px + var(--space-md));
padding-left: var(--space-md); padding-left: 0;
padding-right: var(--space-md); padding-right: 0;
} }
/* Card Spacing */ /* Card Spacing */
@@ -1005,10 +1004,9 @@ watch(
.mobile-totals-bar { .mobile-totals-bar {
background: var(--surface-card); background: var(--surface-card);
border: 1px solid var(--surface-border); border-bottom: 1px solid var(--surface-border);
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) var(--space-xs);
margin-bottom: var(--space-md); margin-bottom: var(--space-sm);
border-radius: var(--radius-md);
} }
.mobile-totals-grid { .mobile-totals-grid {
@@ -1054,7 +1052,7 @@ watch(
display: flex; display: flex;
align-items: center; align-items: center;
min-height: 44px; min-height: 44px;
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) 0;
gap: var(--space-sm); gap: var(--space-sm);
cursor: pointer; cursor: pointer;
} }
@@ -1064,9 +1062,9 @@ watch(
} }
.compact-date { .compact-date {
font-size: var(--text-xs); font-size: var(--text-sm);
color: var(--text-color-secondary); color: var(--text-color-secondary);
min-width: 40px; min-width: 54px;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -1083,20 +1081,7 @@ watch(
font-weight: var(--font-semibold); font-weight: var(--font-semibold);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-size: var(--text-sm); font-size: var(--text-sm);
flex-shrink: 0; color: var(--text-color);
}
.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);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -1104,9 +1089,9 @@ watch(
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-xs); 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); background: var(--surface-hover);
font-size: var(--text-xs); font-size: var(--text-sm);
color: var(--text-color-secondary); color: var(--text-color-secondary);
} }
@@ -1156,24 +1141,8 @@ watch(
Dark Mode Support 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 */ /* Auto dark mode */
@media (prefers-color-scheme: dark) { @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 { :root:not([data-theme]) .numeric-value.negative {
color: var(--red-400); color: var(--red-400);
} }
@@ -1189,7 +1158,7 @@ watch(
@media (max-width: 768px) { @media (max-width: 768px) {
.register-view { .register-view {
padding: var(--space-md); padding: var(--space-xs);
} }
} }
</style> </style>

View File

@@ -50,7 +50,7 @@
<div class="metrics-cards-section"> <div class="metrics-cards-section">
<!-- Mobile: Swipeable KPI Cards Carousel --> <!-- Mobile: Swipeable KPI Cards Carousel -->
<!-- 2 pages: enriched compact cards with embedded charts + financial indicators --> <!-- 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 --> <!-- Page 1: Compact cards with embedded sparkline charts -->
<template #card-0> <template #card-0>
<div class="solduri-grid-2x2"> <div class="solduri-grid-2x2">
@@ -126,20 +126,15 @@
:sold-total="budgetDebtSold" :sold-total="budgetDebtSold"
:breakdown="tvaBreakdown" :breakdown="tvaBreakdown"
/> />
<FinancialIndicatorsCard
:loading="dashboardStore.financialIndicators.loading"
:error="dashboardStore.financialIndicators.error"
:data="dashboardStore.financialIndicators.data"
:period="previousPeriodForIndicators"
mobile
/>
</div> </div>
</template> </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> </SwipeableCards>
<!-- Desktop: Grid layout (carduri grafice originale) - collapsible by default --> <!-- Desktop: Grid layout (carduri grafice originale) - collapsible by default -->
@@ -291,22 +286,22 @@
</template> </template>
</div> </div>
</CollapsibleCard> </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>
</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> </div>
</main> </main>
@@ -826,16 +821,6 @@ const handleRefresh = async () => {
await loadDashboardData(); 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 // Computed property pentru luna curentă - folosește perioada din period selector
const currentMonthLabel = computed(() => { const currentMonthLabel = computed(() => {
// Prioritate: period selector > dashboard current period > loading // Prioritate: period selector > dashboard current period > loading
@@ -856,6 +841,15 @@ const currentMonthLabel = computed(() => {
// Computed property pentru luna anterioară - pentru indicatorii financiari // Computed property pentru luna anterioară - pentru indicatorii financiari
// Luna curentă e în lucru, deci folosim luna anterioară pentru date finale // 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(() => { const previousPeriodForIndicators = computed(() => {
if (!periodStore.selectedPeriod) return null; if (!periodStore.selectedPeriod) return null;
@@ -1705,10 +1699,7 @@ onUnmounted(() => {
} }
/* US-014: Financial Indicators Section - Desktop Only */ /* 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 wrapper (card-6 in SwipeableCards) */
.mobile-budget-card { .mobile-budget-card {
@@ -1988,11 +1979,7 @@ onUnmounted(() => {
box-shadow: none; box-shadow: none;
} }
.mobile-kpi-carousel :deep(.financial-indicators-card) { /* FinancialIndicatorsCard now has its own card styling - do NOT strip it */
border: none;
background: transparent;
box-shadow: none;
}
.mobile-kpi-carousel .mobile-budget-card { .mobile-kpi-carousel .mobile-budget-card {
border: none; border: none;
@@ -2031,4 +2018,5 @@ onUnmounted(() => {
.solduri-grid-2x2 > * { .solduri-grid-2x2 > * {
/* Height auto - only as tall as content needs */ /* Height auto - only as tall as content needs */
} }
</style> </style>

View File

@@ -352,7 +352,6 @@
<span class="partner-sold" :class="{ overdue: group.hasRestant }"> <span class="partner-sold" :class="{ overdue: group.hasRestant }">
{{ formatCurrency(group.totalSold) }} {{ formatCurrency(group.totalSold) }}
</span> </span>
<i :class="isGroupExpanded(group.name) ? 'pi pi-chevron-up' : 'pi pi-chevron-down'" class="partner-chevron"></i>
</div> </div>
</div> </div>
<div v-if="isGroupExpanded(group.name)" class="partner-invoices"> <div v-if="isGroupExpanded(group.name)" class="partner-invoices">
@@ -590,7 +589,7 @@ const formatDate = (value) => {
return date.toLocaleDateString('ro-RO', { return date.toLocaleDateString('ro-RO', {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
year: 'numeric' year: '2-digit'
}) })
} }
@@ -1455,21 +1454,19 @@ onUnmounted(() => {
.mobile-partner-list { .mobile-partner-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-sm); gap: 0;
} }
.mobile-partner-row { .mobile-partner-row {
background: var(--surface-card); background: transparent;
border: 1px solid var(--surface-border); border-bottom: 1px solid var(--surface-border);
border-radius: var(--radius-md);
overflow: hidden;
} }
.partner-header { .partner-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) var(--space-xs);
cursor: pointer; cursor: pointer;
gap: var(--space-sm); gap: var(--space-sm);
min-height: 44px; min-height: 44px;
@@ -1516,25 +1513,15 @@ onUnmounted(() => {
color: var(--text-color); color: var(--text-color);
} }
.partner-sold.overdue {
color: var(--red-600);
}
.partner-chevron {
font-size: 12px;
color: var(--text-color-secondary);
}
.partner-invoices { .partner-invoices {
border-top: 1px solid var(--surface-border); border-top: 1px solid var(--surface-border);
background: var(--surface-hover);
} }
.invoice-line { .invoice-line {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: var(--space-xs) var(--space-md); padding: var(--space-xs) var(--space-xs);
border-bottom: 1px solid var(--surface-border); border-bottom: 1px solid var(--surface-border);
gap: var(--space-sm); gap: var(--space-sm);
} }
@@ -1544,24 +1531,20 @@ onUnmounted(() => {
} }
.invoice-ref { .invoice-ref {
font-size: var(--text-xs); font-size: var(--text-sm);
color: var(--text-color-secondary); color: var(--text-color-secondary);
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.invoice-sold { .invoice-sold {
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: var(--font-semibold); font-weight: var(--font-semibold);
font-variant-numeric: tabular-nums;
font-size: var(--text-sm);
color: var(--text-color); color: var(--text-color);
flex-shrink: 0; flex-shrink: 0;
} }
.invoice-sold.overdue {
color: var(--red-600);
}
/* Empty data state */ /* Empty data state */
.empty-data { .empty-data {
display: flex; display: flex;
@@ -1657,11 +1640,6 @@ onUnmounted(() => {
background: var(--primary-800); 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) { @media (prefers-color-scheme: dark) {
:root:not([data-theme]) .filter-chip { :root:not([data-theme]) .filter-chip {
background: var(--surface-100); background: var(--surface-100);
@@ -1670,10 +1648,5 @@ onUnmounted(() => {
:root:not([data-theme]) .filter-chip.active { :root:not([data-theme]) .filter-chip.active {
background: var(--primary-800); background: var(--primary-800);
} }
:root:not([data-theme]) .partner-sold.overdue,
:root:not([data-theme]) .invoice-sold.overdue {
color: var(--red-400);
}
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -632,17 +632,17 @@ const formatCompact = (amount) => {
const formatDate = (dateString) => { const formatDate = (dateString) => {
if (!dateString) return ""; if (!dateString) return "";
try { try {
return format(new Date(dateString), "dd/MM/yyyy", { locale: ro }); return format(new Date(dateString), "dd/MM/yy", { locale: ro });
} catch (error) { } catch (error) {
return dateString; return dateString;
} }
}; };
// Short date format for mobile (DD/MM only) // Short date format for mobile (DD/MM/YY with year)
const formatDateShort = (dateString) => { const formatDateShort = (dateString) => {
if (!dateString) return ""; if (!dateString) return "";
try { try {
return format(new Date(dateString), "dd/MM", { locale: ro }); return format(new Date(dateString), "dd/MM/yy", { locale: ro });
} catch (error) { } catch (error) {
return ""; return "";
} }
@@ -1025,8 +1025,8 @@ watch(
.mobile-layout .invoices { .mobile-layout .invoices {
padding-top: calc(56px + 48px + var(--space-md)); /* Account for MobileTopBar + tabs */ padding-top: calc(56px + 48px + var(--space-md)); /* Account for MobileTopBar + tabs */
padding-bottom: calc(56px + var(--space-md)); /* Account for fixed MobileBottomNav */ padding-bottom: calc(56px + var(--space-md)); /* Account for fixed MobileBottomNav */
padding-left: var(--space-md); padding-left: 0;
padding-right: var(--space-md); padding-right: 0;
} }
/* ================================================ /* ================================================
@@ -1154,9 +1154,8 @@ watch(
.mobile-totals-bar { .mobile-totals-bar {
background: var(--surface-card); background: var(--surface-card);
border-bottom: 1px solid var(--surface-border); border-bottom: 1px solid var(--surface-border);
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) var(--space-xs);
margin-bottom: var(--space-md); margin-bottom: var(--space-sm);
border-radius: var(--radius-md);
} }
.mobile-totals-content { .mobile-totals-content {
@@ -1193,7 +1192,7 @@ watch(
align-items: center; align-items: center;
border-bottom: 1px solid var(--surface-border); border-bottom: 1px solid var(--surface-border);
min-height: 44px; min-height: 44px;
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) 0;
gap: var(--space-sm); gap: var(--space-sm);
} }
@@ -1346,7 +1345,7 @@ watch(
@media (max-width: 768px) { @media (max-width: 768px) {
.invoices { .invoices {
padding: var(--space-md); padding: var(--space-xs);
} }
.page-title { .page-title {

View File

@@ -1057,8 +1057,8 @@ watch(
.mobile-layout .trial-balance { .mobile-layout .trial-balance {
padding-top: calc(56px + var(--space-md)); /* Account for fixed MobileTopBar */ padding-top: calc(56px + var(--space-md)); /* Account for fixed MobileTopBar */
padding-bottom: calc(56px + var(--space-md)); /* Account for fixed MobileBottomNav */ padding-bottom: calc(56px + var(--space-md)); /* Account for fixed MobileBottomNav */
padding-left: var(--space-md); padding-left: 0;
padding-right: var(--space-md); padding-right: 0;
} }
/* Card Spacing */ /* Card Spacing */
@@ -1088,9 +1088,8 @@ watch(
.mobile-totals-bar { .mobile-totals-bar {
background: var(--surface-card); background: var(--surface-card);
border-bottom: 1px solid var(--surface-border); border-bottom: 1px solid var(--surface-border);
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) var(--space-xs);
margin-bottom: var(--space-md); margin-bottom: var(--space-sm);
border-radius: var(--radius-md);
} }
.mobile-totals-content { .mobile-totals-content {
@@ -1142,7 +1141,7 @@ watch(
align-items: center; align-items: center;
border-bottom: 1px solid var(--surface-border); border-bottom: 1px solid var(--surface-border);
min-height: 44px; min-height: 44px;
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) 0;
gap: var(--space-sm); gap: var(--space-sm);
} }

View File

@@ -73,6 +73,12 @@ const routes = [
component: () => import('@reports/views/MaturityAnalysisView.vue'), component: () => import('@reports/views/MaturityAnalysisView.vue'),
meta: { requiresAuth: true, title: 'Analiză Scadențe - ROA2WEB' } 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) // US-603: Single route for Detailed Invoices with tabs (Clienți/Furnizori)
path: 'detailed-invoices', path: 'detailed-invoices',

View File

@@ -41,11 +41,12 @@
* Events: * Events:
* - item-click: Emitted when a button item (without `to`) is clicked * - item-click: Emitted when a button item (without `to`) is clicked
* *
* Default items (4 links): * Default items (5 links):
* - Dashboard (/dashboard) * - Dashboard (/dashboard)
* - Bonuri (/data-entry) * - Facturi (/reports/detailed-invoices)
* - Detalii (/reports/detailed-invoices) * - Banca (/reports/bank)
* - Setări (/settings) * - Casa (/reports/cash)
* - Indicatori (/reports/financial-indicators)
*/ */
defineProps({ defineProps({
@@ -57,9 +58,10 @@ defineProps({
type: Array, type: Array,
default: () => [ default: () => [
{ to: '/dashboard', icon: 'pi pi-home', label: 'Dashboard' }, { 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: 'Facturi' },
{ to: '/reports/detailed-invoices', icon: 'pi pi-file-edit', label: 'Detalii' }, { to: '/reports/bank', icon: 'pi pi-building', label: 'Banca' },
{ to: '/settings', icon: 'pi pi-cog', label: 'Setări' } { to: '/reports/cash', icon: 'pi pi-wallet', label: 'Casa' },
{ to: '/reports/financial-indicators', icon: 'pi pi-chart-bar', label: 'Indicatori' }
], ],
validator: (items) => { validator: (items) => {
return Array.isArray(items) && items.every( return Array.isArray(items) && items.every(

View File

@@ -724,9 +724,10 @@ const rapoarteItems = [
{ to: '/reports/bank', icon: 'pi pi-building', label: 'Bancă', exactMatch: true } { to: '/reports/bank', icon: 'pi pi-building', label: 'Bancă', exactMatch: true }
] ]
// ANALIZE: Scadențe // ANALIZE: Scadențe, Indicatori Financiari
const analizeItems = [ 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 // ADMINISTRARE: Setări