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

@@ -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>