Replaces "luna prec / luna curentă" columns with a clearer accounting reconciliation flow: Datorat (owed) → Achitat (paid) → Sold (remaining). Backend models gain 3 new fields per sub-account and group. Frontend shows ✓ when a debt is fully cleared. Mobile TVA card now shows both total and remaining sold. SwipeableCards gains fixedDots/fillHeight props for better layout above MobileBottomNav. Telegram formatter updated to use new fields and drops redundant RON suffix from amounts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
567 lines
17 KiB
Vue
567 lines
17 KiB
Vue
<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>
|
|
<!-- type !== 'tva': simple value -->
|
|
<span v-if="type !== 'tva'" class="solduri-compact-card__value" :class="valueColorClass">
|
|
{{ formatAmount(total) }}
|
|
</span>
|
|
<!-- type === 'tva': datorat · sold on same line, same font -->
|
|
<div v-else class="solduri-compact-card__debt-line">
|
|
<span class="solduri-compact-card__value" :class="valueColorClass">{{ formatAmount(total) }}</span>
|
|
<span class="solduri-compact-card__debt-sep">·</span>
|
|
<span class="solduri-compact-card__value solduri-compact-card__value--sold">
|
|
{{ soldTotal !== undefined && soldTotal > 0 ? formatAmount(soldTotal) : '0 ✓' }}
|
|
</span>
|
|
</div>
|
|
</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">{{ formatAmount(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">{{ formatAmount(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">{{ formatAmount(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">{{ formatAmount(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">
|
|
{{ formatAmount(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">
|
|
{{ formatAmount(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">{{ formatAmount(value) }}</span>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<!-- TVA / Datorii Buget: Breakdown pe grupe (TVA/BASS/CAM) cu sub-conturi -->
|
|
<template v-else-if="type === 'tva'">
|
|
<template v-if="Array.isArray(breakdown) && (breakdown as any[]).length">
|
|
<!-- Header coloane -->
|
|
<div class="solduri-compact-card__debt-header">
|
|
<span></span>
|
|
<span class="solduri-compact-card__col-head">Datorat</span>
|
|
<span class="solduri-compact-card__col-head">Sold</span>
|
|
</div>
|
|
|
|
<div v-for="group in (breakdown as any[])" :key="group.key">
|
|
<!-- Rând grup -->
|
|
<div
|
|
class="solduri-compact-card__breakdown-group"
|
|
@click.stop="toggleGroup(group.key)"
|
|
>
|
|
<span class="solduri-compact-card__breakdown-label solduri-compact-card__group-label">
|
|
<i class="pi pi-chevron-right solduri-compact-card__group-toggle"
|
|
:class="{ 'solduri-compact-card__group-toggle--expanded': expandedGroups.has(group.key) }"></i>
|
|
{{ group.label }}
|
|
</span>
|
|
<span class="solduri-compact-card__breakdown-value">
|
|
{{ group.datorat > 0 ? formatAmount(group.datorat) : '-' }}
|
|
</span>
|
|
<span class="solduri-compact-card__breakdown-value"
|
|
:class="group.sold <= 0 && group.datorat > 0 ? 'solduri-compact-card__cleared' : ''">
|
|
{{ group.sold > 0 ? formatAmount(group.sold) : (group.datorat > 0 ? '✓' : '-') }}
|
|
</span>
|
|
</div>
|
|
<!-- Sub-conturi expandabile -->
|
|
<div v-show="expandedGroups.has(group.key)">
|
|
<div
|
|
v-for="acc in group.sub_accounts"
|
|
:key="acc.cont"
|
|
class="solduri-compact-card__breakdown-subitem"
|
|
>
|
|
<span class="solduri-compact-card__breakdown-sublabel">{{ acc.label }}</span>
|
|
<span class="solduri-compact-card__breakdown-subvalue">
|
|
{{ acc.datorat > 0 ? formatAmount(acc.datorat) : '-' }}
|
|
</span>
|
|
<span class="solduri-compact-card__breakdown-subvalue">
|
|
{{ acc.sold > 0 ? formatAmount(acc.sold) : (acc.datorat > 0 ? '✓' : '-') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Obligații luna curentă -->
|
|
<template v-if="(breakdown as any[]).some((g: any) => g.curent > 0)">
|
|
<div class="solduri-compact-card__divider"></div>
|
|
<div class="solduri-compact-card__curent-summary">
|
|
<span class="solduri-compact-card__curent-title">Obligații curente:</span>
|
|
<span
|
|
v-for="g in (breakdown as any[]).filter((g: any) => g.curent > 0)"
|
|
:key="'c-' + g.key"
|
|
class="solduri-compact-card__curent-chip"
|
|
>
|
|
{{ g.label }} {{ formatAmount(g.curent) }}
|
|
</span>
|
|
<span class="solduri-compact-card__curent-total">
|
|
= {{ formatAmount(curentTotal) }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<template v-else>
|
|
<div class="solduri-compact-card__breakdown-item">
|
|
<span class="solduri-compact-card__breakdown-label">Fără date</span>
|
|
<span class="solduri-compact-card__breakdown-value">-</span>
|
|
</div>
|
|
</template>
|
|
</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
|
|
soldTotal?: number
|
|
breakdown?: BreakdownType
|
|
casaTotal?: number
|
|
bancaTotal?: number
|
|
}>()
|
|
|
|
// State
|
|
const isExpanded = ref(false)
|
|
const expandedGroups = ref(new Set<string>())
|
|
const toggleGroup = (key: string) => {
|
|
if (expandedGroups.value.has(key)) {
|
|
expandedGroups.value.delete(key)
|
|
} else {
|
|
expandedGroups.value.add(key)
|
|
}
|
|
expandedGroups.value = new Set(expandedGroups.value)
|
|
}
|
|
|
|
// Computed: Label based on type
|
|
const label = computed(() => {
|
|
const labels: Record<CardType, string> = {
|
|
trezorerie: 'TREZORERIE',
|
|
clienti: 'CLIENȚI',
|
|
furnizori: 'FURNIZORI',
|
|
tva: 'DATORII BUGET'
|
|
}
|
|
return labels[props.type] || props.type.toUpperCase()
|
|
})
|
|
|
|
// Computed: Value color class based on type and value
|
|
const valueColorClass = computed(() => {
|
|
if (props.type === 'tva') {
|
|
// total = tvaPreviousMonthNet = plata - recuperat
|
|
// pozitiv = datorie la buget (roșu), zero/negativ = fără datorie (verde)
|
|
return props.total > 0
|
|
? 'solduri-compact-card__value--danger'
|
|
: 'solduri-compact-card__value--success'
|
|
}
|
|
return ''
|
|
})
|
|
|
|
// Computed: Total obligații curente (doar pentru type === 'tva')
|
|
const curentTotal = computed(() => {
|
|
if (!Array.isArray(props.breakdown)) return 0
|
|
return (props.breakdown as any[])
|
|
.filter((g: any) => Number(g.curent || 0) > 0)
|
|
.reduce((sum: number, g: any) => sum + Number(g.curent || 0), 0)
|
|
})
|
|
|
|
// 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 props.breakdown !== null && props.breakdown !== undefined
|
|
}
|
|
return false
|
|
})
|
|
|
|
// Methods
|
|
const toggleExpanded = () => {
|
|
if (hasBreakdown.value) {
|
|
isExpanded.value = !isExpanded.value
|
|
}
|
|
}
|
|
|
|
const formatAmount = (amount: number | undefined | null): string => {
|
|
if (amount === undefined || amount === null) return '0'
|
|
return new Intl.NumberFormat('ro-RO', {
|
|
style: 'decimal',
|
|
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);
|
|
}
|
|
|
|
/* Debt line: datorat · sold inline, same font */
|
|
.solduri-compact-card__debt-line {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: var(--space-xs);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.solduri-compact-card__debt-sep {
|
|
color: var(--text-color-secondary);
|
|
font-size: var(--text-lg);
|
|
font-weight: var(--font-bold);
|
|
}
|
|
|
|
.solduri-compact-card__value--sold {
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
/* 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-sm);
|
|
color: var(--color-text-secondary);
|
|
font-weight: var(--font-medium);
|
|
}
|
|
|
|
.solduri-compact-card__breakdown-value {
|
|
font-size: var(--text-sm);
|
|
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);
|
|
}
|
|
|
|
/* TVA: 3-column grid layout (label | datorat | sold) */
|
|
.solduri-compact-card__debt-header,
|
|
.solduri-compact-card__breakdown-group,
|
|
.solduri-compact-card__breakdown-subitem {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto auto;
|
|
gap: var(--space-sm);
|
|
align-items: center;
|
|
}
|
|
|
|
.solduri-compact-card__col-head {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-color-secondary);
|
|
text-align: right;
|
|
min-width: 54px;
|
|
}
|
|
|
|
/* Override: breakdown-value/subvalue right-aligned with min-width in grid */
|
|
.solduri-compact-card__breakdown-group .solduri-compact-card__breakdown-value,
|
|
.solduri-compact-card__breakdown-subitem .solduri-compact-card__breakdown-subvalue {
|
|
text-align: right;
|
|
min-width: 54px;
|
|
}
|
|
|
|
/* TVA grup toggle */
|
|
.solduri-compact-card__breakdown-group {
|
|
cursor: pointer;
|
|
font-weight: var(--font-semibold);
|
|
padding: var(--space-xs) 0;
|
|
}
|
|
.solduri-compact-card__breakdown-group:hover { background: var(--surface-hover); }
|
|
|
|
.solduri-compact-card__group-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
}
|
|
|
|
.solduri-compact-card__group-toggle {
|
|
font-size: var(--text-xs);
|
|
color: var(--color-text-secondary);
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
.solduri-compact-card__group-toggle--expanded { transform: rotate(90deg); }
|
|
|
|
/* Cleared (paid) indicator */
|
|
.solduri-compact-card__cleared {
|
|
color: var(--green-600);
|
|
font-weight: var(--font-semibold);
|
|
}
|
|
|
|
/* Divider between breakdown and obligații curente */
|
|
.solduri-compact-card__divider {
|
|
height: 1px;
|
|
background: var(--surface-border);
|
|
margin: var(--space-sm) 0;
|
|
}
|
|
|
|
/* Obligații curente section */
|
|
.solduri-compact-card__curent-summary {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-xs);
|
|
align-items: center;
|
|
}
|
|
|
|
.solduri-compact-card__curent-title {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.solduri-compact-card__curent-chip {
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--font-semibold);
|
|
background: var(--surface-hover);
|
|
border-radius: var(--radius-sm);
|
|
padding: 2px var(--space-xs);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.solduri-compact-card__curent-total {
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--font-bold);
|
|
font-family: var(--font-mono, monospace);
|
|
color: var(--text-color);
|
|
margin-left: var(--space-xs);
|
|
}
|
|
|
|
/* 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>
|