feat(dashboard-solduri): Complete dashboard solduri v2 implementation

## Features
- US-2001: Create reusable SolduriCompactCard component
- US-2004: Solduri section on Desktop (top, without title)
- US-2005: Remove MaturityAndDetailsCard from Dashboard
- US-2006: Integrate Solduri data from dashboardStore
- US-2007: Visual indicators for financial status
- US-2008: Refresh button in Dashboard header

## UI Improvements
- Desktop: 2x2 grid for solduri cards with larger breakdown fonts
- Mobile: Single column layout with auto height
- Theme persistence: synchronous initialization to prevent flash
- Unified "Bonuri" icon (pi-shopping-bag) across all navigation

## Files Changed
- New: SolduriCompactCard.vue - expandable cards for Trezorerie/Clienți/Furnizori/TVA
- Modified: DashboardView.vue - integrated solduri section
- Modified: index.html - theme init script
- Modified: Mobile navigation components - icon consistency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-19 08:37:10 +00:00
parent eedc2bca67
commit 15327687f4
9 changed files with 909 additions and 631 deletions

View File

@@ -128,7 +128,7 @@ export default {
// PRINCIPALE: Dashboard, Bonuri
const principaleItems = ref([
{ to: '/dashboard', icon: 'pi pi-home', label: 'Dashboard', exactMatch: true },
{ to: '/data-entry', icon: 'pi pi-receipt', label: 'Bonuri', exactMatch: false }
{ to: '/data-entry', icon: 'pi pi-shopping-bag', label: 'Bonuri', exactMatch: false }
]);
// RAPOARTE: Facturi, Balanță, Casa și Banca

View File

@@ -0,0 +1,374 @@
<template>
<div
class="solduri-compact-card"
:class="[`solduri-compact-card--${type}`, { 'solduri-compact-card--expanded': isExpanded }]"
@click="toggleExpanded"
>
<!-- Header: Label + Value -->
<div class="solduri-compact-card__header">
<div class="solduri-compact-card__content">
<span class="solduri-compact-card__label">{{ label }}</span>
<span class="solduri-compact-card__value" :class="valueColorClass">
{{ formatCurrency(total) }}
</span>
</div>
<i
class="pi pi-chevron-down solduri-compact-card__chevron"
:class="{ 'solduri-compact-card__chevron--expanded': isExpanded }"
></i>
</div>
<!-- Expandable Breakdown Section -->
<div v-if="isExpanded && hasBreakdown" class="solduri-compact-card__breakdown">
<!-- Trezorerie: Casa + Bancă -->
<template v-if="type === 'trezorerie'">
<!-- Casa Total -->
<div class="solduri-compact-card__breakdown-item">
<span class="solduri-compact-card__breakdown-label">Casa</span>
<span class="solduri-compact-card__breakdown-value">{{ formatCurrency(casaTotal) }}</span>
</div>
<!-- Sub-conturi Casa (imediat sub Casa) -->
<template v-if="breakdown?.casa?.items?.length">
<div
v-for="(item, idx) in breakdown.casa.items"
:key="`casa-${idx}`"
class="solduri-compact-card__breakdown-subitem"
>
<span class="solduri-compact-card__breakdown-sublabel">
{{ item.nume || `Cont ${item.cont}` }}
</span>
<span class="solduri-compact-card__breakdown-subvalue">{{ formatCurrency(item.sold) }}</span>
</div>
</template>
<!-- Bancă Total -->
<div class="solduri-compact-card__breakdown-item">
<span class="solduri-compact-card__breakdown-label">Bancă</span>
<span class="solduri-compact-card__breakdown-value">{{ formatCurrency(bancaTotal) }}</span>
</div>
<!-- Sub-conturi Bancă (imediat sub Bancă) -->
<template v-if="breakdown?.banca?.items?.length">
<div
v-for="(item, idx) in breakdown.banca.items"
:key="`banca-${idx}`"
class="solduri-compact-card__breakdown-subitem"
>
<span class="solduri-compact-card__breakdown-sublabel">
{{ item.nume || `Cont ${item.cont}` }}
</span>
<span class="solduri-compact-card__breakdown-subvalue">{{ formatCurrency(item.sold) }}</span>
</div>
</template>
</template>
<!-- Clienți/Furnizori: Buckets (În termen, Restant) -->
<template v-else-if="type === 'clienti' || type === 'furnizori'">
<div class="solduri-compact-card__breakdown-item">
<span class="solduri-compact-card__breakdown-label">În termen</span>
<span class="solduri-compact-card__breakdown-value">
{{ formatCurrency(breakdown?.in_termen?.total || 0) }}
</span>
</div>
<div class="solduri-compact-card__breakdown-item">
<span class="solduri-compact-card__breakdown-label">Restant</span>
<span class="solduri-compact-card__breakdown-value solduri-compact-card__breakdown-value--warning">
{{ formatCurrency(breakdown?.restant?.total || 0) }}
</span>
</div>
<!-- Perioade restante -->
<template v-if="breakdown?.restant?.perioade">
<div
v-for="(value, key) in breakdown.restant.perioade"
:key="key"
class="solduri-compact-card__breakdown-subitem"
>
<span class="solduri-compact-card__breakdown-sublabel">{{ formatPeriodLabel(key) }}</span>
<span class="solduri-compact-card__breakdown-subvalue">{{ formatCurrency(value) }}</span>
</div>
</template>
</template>
<!-- TVA: Simple display (no breakdown needed) -->
<template v-else-if="type === 'tva'">
<div class="solduri-compact-card__breakdown-item">
<span class="solduri-compact-card__breakdown-label">
{{ total >= 0 ? 'TVA de recuperat' : 'TVA de plată' }}
</span>
<span
class="solduri-compact-card__breakdown-value"
:class="total >= 0 ? 'solduri-compact-card__breakdown-value--success' : 'solduri-compact-card__breakdown-value--danger'"
>
{{ formatCurrency(Math.abs(total)) }}
</span>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
// Type definitions
type CardType = 'trezorerie' | 'clienti' | 'furnizori' | 'tva'
interface TrezorerieBreakdown {
casa?: {
total?: number
items?: Array<{ nume?: string; cont?: string; sold: number }>
}
banca?: {
total?: number
items?: Array<{ nume?: string; cont?: string; sold: number }>
}
}
interface ClientiFurnizoriBreakdown {
total?: number
in_termen?: { total?: number }
restant?: {
total?: number
perioade?: Record<string, number>
}
}
type BreakdownType = TrezorerieBreakdown | ClientiFurnizoriBreakdown | null
// Props
const props = defineProps<{
type: CardType
total: number
breakdown?: BreakdownType
casaTotal?: number
bancaTotal?: number
}>()
// State
const isExpanded = ref(false)
// Computed: Label based on type
const label = computed(() => {
const labels: Record<CardType, string> = {
trezorerie: 'TREZORERIE',
clienti: 'CLIENȚI',
furnizori: 'FURNIZORI',
tva: 'TVA'
}
return labels[props.type] || props.type.toUpperCase()
})
// Computed: Value color class based on type and value
const valueColorClass = computed(() => {
if (props.type === 'tva') {
return props.total >= 0
? 'solduri-compact-card__value--success'
: 'solduri-compact-card__value--danger'
}
return ''
})
// Computed: Check if breakdown data exists
const hasBreakdown = computed(() => {
if (props.type === 'trezorerie') {
return props.casaTotal !== undefined || props.bancaTotal !== undefined || props.breakdown
}
if (props.type === 'clienti' || props.type === 'furnizori') {
return props.breakdown !== null && props.breakdown !== undefined
}
if (props.type === 'tva') {
return true // TVA always shows status
}
return false
})
// Methods
const toggleExpanded = () => {
if (hasBreakdown.value) {
isExpanded.value = !isExpanded.value
}
}
const formatCurrency = (amount: number | undefined | null): string => {
if (amount === undefined || amount === null) return '0 RON'
return new Intl.NumberFormat('ro-RO', {
style: 'currency',
currency: 'RON',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount)
}
const formatPeriodLabel = (key: string): string => {
const labelMap: Record<string, string> = {
'7_zile': '7 zile',
'14_zile': '14 zile',
'30_zile': '30 zile',
'60_zile': '60 zile',
'90_zile': '90 zile',
'peste_90_zile': 'Peste 90 zile'
}
return labelMap[key] || key
}
</script>
<style scoped>
/* SolduriCompactCard - Compact card for 2x2 grid layout */
.solduri-compact-card {
background: var(--surface-card);
border: 1px solid var(--surface-border);
border-radius: var(--radius-md);
padding: var(--space-md);
cursor: pointer;
transition: all var(--transition-fast);
min-height: 80px;
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.solduri-compact-card:hover {
box-shadow: var(--shadow-md);
border-color: var(--color-primary);
}
.solduri-compact-card:active {
transform: scale(0.98);
}
/* Header Layout */
.solduri-compact-card__header {
display: flex;
align-items: center;
gap: var(--space-sm);
}
/* Content */
.solduri-compact-card__content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-xs);
min-width: 0;
}
.solduri-compact-card__label {
font-size: var(--text-xs);
font-weight: var(--font-semibold);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.solduri-compact-card__value {
font-size: var(--text-lg);
font-weight: var(--font-bold);
color: var(--color-text);
font-family: var(--font-mono, monospace);
line-height: var(--leading-tight);
}
/* Value color modifiers */
.solduri-compact-card__value--success {
color: var(--green-600);
}
.solduri-compact-card__value--danger {
color: var(--red-600);
}
/* Chevron */
.solduri-compact-card__chevron {
color: var(--color-text-secondary);
font-size: var(--text-sm);
transition: transform var(--transition-fast);
}
.solduri-compact-card__chevron--expanded {
transform: rotate(180deg);
}
/* Breakdown Section */
.solduri-compact-card__breakdown {
padding-top: var(--space-sm);
border-top: 1px solid var(--surface-border);
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.solduri-compact-card__breakdown-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-xs) 0;
}
.solduri-compact-card__breakdown-label {
font-size: var(--text-base);
color: var(--color-text-secondary);
font-weight: var(--font-medium);
}
.solduri-compact-card__breakdown-value {
font-size: var(--text-base);
font-weight: var(--font-semibold);
color: var(--color-text);
font-family: var(--font-mono, monospace);
}
.solduri-compact-card__breakdown-value--success {
color: var(--green-600);
}
.solduri-compact-card__breakdown-value--danger {
color: var(--red-600);
}
.solduri-compact-card__breakdown-value--warning {
color: var(--orange-600);
}
/* Sub-items (indented) */
.solduri-compact-card__breakdown-subitem {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-xs) 0;
padding-left: var(--space-md);
}
.solduri-compact-card__breakdown-sublabel {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.solduri-compact-card__breakdown-subvalue {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--color-text);
font-family: var(--font-mono, monospace);
}
/* Responsive - Mobile */
@media (max-width: 768px) {
.solduri-compact-card {
padding: var(--space-sm);
min-height: 70px;
}
.solduri-compact-card__value {
font-size: var(--text-base);
}
.solduri-compact-card__breakdown-subitem {
padding-left: var(--space-sm);
}
}
/* Touch target compliance - minimum 44x44px */
@media (pointer: coarse) {
.solduri-compact-card {
min-height: 80px;
}
}
</style>

View File

@@ -4,7 +4,9 @@
v-if="isMobile"
title="Dashboard"
:show-menu="true"
:actions="mobileTopBarActions"
@menu-click="showDrawer = true"
@action-click="handleMobileAction"
/>
<!-- Mobile Drawer Menu (replaces old Sidebar) -->
@@ -21,6 +23,16 @@
<!-- Dashboard Header - only on desktop -->
<div v-if="!isMobile" class="page-header">
<h1 class="page-title">Dashboard</h1>
<Button
icon="pi pi-refresh"
text
rounded
class="refresh-btn"
:class="{ 'is-loading': isLoading }"
@click="handleRefresh"
v-tooltip.bottom="'Actualizează datele'"
aria-label="Actualizează datele"
/>
</div>
<!-- Company selection removed - now handled in header only -->
@@ -28,8 +40,36 @@
<!-- Secțiune Carduri Noi - Adăugare -->
<div class="metrics-cards-section" v-if="!isLoading">
<!-- Mobile: Swipeable KPI Cards Carousel -->
<SwipeableCards v-if="isMobile" :totalCards="4" class="mobile-kpi-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">
<!-- Page 1: Grid 2x2 cu Solduri Compacte -->
<template #card-0>
<div class="solduri-grid-2x2">
<SolduriCompactCard
type="trezorerie"
:total="totalTrezorerie"
:casaTotal="treasuryData?.breakdown?.casa?.total || 0"
:bancaTotal="treasuryData?.breakdown?.banca?.total || 0"
:breakdown="treasuryData?.breakdown"
/>
<SolduriCompactCard
type="clienti"
:total="netBalanceData?.clienti_total || 0"
:breakdown="netBalanceData?.breakdown?.clienti"
/>
<SolduriCompactCard
type="furnizori"
:total="netBalanceData?.furnizori_total || 0"
:breakdown="netBalanceData?.breakdown?.furnizori"
/>
<SolduriCompactCard
type="tva"
:total="tvaTotal"
/>
</div>
</template>
<!-- Page 2: TreasuryDualCard (original graph card) -->
<template #card-1>
<TreasuryDualCard
:casaTotal="treasuryData?.breakdown?.casa?.total || 0"
:bancaTotal="treasuryData?.breakdown?.banca?.total || 0"
@@ -45,7 +85,8 @@
:previousSparklineLabels="previousSparklineLabels"
/>
</template>
<template #card-1>
<!-- Page 3: CashFlowMetricCard (original graph card) -->
<template #card-2>
<CashFlowMetricCard
:inflowsValue="monthlyInflows"
:outflowsValue="monthlyOutflows"
@@ -59,7 +100,8 @@
:previousSparklineLabels="previousSparklineLabels"
/>
</template>
<template #card-2>
<!-- Page 4: ClientiBalanceCard (original graph card) -->
<template #card-3>
<ClientiBalanceCard
:total="netBalanceData?.clienti_total || 0"
:trend="clientiTrend"
@@ -70,7 +112,8 @@
:breakdown="netBalanceData?.breakdown?.clienti"
/>
</template>
<template #card-3>
<!-- Page 5: FurnizoriBalanceCard (original graph card) -->
<template #card-4>
<FurnizoriBalanceCard
:total="netBalanceData?.furnizori_total || 0"
:trend="furnizoriTrend"
@@ -83,8 +126,33 @@
</template>
</SwipeableCards>
<!-- Desktop: Grid layout -->
<div v-else class="metrics-row">
<!-- US-2004: Desktop Solduri Section (sus, fără titlu) -->
<div v-if="!isMobile" class="desktop-solduri-section">
<SolduriCompactCard
type="trezorerie"
:total="totalTrezorerie"
:casaTotal="treasuryData?.breakdown?.casa?.total || 0"
:bancaTotal="treasuryData?.breakdown?.banca?.total || 0"
:breakdown="treasuryData?.breakdown"
/>
<SolduriCompactCard
type="clienti"
:total="netBalanceData?.clienti_total || 0"
:breakdown="netBalanceData?.breakdown?.clienti"
/>
<SolduriCompactCard
type="furnizori"
:total="netBalanceData?.furnizori_total || 0"
:breakdown="netBalanceData?.breakdown?.furnizori"
/>
<SolduriCompactCard
type="tva"
:total="tvaTotal"
/>
</div>
<!-- Desktop: Grid layout (carduri grafice originale) -->
<div v-if="!isMobile" class="metrics-row">
<TreasuryDualCard
:casaTotal="treasuryData?.breakdown?.casa?.total || 0"
:bancaTotal="treasuryData?.breakdown?.banca?.total || 0"
@@ -131,18 +199,6 @@
/>
</div>
<!-- Desktop: Rând 2: Analiză comparativă și Date Detaliate (combinat) -->
<div v-if="!isMobile" class="comparison-row">
<MaturityAndDetailsCard
:companyId="companyStore.selectedCompany?.id_firma"
@periodChanged="handleMaturityPeriodChange"
/>
</div>
</div>
<!-- Dashboard Content -->
<div class="dashboard-content">
<!-- Componenta MaturityAndDetailsCard include acum și tabelul detaliat -->
</div>
<!-- Loading State -->
@@ -161,13 +217,14 @@
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useRouter } from "vue-router";
import { useToast } from "primevue/usetoast";
import Button from "primevue/button";
// Import componente noi
import MetricCard from "@reports/components/dashboard/cards/MetricCard.vue";
import CashFlowMetricCard from "@reports/components/dashboard/cards/CashFlowMetricCard.vue";
import MaturityAndDetailsCard from "@reports/components/dashboard/cards/MaturityAndDetailsCard.vue";
import ClientiBalanceCard from "@reports/components/dashboard/cards/ClientiBalanceCard.vue";
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";
// Mobile components
import SwipeableCards from "@shared/components/mobile/SwipeableCards.vue";
import MobileTopBar from "@shared/components/mobile/MobileTopBar.vue";
@@ -205,12 +262,6 @@ const netBalanceData = ref(null);
const selectedPeriod = ref("12m");
const selectedChartType = ref("line");
// Handlers pentru schimbare perioadă
const handleMaturityPeriodChange = (period) => {
console.log("Maturity period changed:", period);
// Trigger reload cu noua perioadă
};
// Calculare trend bazată pe date reale din trends.raw
const calculateTrend = (metric) => {
if (!dashboardStore.trends?.raw) return null;
@@ -406,6 +457,18 @@ const treasuryPreviousSparkline = computed(() =>
const sparklineLabels = computed(() => getSparklineLabels());
const previousSparklineLabels = computed(() => getPreviousSparklineLabels());
// US-2002: Computed properties for SolduriCompactCard grid
const totalTrezorerie = computed(() => {
const casaTotal = treasuryData.value?.breakdown?.casa?.total || 0;
const bancaTotal = treasuryData.value?.breakdown?.banca?.total || 0;
return casaTotal + bancaTotal;
});
const tvaTotal = computed(() => {
// TVA from dashboard summary if available, otherwise default to 0
return dashboardStore.summary?.tva_sold || 0;
});
// Casa and Bancă specific trends and sparklines
const casaTrend = computed(() => {
// Calculate trend based on Casa proportion of treasury
@@ -522,6 +585,29 @@ const handleLogout = async () => {
router.push('/login');
};
// US-2008: Mobile top bar actions with refresh button
const mobileTopBarActions = computed(() => [
{
id: 'refresh',
icon: isLoading.value ? 'pi pi-spin pi-refresh' : 'pi pi-refresh',
label: 'Actualizează',
tooltip: 'Actualizează datele'
}
]);
// US-2008: Handle mobile action clicks
const handleMobileAction = async (action) => {
if (action.id === 'refresh') {
await handleRefresh();
}
};
// US-2008: Handle refresh button click (shared between mobile and desktop)
const handleRefresh = async () => {
if (isLoading.value) return; // Prevent multiple clicks during loading
await loadDashboardData();
};
// Computed property pentru luna curentă - folosește perioada din period selector
const currentMonthLabel = computed(() => {
// Prioritate: period selector > dashboard current period > loading
@@ -990,6 +1076,30 @@ onUnmounted(() => {
padding-bottom: 56px; /* MobileBottomNav height */
}
/* US-2008: Refresh Button Styles */
.refresh-btn {
color: var(--color-text-secondary);
transition: color var(--transition-fast), transform var(--transition-fast);
}
.refresh-btn:hover {
color: var(--color-primary);
}
/* US-2008: Rotating animation during loading */
.refresh-btn.is-loading .pi-refresh {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Company Selection */
.company-selection {
max-width: 500px;
@@ -1006,13 +1116,6 @@ onUnmounted(() => {
font-style: italic;
}
/* Dashboard Content */
.dashboard-content {
display: flex;
flex-direction: column;
gap: var(--space-xl);
}
/* Dashboard Sections */
.dashboard-section {
background: var(--color-bg);
@@ -1273,27 +1376,29 @@ onUnmounted(() => {
padding: 0 var(--space-md);
}
/* US-2004: Desktop Solduri Section - 2x2 grid (2 cards per row) */
.desktop-solduri-section {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
/* Metrics Cards Layout - Component-specific grid layouts */
.metrics-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.analysis-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.comparison-row {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
/* Responsive - All breakpoints consolidated */
@media (max-width: 1200px) {
@@ -1325,4 +1430,17 @@ onUnmounted(() => {
.mobile-kpi-carousel {
margin-bottom: var(--space-lg);
}
/* US-2002: Solduri list for mobile first page - 1 card per row */
.solduri-grid-2x2 {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-xs);
}
/* Touch target compliance - SolduriCompactCard handles its own min-height */
.solduri-grid-2x2 > * {
/* Height auto - only as tall as content needs */
}
</style>