feat: Add A-Z filter for clients/suppliers in Telegram bot

- Add A-Z alphabetical filter keyboard for clients and suppliers lists
  (same pattern as company selection, without emoji)
- Increase clients/suppliers list pagination from 10 to 20 items per page
- Remove emoji from company A-Z filter button for consistency
- Add 6 new callback handlers: clients_alpha_menu, clients_alpha:LETTER,
  clients_alpha_page:PAGE:LETTER, and supplier equivalents
- Dashboard service and models updates
- Telegram bot: email handlers, auth, DB operations, internal API improvements
- Frontend: dashboard cards updates (CashFlow, Clienti, Furnizori, Treasury)
- Frontend: SolduriCompactCard and CollapsibleCard improvements
- DashboardView enhancements
- start.sh and run-with-restart.sh script updates
- IIS web.config and service worker updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-21 14:34:15 +00:00
parent 1366dbc11c
commit 30f55cf18b
28 changed files with 1671 additions and 520 deletions

View File

@@ -9,7 +9,7 @@
<div class="solduri-compact-card__content">
<span class="solduri-compact-card__label">{{ label }}</span>
<span class="solduri-compact-card__value" :class="valueColorClass">
{{ formatCurrency(total) }}
{{ formatAmount(total) }}
</span>
</div>
<i
@@ -25,7 +25,7 @@
<!-- 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>
<span class="solduri-compact-card__breakdown-value">{{ formatAmount(casaTotal) }}</span>
</div>
<!-- Sub-conturi Casa (imediat sub Casa) -->
<template v-if="breakdown?.casa?.items?.length">
@@ -37,13 +37,13 @@
<span class="solduri-compact-card__breakdown-sublabel">
{{ item.nume || `Cont ${item.cont}` }}
</span>
<span class="solduri-compact-card__breakdown-subvalue">{{ formatCurrency(item.sold) }}</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">{{ formatCurrency(bancaTotal) }}</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">
@@ -55,7 +55,7 @@
<span class="solduri-compact-card__breakdown-sublabel">
{{ item.nume || `Cont ${item.cont}` }}
</span>
<span class="solduri-compact-card__breakdown-subvalue">{{ formatCurrency(item.sold) }}</span>
<span class="solduri-compact-card__breakdown-subvalue">{{ formatAmount(item.sold) }}</span>
</div>
</template>
</template>
@@ -65,13 +65,13 @@
<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) }}
{{ 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">
{{ formatCurrency(breakdown?.restant?.total || 0) }}
{{ formatAmount(breakdown?.restant?.total || 0) }}
</span>
</div>
<!-- Perioade restante -->
@@ -82,24 +82,50 @@
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>
<span class="solduri-compact-card__breakdown-subvalue">{{ formatAmount(value) }}</span>
</div>
</template>
</template>
<!-- TVA: Simple display (no breakdown needed) -->
<!-- TVA / Datorii Buget: Breakdown pe grupe (TVA/BASS/CAM) cu sub-conturi -->
<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 v-if="Array.isArray(breakdown) && (breakdown as any[]).length">
<div v-for="group in (breakdown as any[])" :key="group.key">
<!-- Rând grup -->
<div
class="solduri-compact-card__breakdown-item 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.precedent !== 0 ? formatAmount(group.precedent) : '-' }}
</span>
</div>
<!-- Sub-conturi -->
<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.precedent !== 0 ? formatAmount(acc.precedent) : '-' }}
</span>
</div>
</div>
</div>
</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>
@@ -144,6 +170,15 @@ const props = defineProps<{
// 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(() => {
@@ -151,7 +186,7 @@ const label = computed(() => {
trezorerie: 'TREZORERIE',
clienti: 'CLIENȚI',
furnizori: 'FURNIZORI',
tva: 'TVA'
tva: 'DATORII BUGET'
}
return labels[props.type] || props.type.toUpperCase()
})
@@ -159,9 +194,11 @@ const label = computed(() => {
// 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'
// 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 ''
})
@@ -175,7 +212,7 @@ const hasBreakdown = computed(() => {
return props.breakdown !== null && props.breakdown !== undefined
}
if (props.type === 'tva') {
return true // TVA always shows status
return props.breakdown !== null && props.breakdown !== undefined
}
return false
})
@@ -187,11 +224,10 @@ const toggleExpanded = () => {
}
}
const formatCurrency = (amount: number | undefined | null): string => {
if (amount === undefined || amount === null) return '0 RON'
const formatAmount = (amount: number | undefined | null): string => {
if (amount === undefined || amount === null) return '0'
return new Intl.NumberFormat('ro-RO', {
style: 'currency',
currency: 'RON',
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount)
@@ -304,13 +340,13 @@ const formatPeriodLabel = (key: string): string => {
}
.solduri-compact-card__breakdown-label {
font-size: var(--text-base);
font-size: var(--text-sm);
color: var(--color-text-secondary);
font-weight: var(--font-medium);
}
.solduri-compact-card__breakdown-value {
font-size: var(--text-base);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--color-text);
font-family: var(--font-mono, monospace);
@@ -349,6 +385,26 @@ const formatPeriodLabel = (key: string): string => {
font-family: var(--font-mono, monospace);
}
/* TVA grup toggle */
.solduri-compact-card__breakdown-group {
cursor: pointer;
font-weight: var(--font-semibold);
}
.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); }
/* Responsive - Mobile */
@media (max-width: 768px) {
.solduri-compact-card {