feat: Add totals from all filtered records to invoices, treasury, and trial balance

Previously, totals were computed client-side from only the current page data,
which gave incorrect results when paginating. Now the backend calculates totals
across ALL filtered records and returns them in the API response.

- Invoice: Add total_sold_all field for sum of all filtered invoice balances
- Treasury: Add sold_precedent_all, total_incasari_all, total_plati_all, sold_final_all
- Trial Balance: Add 6-column totals (debit/credit for each balance type)
- Frontend stores and views updated to use backend totals

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-09 15:45:24 +02:00
parent c75e896a84
commit de24a79db5
12 changed files with 248 additions and 76 deletions

View File

@@ -8,6 +8,8 @@ export const useInvoicesStore = defineStore("invoices", () => {
const isLoading = ref(false);
const error = ref(null);
const accountingPeriod = ref({ an: null, luna: null });
// Total sold din TOATE facturile filtrate (nu doar pagina curentă)
const totalSoldAll = ref(0);
const filters = ref({
company: null,
type: "CLIENTI", // CLIENTI or FURNIZORI
@@ -106,6 +108,9 @@ export const useInvoicesStore = defineStore("invoices", () => {
invoices.value = response.data.invoices || [];
pagination.value.totalRecords = response.data.total_count || 0;
// Store total sold from ALL filtered invoices (not just current page)
totalSoldAll.value = response.data.total_sold_all || 0;
// Store accounting period if available
if (response.data.accounting_period) {
accountingPeriod.value = response.data.accounting_period;
@@ -152,6 +157,7 @@ export const useInvoicesStore = defineStore("invoices", () => {
isLoading.value = false;
error.value = null;
accountingPeriod.value = { an: null, luna: null };
totalSoldAll.value = 0;
clearFilters();
pagination.value = {
page: 1,
@@ -170,6 +176,7 @@ export const useInvoicesStore = defineStore("invoices", () => {
isLoading,
error,
accountingPeriod,
totalSoldAll,
filters,
pagination,

View File

@@ -14,6 +14,11 @@ export const useTreasuryStore = defineStore("treasury", () => {
const totals = ref({
total_incasari: 0,
total_plati: 0,
// Totaluri din TOATE înregistrările filtrate (nu doar pagina curentă)
sold_precedent_all: 0,
total_incasari_all: 0,
total_plati_all: 0,
sold_final_all: 0,
});
const accountingPeriod = ref({ an: null, luna: null });
@@ -38,6 +43,11 @@ export const useTreasuryStore = defineStore("treasury", () => {
totals.value = {
total_incasari: response.data.total_incasari,
total_plati: response.data.total_plati,
// Totaluri din TOATE înregistrările filtrate (nu doar pagina curentă)
sold_precedent_all: response.data.sold_precedent_all || 0,
total_incasari_all: response.data.total_incasari_all || 0,
total_plati_all: response.data.total_plati_all || 0,
sold_final_all: response.data.sold_final_all || 0,
};
// Store accounting period if available

View File

@@ -11,6 +11,16 @@ export const useTrialBalanceStore = defineStore("trialBalance", () => {
const isLoading = ref(false);
const error = ref(null);
// Totaluri din TOATE înregistrările filtrate (nu doar pagina curentă)
const totals = ref({
total_sold_precedent_debit: 0,
total_sold_precedent_credit: 0,
total_rulaj_lunar_debit: 0,
total_rulaj_lunar_credit: 0,
total_sold_final_debit: 0,
total_sold_final_credit: 0,
});
const filters = ref({
luna: new Date().getMonth() + 1, // Current month (1-12)
an: new Date().getFullYear(), // Current year
@@ -83,6 +93,11 @@ export const useTrialBalanceStore = defineStore("trialBalance", () => {
totalPages: paginationData.total_pages,
};
// Store totals from ALL filtered records (not just current page)
if (response.data.data.totals) {
totals.value = response.data.data.totals;
}
return { success: true };
} else {
throw new Error("Invalid response format");
@@ -146,6 +161,14 @@ export const useTrialBalanceStore = defineStore("trialBalance", () => {
trialBalanceData.value = [];
isLoading.value = false;
error.value = null;
totals.value = {
total_sold_precedent_debit: 0,
total_sold_precedent_credit: 0,
total_rulaj_lunar_debit: 0,
total_rulaj_lunar_credit: 0,
total_sold_final_debit: 0,
total_sold_final_credit: 0,
};
filters.value = {
luna: new Date().getMonth() + 1,
an: new Date().getFullYear(),
@@ -169,6 +192,7 @@ export const useTrialBalanceStore = defineStore("trialBalance", () => {
trialBalanceData,
isLoading,
error,
totals,
filters,
pagination,
sorting,

View File

@@ -115,33 +115,34 @@
</Card>
<!-- Summary Stats - Compact, right aligned -->
<!-- Folosește totaluri din TOATE înregistrările (backend) nu doar pagina curentă -->
<div v-if="companyStore.selectedCompany" class="summary-stats-inline">
<div class="stat-item">
<span class="stat-label">Sold Precedent:</span>
<span
class="stat-value"
:class="computedTotals.soldPrecedent >= 0 ? 'incasari' : 'plati'"
>{{ formatCurrency(computedTotals.soldPrecedent) }}</span
:class="treasuryStore.totals.sold_precedent_all >= 0 ? 'incasari' : 'plati'"
>{{ formatCurrency(treasuryStore.totals.sold_precedent_all) }}</span
>
</div>
<div class="stat-item">
<span class="stat-label">Încasări:</span>
<span class="stat-value incasari">{{
formatCurrency(computedTotals.incasari)
formatCurrency(treasuryStore.totals.total_incasari_all)
}}</span>
</div>
<div class="stat-item">
<span class="stat-label">Plăți:</span>
<span class="stat-value plati">{{
formatCurrency(computedTotals.plati)
formatCurrency(treasuryStore.totals.total_plati_all)
}}</span>
</div>
<div class="stat-item">
<span class="stat-label">Sold Final:</span>
<span
class="stat-value"
:class="computedTotals.soldFinal >= 0 ? 'incasari' : 'plati'"
>{{ formatCurrency(computedTotals.soldFinal) }}</span
:class="treasuryStore.totals.sold_final_all >= 0 ? 'incasari' : 'plati'"
>{{ formatCurrency(treasuryStore.totals.sold_final_all) }}</span
>
</div>
</div>
@@ -325,34 +326,6 @@ const truncateText = (text, maxLength = 100) => {
return text.substring(0, maxLength) + "...";
};
// Computed totals - separate sold precedent from actual transactions
// Rows with dataact = null are opening balances (sold precedent)
const computedTotals = computed(() => {
let soldPrecedent = 0;
let incasari = 0;
let plati = 0;
treasuryStore.registers.forEach((row) => {
if (row.dataact === null || row.dataact === undefined) {
// Opening balance row - the sold value is the opening balance
soldPrecedent += parseFloat(row.sold) || 0;
} else {
// Transaction row
incasari += parseFloat(row.incasari) || 0;
plati += parseFloat(row.plati) || 0;
}
});
const soldFinal = soldPrecedent + incasari - plati;
return {
soldPrecedent,
incasari,
plati,
soldFinal,
};
});
// Check if current filter is a VALUTA type (to show Valuta column)
const isValutaType = computed(() => {
return (

View File

@@ -128,6 +128,17 @@
</template>
</Card>
<!-- Summary Stats - Compact, right aligned -->
<!-- Total sold din TOATE facturile filtrate (nu doar pagina curentă) -->
<div v-if="companyStore.selectedCompany && invoicesStore.hasInvoices" class="summary-stats-inline">
<div class="stat-item">
<span class="stat-label">Total Sold:</span>
<span class="stat-value" :class="invoicesStore.totalSoldAll > 0 ? 'plati' : 'incasari'">
{{ formatCurrency(invoicesStore.totalSoldAll) }}
</span>
</div>
</div>
<!-- Invoices Table -->
<Card v-if="companyStore.selectedCompany" class="table-card">
<template #content>
@@ -227,14 +238,6 @@
</template>
</Column>
</DataTable>
<!-- Total Sold -->
<div v-if="invoicesStore.hasInvoices" class="total-sold">
<span class="total-sold-label">Total Sold:</span>
<span class="total-sold-value">{{
formatCurrency(totalSold)
}}</span>
</div>
</template>
</Card>
</div>
@@ -272,12 +275,6 @@ const pagination = ref({
});
// Computed
const totalSold = computed(() => {
return invoicesStore.invoiceList.reduce((sum, invoice) => {
return sum + (parseFloat(invoice.soldfinal) || 0);
}, 0);
});
const accountingPeriodText = computed(() => {
// Use the global period store
return periodStore.selectedPeriod?.display_name || "";
@@ -739,29 +736,6 @@ watch(
display: block;
}
.total-sold {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 1rem;
padding: 1rem;
margin-top: 1rem;
border-top: 2px solid var(--surface-border);
background-color: var(--surface-50);
}
.total-sold-label {
font-weight: 600;
font-size: 1.1rem;
color: var(--text-color);
}
.total-sold-value {
font-weight: 700;
font-size: 1.3rem;
color: var(--primary-color);
}
/* Enhanced striped rows with better contrast - same as Trial Balance */
.table-card :deep(.p-datatable .p-datatable-tbody > tr) {
transition: background-color 0.2s ease;

View File

@@ -97,6 +97,35 @@
</template>
</Card>
<!-- Summary Totals - 2 rows (Debit/Credit) for visual balance verification -->
<!-- Totaluri din TOATE înregistrările filtrate (nu doar pagina curentă) -->
<div v-if="companyStore.selectedCompany && trialBalanceStore.hasData" class="totals-table-container">
<table class="totals-table">
<thead>
<tr>
<th></th>
<th>Sold Precedent</th>
<th>Rulaj Lunar</th>
<th>Sold Final</th>
</tr>
</thead>
<tbody>
<tr>
<td class="row-label">Debit</td>
<td class="numeric">{{ formatCurrency(trialBalanceStore.totals.total_sold_precedent_debit) }}</td>
<td class="numeric">{{ formatCurrency(trialBalanceStore.totals.total_rulaj_lunar_debit) }}</td>
<td class="numeric">{{ formatCurrency(trialBalanceStore.totals.total_sold_final_debit) }}</td>
</tr>
<tr>
<td class="row-label">Credit</td>
<td class="numeric">{{ formatCurrency(trialBalanceStore.totals.total_sold_precedent_credit) }}</td>
<td class="numeric">{{ formatCurrency(trialBalanceStore.totals.total_rulaj_lunar_credit) }}</td>
<td class="numeric">{{ formatCurrency(trialBalanceStore.totals.total_sold_final_credit) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Trial Balance Table -->
<Card v-if="companyStore.selectedCompany" class="table-card">
<template #content>
@@ -657,6 +686,49 @@ watch(
text-align: right;
}
/* Totals Table for Trial Balance - 2 rows (Debit/Credit) */
.totals-table-container {
margin-bottom: var(--space-lg);
display: flex;
justify-content: flex-end;
}
.totals-table {
border-collapse: collapse;
font-size: 0.9rem;
background: var(--surface-card);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.totals-table th,
.totals-table td {
padding: 0.5rem 1rem;
text-align: right;
}
.totals-table th {
background: var(--surface-100);
font-weight: 600;
color: var(--text-color-secondary);
}
.totals-table .row-label {
text-align: left;
font-weight: 600;
background: var(--surface-50);
}
.totals-table .numeric {
font-family: var(--font-mono, 'Roboto Mono', monospace);
font-variant-numeric: tabular-nums;
}
.totals-table tbody tr:first-child td {
border-bottom: 1px solid var(--surface-border);
}
/* Responsive */
@media (max-width: 768px) {
.trial-balance {