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

@@ -139,12 +139,11 @@ const toggleChartsExpanded = () => {
chartsExpanded.value = !chartsExpanded.value;
};
// Format currency
// Format amount (without RON suffix)
const formatCurrency = (amount) => {
if (!amount && amount !== 0) return "0 RON";
if (!amount && amount !== 0) return "0";
return new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(Math.abs(amount));
@@ -303,8 +302,7 @@ const initializeInflowsChart = async () => {
const value = context.parsed.y;
const label = context.dataset.label || "";
const formattedValue = new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
@@ -513,8 +511,7 @@ const initializeOutflowsChart = async () => {
const value = context.parsed.y;
const label = context.dataset.label || "";
const formattedValue = new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
@@ -618,6 +615,18 @@ onBeforeUnmount(() => {
min-height: 420px;
}
/* Metric label and value typography */
.metric-label {
font-size: var(--text-sm);
color: var(--text-color-secondary);
}
.metric-value {
font-size: var(--text-sm);
font-weight: var(--font-bold);
font-family: var(--font-mono, monospace);
}
/* Split layout: Încasări | Divider | Plăți */
.values-section {
display: grid;

View File

@@ -155,12 +155,11 @@ const toggleChartsExpanded = () => {
chartsExpanded.value = !chartsExpanded.value;
};
// Format currency
// Format amount (without RON suffix)
const formatCurrency = (amount) => {
if (!amount && amount !== 0) return "0 RON";
if (!amount && amount !== 0) return "0";
return new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(Math.abs(amount));
@@ -370,8 +369,7 @@ const initializeChart = async () => {
const value = context.parsed.y;
const label = context.dataset.label || "";
const formattedValue = new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
@@ -496,7 +494,7 @@ onBeforeUnmount(() => {
}
.header-label {
font-size: var(--text-base);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--text-color);
}
@@ -508,11 +506,31 @@ onBeforeUnmount(() => {
}
.header-total {
font-size: var(--text-xl);
font-size: var(--text-sm);
font-weight: var(--font-bold);
font-family: var(--font-mono, monospace);
}
/* Breakdown section typography */
.breakdown-label {
font-size: var(--text-sm);
}
.breakdown-value {
font-size: var(--text-sm);
font-family: var(--font-mono, monospace);
}
.breakdown-sublabel {
font-size: var(--text-xs);
color: var(--text-color-secondary);
}
.breakdown-subvalue {
font-size: var(--text-xs);
font-family: var(--font-mono, monospace);
}
.header-total.positive {
color: var(--green-600);
}

View File

@@ -155,12 +155,11 @@ const toggleChartsExpanded = () => {
chartsExpanded.value = !chartsExpanded.value;
};
// Format currency
// Format amount (without RON suffix)
const formatCurrency = (amount) => {
if (!amount && amount !== 0) return "0 RON";
if (!amount && amount !== 0) return "0";
return new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(Math.abs(amount));
@@ -370,8 +369,7 @@ const initializeChart = async () => {
const value = context.parsed.y;
const label = context.dataset.label || "";
const formattedValue = new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
@@ -496,7 +494,7 @@ onBeforeUnmount(() => {
}
.header-label {
font-size: var(--text-base);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--text-color);
}
@@ -508,11 +506,31 @@ onBeforeUnmount(() => {
}
.header-total {
font-size: var(--text-xl);
font-size: var(--text-sm);
font-weight: var(--font-bold);
font-family: var(--font-mono, monospace);
}
/* Breakdown section typography */
.breakdown-label {
font-size: var(--text-sm);
}
.breakdown-value {
font-size: var(--text-sm);
font-family: var(--font-mono, monospace);
}
.breakdown-sublabel {
font-size: var(--text-xs);
color: var(--text-color-secondary);
}
.breakdown-subvalue {
font-size: var(--text-xs);
font-family: var(--font-mono, monospace);
}
.header-total.positive {
color: var(--green-600);
}

View File

@@ -3,7 +3,7 @@
<!-- Treasury items - Casa și Bancă stacked vertical cu expand individual -->
<div class="treasury-items">
<!-- Casa -->
<div class="treasury-group" v-if="casaItems.length > 0 || casaTotal > 0">
<div class="treasury-group" v-if="casaItems.length > 0 || casaTotal !== 0">
<div class="treasury-header" @click="toggleCasaExpanded">
<div class="treasury-header-left">
<i
@@ -12,7 +12,7 @@
></i>
<span class="treasury-label">Casa</span>
</div>
<span class="treasury-value text-success">{{ formatCurrency(casaTotal) }}</span>
<span class="treasury-value" :class="casaTotal >= 0 ? 'text-success' : 'text-danger'">{{ formatCurrency(casaTotal) }}</span>
</div>
<!-- Casa Sub-items -->
@@ -26,13 +26,13 @@
{{ item.nume || `Cont ${item.cont}` }}
<span v-if="item.cont" class="treasury-cont">({{ item.cont }})</span>
</span>
<span class="treasury-subvalue">{{ formatCurrency(item.sold) }}</span>
<span class="treasury-subvalue" :class="{ 'text-danger': item.sold < 0 }">{{ formatCurrency(item.sold) }}</span>
</div>
</div>
</div>
<!-- Bancă -->
<div class="treasury-group" v-if="bancaItems.length > 0 || bancaTotal > 0">
<div class="treasury-group" v-if="bancaItems.length > 0 || bancaTotal !== 0">
<div class="treasury-header" @click="toggleBancaExpanded">
<div class="treasury-header-left">
<i
@@ -41,7 +41,7 @@
></i>
<span class="treasury-label">Bancă</span>
</div>
<span class="treasury-value text-primary">{{ formatCurrency(bancaTotal) }}</span>
<span class="treasury-value" :class="bancaTotal >= 0 ? 'text-primary' : 'text-danger'">{{ formatCurrency(bancaTotal) }}</span>
</div>
<!-- Bancă Sub-items -->
@@ -55,7 +55,7 @@
{{ item.nume || `Cont ${item.cont}` }}
<span v-if="item.cont" class="treasury-cont">({{ item.cont }})</span>
</span>
<span class="treasury-subvalue">{{ formatCurrency(item.sold) }}</span>
<span class="treasury-subvalue" :class="{ 'text-danger': item.sold < 0 }">{{ formatCurrency(item.sold) }}</span>
</div>
</div>
</div>
@@ -189,15 +189,14 @@ const toggleChartsExpanded = () => {
chartsExpanded.value = !chartsExpanded.value;
};
// Format currency
// Format amount (without RON suffix)
const formatCurrency = (amount) => {
if (!amount && amount !== 0) return "0 RON";
if (!amount && amount !== 0) return "0";
return new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(Math.abs(amount));
}).format(amount);
};
// Check if sparkline data exists
@@ -327,8 +326,7 @@ const initializeCasaChart = async () => {
const value = context.parsed.y;
const label = context.dataset.label || "";
const formattedValue = new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
@@ -511,8 +509,7 @@ const initializeBancaChart = async () => {
const value = context.parsed.y;
const label = context.dataset.label || "";
const formattedValue = new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
@@ -663,13 +660,13 @@ onBeforeUnmount(() => {
}
.treasury-label {
font-size: var(--text-base);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--text-color);
}
.treasury-value {
font-size: var(--text-xl);
font-size: var(--text-sm);
font-weight: var(--font-bold);
font-family: var(--font-mono, monospace);
}
@@ -775,10 +772,6 @@ onBeforeUnmount(() => {
min-height: 280px;
}
.treasury-value {
font-size: var(--text-lg);
}
.sparkline-chart {
height: 130px;
}

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 {

View File

@@ -41,8 +41,13 @@
<!-- Company selection removed - now handled in header only -->
<!-- Loading Bar - non-blocking, subtle indicator while data loads -->
<div v-if="isLoading" class="loading-bar-container">
<div class="loading-bar"></div>
</div>
<!-- Secțiune Carduri Noi - Adăugare -->
<div class="metrics-cards-section" v-if="!isLoading">
<div class="metrics-cards-section">
<!-- Mobile: Swipeable KPI Cards Carousel -->
<!-- US-2002: 6 pages - first page is 2x2 grid with solduri, pages 2-5 are original graph cards, page 6 is financial indicators -->
<SwipeableCards v-if="isMobile" :totalCards="6" class="mobile-kpi-carousel">
@@ -68,7 +73,8 @@
/>
<SolduriCompactCard
type="tva"
:total="tvaTotal"
:total="tvaPreviousMonthNet"
:breakdown="tvaBreakdown"
/>
</div>
</template>
@@ -220,12 +226,66 @@
:cacheInfo="netBalanceCacheInfo"
/>
</CollapsibleCard>
<CollapsibleCard
label="Datorii la buget"
:value="budgetDebtTotalPrecedent"
:value-class="budgetDebtTotalPrecedent > 0 ? 'negative' : 'positive'"
>
<div class="budget-debt-breakdown-desktop">
<div class="budget-debt-breakdown-header">
<span class="budget-debt-col-label">Categorie</span>
<span class="budget-debt-col-header-value">Luna prec.</span>
<span class="budget-debt-col-header-value">Luna curentă</span>
</div>
<template v-for="group in budgetDebtBreakdown" :key="group.key">
<!-- Rând grup (clickabil pentru expand sub-conturi) -->
<div
class="budget-debt-breakdown-row budget-debt-group-row"
@click="toggleBudgetGroup(group.key)"
>
<span class="budget-debt-col-label budget-debt-group-label">
<i class="pi pi-chevron-right budget-debt-toggle"
:class="{ expanded: expandedBudgetGroups.has(group.key) }"></i>
{{ group.label }}
</span>
<span class="budget-debt-col-value">
{{ group.precedent !== 0 ? formatAmount(group.precedent) : '-' }}
</span>
<span class="budget-debt-col-value">
{{ group.curent !== 0 ? formatAmount(group.curent) : '-' }}
</span>
</div>
<!-- Sub-conturi (expandabile) -->
<div v-show="expandedBudgetGroups.has(group.key)" class="budget-debt-sub-accounts">
<div
v-for="acc in group.sub_accounts"
:key="acc.cont"
class="budget-debt-breakdown-row budget-debt-subrow"
>
<span class="budget-debt-col-label budget-debt-sub-label">{{ acc.label }}</span>
<span class="budget-debt-col-value budget-debt-sub-value">
{{ acc.precedent !== 0 ? formatAmount(acc.precedent) : '-' }}
</span>
<span class="budget-debt-col-value budget-debt-sub-value">
{{ acc.curent !== 0 ? formatAmount(acc.curent) : '-' }}
</span>
</div>
</div>
</template>
<div v-if="budgetDebtBreakdown.length === 0" class="budget-debt-breakdown-empty">
Nu există datorii înregistrate
</div>
</div>
</CollapsibleCard>
</div>
</div>
<!-- Financial Indicators Section - Desktop Only (US-014) -->
<div v-if="!isMobile && !isLoading" class="financial-indicators-section">
<div v-if="!isMobile" class="financial-indicators-section">
<FinancialIndicatorsCard
:loading="dashboardStore.financialIndicators.loading"
:error="dashboardStore.financialIndicators.error"
@@ -236,11 +296,6 @@
/>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="loading-state">
<div class="loading-spinner"></div>
<p>Se încarcă datele dashboard-ului...</p>
</div>
</div>
</main>
@@ -507,10 +562,63 @@ const totalTrezorerie = computed(() => {
});
const tvaTotal = computed(() => {
// TVA from dashboard summary if available, otherwise default to 0
return dashboardStore.summary?.tva_sold || 0;
const s = dashboardStore.summary;
if (!s) return 0;
// Pozitiv = de recuperat, Negativ = de plată
return Number(s.tva_recuperat_curent || 0) - Number(s.tva_plata_curent || 0);
});
const tvaPreviousMonth = computed(() => {
const s = dashboardStore.summary;
if (!s) return { plata: 0, recuperat: 0 };
return {
plata: Number(s.tva_plata_precedent || 0),
recuperat: Number(s.tva_recuperat_precedent || 0),
};
});
const tvaCurrentMonth = computed(() => {
const s = dashboardStore.summary;
if (!s) return { plata: 0, recuperat: 0 };
return {
plata: Number(s.tva_plata_curent || 0),
recuperat: Number(s.tva_recuperat_curent || 0),
};
});
// TVA net luna precedentă (valoare pozitivă = datorie la buget)
const tvaPreviousMonthNet = computed(() => {
const prev = tvaPreviousMonth.value;
return (prev.plata || 0) - (prev.recuperat || 0);
});
// Breakdown pe grupe datorii buget (TVA/BASS/CAM cu sub-conturi)
const budgetDebtBreakdown = computed(() => {
return dashboardStore.summary?.budget_debt_breakdown || [];
});
// Total precedent din toate grupurile (pentru header CollapsibleCard)
const budgetDebtTotalPrecedent = computed(() => {
const groups = dashboardStore.summary?.budget_debt_breakdown || [];
return groups.reduce((sum, g) => sum + Number(g.precedent || 0), 0);
});
const tvaBreakdown = computed(() => {
return dashboardStore.summary?.budget_debt_breakdown || [];
});
// Reactive state pentru expand/collapse grupe buget
const expandedBudgetGroups = ref(new Set());
const toggleBudgetGroup = (key) => {
if (expandedBudgetGroups.value.has(key)) {
expandedBudgetGroups.value.delete(key);
} else {
expandedBudgetGroups.value.add(key);
}
// Force reactivity update on Set
expandedBudgetGroups.value = new Set(expandedBudgetGroups.value);
};
// Net Cash Flow for CollapsibleCard header
const netCashFlow = computed(() => {
return (monthlyInflows.value || 0) - (monthlyOutflows.value || 0);
@@ -716,18 +824,18 @@ const handleCompanyChanged = async (company) => {
}
};
const formatCurrency = (amount) => {
if (!amount && amount !== 0) return "0,00 RON";
const formatAmount = (amount) => {
if (!amount && amount !== 0) return "0";
const numAmount = typeof amount === "string" ? parseFloat(amount) : amount;
if (isNaN(numAmount)) return "0,00 RON";
if (isNaN(numAmount)) return "0";
try {
return new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
style: "decimal",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(numAmount);
} catch (error) {
return `${numAmount.toLocaleString("ro-RO", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} RON`;
} catch {
return numAmount.toLocaleString("ro-RO", { minimumFractionDigits: 0, maximumFractionDigits: 0 });
}
};
@@ -1100,6 +1208,14 @@ const loadDashboardData = async () => {
const indicatorAn = prevPeriod?.an || null;
try {
// Fire financial indicators independently - it has its own loading state
// and should not block the main dashboard cards from appearing
dashboardStore.loadFinancialIndicators(
companyStore.selectedCompany.id_firma,
indicatorLuna,
indicatorAn,
);
await Promise.all([
dashboardStore.loadDashboardSummary(
companyStore.selectedCompany.id_firma,
@@ -1111,12 +1227,6 @@ const loadDashboardData = async () => {
loadMonthlyFlows(),
loadTreasuryBreakdown(),
loadNetBalanceBreakdown(),
// US-014: Load financial indicators for desktop card (luna anterioară)
dashboardStore.loadFinancialIndicators(
companyStore.selectedCompany.id_firma,
indicatorLuna,
indicatorAn,
),
]);
} catch (error) {
console.error("Failed to load dashboard data:", error);
@@ -1447,15 +1557,26 @@ onUnmounted(() => {
color: var(--color-text-secondary);
}
/* Loading State - Uses global .loading-spinner from interactive.css */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-xl);
color: var(--color-text-secondary);
text-align: center;
/* Loading Bar - non-blocking, subtle indicator at top of content */
.loading-bar-container {
height: 3px;
background: var(--surface-border);
border-radius: var(--radius-full);
overflow: hidden;
margin-bottom: var(--space-md);
}
.loading-bar {
height: 100%;
background: var(--color-primary);
animation: loading-progress 1.5s ease-in-out infinite;
border-radius: var(--radius-full);
}
@keyframes loading-progress {
0% { width: 0%; transform: translateX(0%); }
50% { width: 60%; transform: translateX(60%); }
100% { width: 30%; transform: translateX(300%); }
}
/* Responsive Design - Consolidated (Component-specific only) */
@@ -1537,6 +1658,104 @@ onUnmounted(() => {
width: 100%;
}
/* Budget Debt Breakdown in Desktop CollapsibleCard */
.budget-debt-breakdown-desktop {
display: flex;
flex-direction: column;
padding: var(--space-md);
}
.budget-debt-breakdown-header {
display: grid;
grid-template-columns: 1fr auto auto;
gap: var(--space-md);
padding: var(--space-xs) 0 var(--space-sm) 0;
border-bottom: 1px solid var(--surface-border);
margin-bottom: var(--space-xs);
}
.budget-debt-breakdown-row {
display: grid;
grid-template-columns: 1fr auto auto;
gap: var(--space-md);
padding: var(--space-xs) 0;
align-items: center;
}
.budget-debt-col-label {
font-size: var(--text-sm);
color: var(--color-text-secondary);
font-weight: var(--font-medium);
}
.budget-debt-breakdown-header .budget-debt-col-label {
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.budget-debt-col-value {
font-size: var(--text-sm);
font-weight: var(--font-semibold);
font-family: var(--font-mono, monospace);
color: var(--color-text);
white-space: nowrap;
min-width: 130px;
text-align: right;
}
.budget-debt-col-header-value {
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-secondary);
white-space: nowrap;
min-width: 130px;
text-align: right;
}
.budget-debt-breakdown-empty {
padding: var(--space-sm) 0;
color: var(--color-text-secondary);
font-style: italic;
font-size: var(--text-sm);
}
/* Grup row - clickabil, bold */
.budget-debt-group-row {
cursor: pointer;
font-weight: var(--font-semibold);
background: var(--surface-ground);
border-radius: var(--radius-sm);
}
.budget-debt-group-row:hover { background: var(--surface-hover); }
.budget-debt-group-label {
display: flex;
align-items: center;
gap: var(--space-xs);
}
.budget-debt-toggle {
font-size: var(--text-xs);
color: var(--text-color-secondary);
transition: transform var(--transition-fast);
}
.budget-debt-toggle.expanded { transform: rotate(90deg); }
/* Sub-conturi indentate */
.budget-debt-sub-accounts {
background: var(--surface-card);
border-left: 2px solid var(--surface-border);
margin-left: var(--space-sm);
}
.budget-debt-subrow { opacity: 0.85; }
.budget-debt-sub-label { padding-left: var(--space-md); font-size: var(--text-xs); }
.budget-debt-sub-value { font-size: var(--text-xs); }
/* Responsive - All breakpoints consolidated */
@media (max-width: 1200px) {
.metrics-row {

View File

@@ -76,8 +76,7 @@ const formattedValue = computed(() => {
if (props.formatCurrency && typeof props.value === 'number') {
return new Intl.NumberFormat('ro-RO', {
style: 'currency',
currency: 'RON',
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(props.value)
@@ -138,12 +137,14 @@ const toggleExpanded = () => {
flex-shrink: 0;
}
/* Label */
/* Label - matches SolduriCompactCard style */
.collapsible-card__label {
font-size: var(--text-sm);
font-size: var(--text-xs);
color: var(--text-color-secondary);
font-weight: var(--font-medium);
font-weight: var(--font-semibold);
white-space: nowrap;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Value */
@@ -223,10 +224,6 @@ const toggleExpanded = () => {
padding: var(--space-sm) var(--space-md);
}
.collapsible-card__label {
font-size: var(--text-xs);
}
.collapsible-card__value {
font-size: var(--text-base);
}