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:
374
src/modules/reports/components/solduri/SolduriCompactCard.vue
Normal file
374
src/modules/reports/components/solduri/SolduriCompactCard.vue
Normal 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>
|
||||
Reference in New Issue
Block a user