diff --git a/CLAUDE.md b/CLAUDE.md index 30e7c88..7a56538 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -186,8 +186,11 @@ ttl_your_data: int = int(os.getenv('CACHE_TTL_YOUR_DATA', '600')) # 10 min defa **Golden Rules**: - ✅ Use global patterns first (`.roa-card`, `.roa-metric`, `.roa-badge-*`) - check `CSS_PATTERNS.md` - ✅ Use design tokens (`var(--color-primary)`) not hardcoded values (`#2563eb`) +- ✅ **Use shared CSS** from `src/assets/css/` - NEVER create new CSS when shared classes exist +- ✅ For inline stats/totals use `.summary-stats-inline`, `.stat-item`, `.stat-label`, `.stat-value` from `stats.css` - ❌ Never use `:deep()` in components (use `src/assets/css/vendor/` for PrimeVue overrides) - ❌ Never duplicate CSS (write once, use everywhere) +- ❌ Never add new scoped CSS for patterns that already exist in shared CSS files **Tables - Unified Column Structure & Filter Buttons**: - ✅ **ALWAYS use separate columns** for related data (Debit | Credit, not Debit+Credit stacked) diff --git a/reports-app/backend/app/models/invoice.py b/reports-app/backend/app/models/invoice.py index 4d57f73..844e509 100644 --- a/reports-app/backend/app/models/invoice.py +++ b/reports-app/backend/app/models/invoice.py @@ -64,6 +64,8 @@ class InvoiceListResponse(BaseModel): page_size: int has_more: bool accounting_period: Optional[dict] = Field(default=None, description="Perioada contabilă (an, luna)") + # Total sold din TOATE facturile filtrate (nu doar pagina curentă) + total_sold_all: Decimal = Field(default=Decimal('0.00'), description="Total sold din toate facturile filtrate") class InvoiceSummary(BaseModel): """Rezumat pentru facturi - pentru dashboard""" diff --git a/reports-app/backend/app/models/treasury.py b/reports-app/backend/app/models/treasury.py index 3208868..bc652fd 100644 --- a/reports-app/backend/app/models/treasury.py +++ b/reports-app/backend/app/models/treasury.py @@ -44,4 +44,9 @@ class RegisterListResponse(BaseModel): page: int page_size: int has_more: bool - accounting_period: Optional[AccountingPeriod] = None \ No newline at end of file + accounting_period: Optional[AccountingPeriod] = None + # Totaluri din TOATE înregistrările filtrate (nu doar pagina curentă) + sold_precedent_all: Decimal = Decimal('0.00') + total_incasari_all: Decimal = Decimal('0.00') + total_plati_all: Decimal = Decimal('0.00') + sold_final_all: Decimal = Decimal('0.00') \ No newline at end of file diff --git a/reports-app/backend/app/models/trial_balance.py b/reports-app/backend/app/models/trial_balance.py index 2e84b87..c370411 100644 --- a/reports-app/backend/app/models/trial_balance.py +++ b/reports-app/backend/app/models/trial_balance.py @@ -49,6 +49,18 @@ class TrialBalancePagination(BaseModel): page_size: int = Field(description="Items per page") +class TrialBalanceTotals(BaseModel): + """ + Totals for all 6 columns from all filtered records (not just current page) + """ + total_sold_precedent_debit: Decimal = Decimal('0.00') + total_sold_precedent_credit: Decimal = Decimal('0.00') + total_rulaj_lunar_debit: Decimal = Decimal('0.00') + total_rulaj_lunar_credit: Decimal = Decimal('0.00') + total_sold_final_debit: Decimal = Decimal('0.00') + total_sold_final_credit: Decimal = Decimal('0.00') + + class TrialBalanceResponse(BaseModel): """ Complete response for trial balance endpoint diff --git a/reports-app/backend/app/services/invoice_service.py b/reports-app/backend/app/services/invoice_service.py index fe6d134..e5b44f6 100644 --- a/reports-app/backend/app/services/invoice_service.py +++ b/reports-app/backend/app/services/invoice_service.py @@ -142,7 +142,16 @@ class InvoiceService: count_query = f"SELECT COUNT(*) FROM ({base_query})" cursor.execute(count_query, params) total_count = cursor.fetchone()[0] - + + # Query pentru TOTAL SOLD din TOATE facturile filtrate (nu doar pagina curentă) + total_sold_query = f""" + SELECT NVL(SUM(sold), 0) as total_sold + FROM ({base_query}) + """ + cursor.execute(total_sold_query, params) + total_sold_result = cursor.fetchone() + total_sold_all = Decimal(str(total_sold_result[0])) if total_sold_result else Decimal('0.00') + # Get accounting period - use params if provided, else from calendar if use_param_period: accounting_period = { @@ -226,7 +235,9 @@ class InvoiceService: page=filter_params.page, page_size=filter_params.page_size, has_more=len(invoices) == filter_params.page_size, - accounting_period=accounting_period + accounting_period=accounting_period, + # Total sold din TOATE facturile filtrate + total_sold_all=total_sold_all ) @staticmethod diff --git a/reports-app/backend/app/services/treasury_service.py b/reports-app/backend/app/services/treasury_service.py index 3cb3fab..9f46cc9 100644 --- a/reports-app/backend/app/services/treasury_service.py +++ b/reports-app/backend/app/services/treasury_service.py @@ -253,6 +253,60 @@ class TreasuryService: cursor.execute(count_plsql, count_params) total_count = total_count_var.getvalue() + # Query pentru TOTALURI din TOATE înregistrările filtrate (nu doar pagina curentă) + # sold_precedent = suma sold pentru rânduri cu dataact IS NULL + # total_incasari = suma incasari pentru rânduri cu dataact IS NOT NULL + # total_plati = suma plati pentru rânduri cu dataact IS NOT NULL + # Notă: where_clause poate fi gol sau poate conține "WHERE ..." + # Dacă e gol, adăugăm WHERE; dacă nu, adăugăm AND + dataact_null_cond = " AND dataact IS NULL" if where_clause else " WHERE dataact IS NULL" + dataact_not_null_cond = " AND dataact IS NOT NULL" if where_clause else " WHERE dataact IS NOT NULL" + + totals_plsql = f""" + DECLARE + v_an NUMBER; + v_luna NUMBER; + BEGIN + -- Obține anul și luna din parametri sau calendar + {period_select} + + {schema}.PACK_SESIUNE.SETAN(v_an); + {schema}.PACK_SESIUNE.SETLUNA(v_luna); + + -- Sold precedent: suma sold pentru rânduri fără dată (opening balance) + SELECT NVL(SUM(sold), 0) INTO :sold_precedent_all + FROM ({base_select}) sub{where_clause}{dataact_null_cond}; + + -- Total încasări: suma incasari pentru rânduri cu dată (transactions) + SELECT NVL(SUM(incasari), 0) INTO :total_incasari_all + FROM ({base_select}) sub{where_clause}{dataact_not_null_cond}; + + -- Total plăți: suma plati pentru rânduri cu dată (transactions) + SELECT NVL(SUM(plati), 0) INTO :total_plati_all + FROM ({base_select}) sub{where_clause}{dataact_not_null_cond}; + END; + """ + + sold_precedent_all_var = cursor.var(oracledb.NUMBER) + total_incasari_all_var = cursor.var(oracledb.NUMBER) + total_plati_all_var = cursor.var(oracledb.NUMBER) + + totals_params = { + 'sold_precedent_all': sold_precedent_all_var, + 'total_incasari_all': total_incasari_all_var, + 'total_plati_all': total_plati_all_var + } + if use_param_period: + totals_params['param_an'] = filter_params.an + totals_params['param_luna'] = filter_params.luna + + cursor.execute(totals_plsql, totals_params) + + sold_precedent_all = Decimal(str(sold_precedent_all_var.getvalue() or 0)) + total_incasari_all = Decimal(str(total_incasari_all_var.getvalue() or 0)) + total_plati_all = Decimal(str(total_plati_all_var.getvalue() or 0)) + sold_final_all = sold_precedent_all + total_incasari_all - total_plati_all + # Procesare rezultate registers = [] total_incasari = Decimal('0.00') @@ -288,7 +342,12 @@ class TreasuryService: page=filter_params.page, page_size=filter_params.page_size, has_more=len(registers) == filter_params.page_size, - accounting_period=AccountingPeriod(an=accounting_year, luna=accounting_month) + accounting_period=AccountingPeriod(an=accounting_year, luna=accounting_month), + # Totaluri din TOATE înregistrările filtrate + sold_precedent_all=sold_precedent_all, + total_incasari_all=total_incasari_all, + total_plati_all=total_plati_all, + sold_final_all=sold_final_all ) @staticmethod diff --git a/reports-app/backend/app/services/trial_balance_service.py b/reports-app/backend/app/services/trial_balance_service.py index a33667f..7fd8132 100644 --- a/reports-app/backend/app/services/trial_balance_service.py +++ b/reports-app/backend/app/services/trial_balance_service.py @@ -135,6 +135,29 @@ class TrialBalanceService: cursor.execute(count_query, params) total_count = cursor.fetchone()[0] + # Query pentru TOTALURI din TOATE înregistrările filtrate (nu doar pagina curentă) + totals_query = f""" + SELECT + NVL(SUM(PRECDEB), 0) as total_prec_deb, + NVL(SUM(PRECCRED), 0) as total_prec_cred, + NVL(SUM(RULDEB), 0) as total_rul_deb, + NVL(SUM(RULCRED), 0) as total_rul_cred, + NVL(SUM(SOLDDEB), 0) as total_sold_deb, + NVL(SUM(SOLDCRED), 0) as total_sold_cred + FROM ({base_query}) + """ + cursor.execute(totals_query, params) + totals_row = cursor.fetchone() + + totals = { + "total_sold_precedent_debit": Decimal(str(totals_row[0])) if totals_row else Decimal('0.00'), + "total_sold_precedent_credit": Decimal(str(totals_row[1])) if totals_row else Decimal('0.00'), + "total_rulaj_lunar_debit": Decimal(str(totals_row[2])) if totals_row else Decimal('0.00'), + "total_rulaj_lunar_credit": Decimal(str(totals_row[3])) if totals_row else Decimal('0.00'), + "total_sold_final_debit": Decimal(str(totals_row[4])) if totals_row else Decimal('0.00'), + "total_sold_final_credit": Decimal(str(totals_row[5])) if totals_row else Decimal('0.00') + } + # Add sorting base_query += f" ORDER BY {sort_by.upper()} {sort_order.upper()}" @@ -189,5 +212,7 @@ class TrialBalanceService: "an": an, "cont_filter": cont_filter, "denumire_filter": denumire_filter - } + }, + # Totaluri din TOATE înregistrările filtrate (nu doar pagina curentă) + "totals": totals } diff --git a/reports-app/frontend/src/stores/invoices.js b/reports-app/frontend/src/stores/invoices.js index f240295..3dbf7f1 100644 --- a/reports-app/frontend/src/stores/invoices.js +++ b/reports-app/frontend/src/stores/invoices.js @@ -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, diff --git a/reports-app/frontend/src/stores/treasury.js b/reports-app/frontend/src/stores/treasury.js index e1b9be7..5c21388 100644 --- a/reports-app/frontend/src/stores/treasury.js +++ b/reports-app/frontend/src/stores/treasury.js @@ -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 diff --git a/reports-app/frontend/src/stores/trialBalance.js b/reports-app/frontend/src/stores/trialBalance.js index c406ca9..d252234 100644 --- a/reports-app/frontend/src/stores/trialBalance.js +++ b/reports-app/frontend/src/stores/trialBalance.js @@ -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, diff --git a/reports-app/frontend/src/utils/exportUtils.js b/reports-app/frontend/src/utils/exportUtils.js index b4641a2..72281d4 100644 --- a/reports-app/frontend/src/utils/exportUtils.js +++ b/reports-app/frontend/src/utils/exportUtils.js @@ -121,11 +121,16 @@ export const exportToPDF = (data, columns, filename, header) => { const tableColumns = columns.map((col) => col.header); const totalRowIndices = new Set(); // Track which rows are totals + const grandTotalRowIndices = new Set(); // Track grand total rows + const tableRows = data.map((row, rowIndex) => { // Track total rows for special styling if (row._isTotal) { totalRowIndices.add(rowIndex); } + if (row._isGrandTotal) { + grandTotalRowIndices.add(rowIndex); + } return columns.map((col) => { const value = row[col.field]; @@ -180,8 +185,8 @@ export const exportToPDF = (data, columns, filename, header) => { const defaultWidths = { 0: totalWidth * 0.07, // Cont: ~20mm 1: totalWidth * 0.33, // Denumire: ~93mm - 2: totalWidth * 0.1, // Sold Prec D: ~28mm - 3: totalWidth * 0.1, // Sold Prec C: ~28mm + 2: totalWidth * 0.1, // Sume Prec D: ~28mm + 3: totalWidth * 0.1, // Sume Prec C: ~28mm 4: totalWidth * 0.1, // Rulaj D: ~28mm 5: totalWidth * 0.1, // Rulaj C: ~28mm 6: totalWidth * 0.1, // Sold Final D: ~28mm @@ -313,10 +318,16 @@ export const exportToPDF = (data, columns, filename, header) => { const colIndex = data.column.index; const column = columns[colIndex]; - // Style total rows differently (bold, light gray background) - if (totalRowIndices.has(rowIndex)) { + // Style grand total rows (bold, darker gray background) + if (grandTotalRowIndices.has(rowIndex)) { data.cell.styles.fontStyle = "bold"; - data.cell.styles.fillColor = [230, 230, 230]; // Light gray + data.cell.styles.fillColor = [200, 200, 200]; // Darker gray + data.cell.styles.fontSize = 10; + } + // Style class total rows (bold, light gray background) + else if (totalRowIndices.has(rowIndex)) { + data.cell.styles.fontStyle = "bold"; + data.cell.styles.fillColor = [235, 235, 235]; // Light gray } if (column) { @@ -332,6 +343,15 @@ export const exportToPDF = (data, columns, filename, header) => { } } }, + willDrawCell: function (data) { + // Draw double line above grand total row + if (data.section === "body" && grandTotalRowIndices.has(data.row.index)) { + const doc = data.doc; + doc.setDrawColor(100, 100, 100); + doc.setLineWidth(0.5); + doc.line(data.cell.x, data.cell.y, data.cell.x + data.cell.width, data.cell.y); + } + }, }); // Add footer to all pages AFTER table generation diff --git a/reports-app/frontend/src/views/BankCashRegisterView.vue b/reports-app/frontend/src/views/BankCashRegisterView.vue index c0871b0..6ac9276 100644 --- a/reports-app/frontend/src/views/BankCashRegisterView.vue +++ b/reports-app/frontend/src/views/BankCashRegisterView.vue @@ -115,33 +115,34 @@ +
Sold Precedent: {{ formatCurrency(computedTotals.soldPrecedent) }}{{ formatCurrency(treasuryStore.totals.sold_precedent_all) }}
Încasări: {{ - formatCurrency(computedTotals.incasari) + formatCurrency(treasuryStore.totals.total_incasari_all) }}
Plăți: {{ - formatCurrency(computedTotals.plati) + formatCurrency(treasuryStore.totals.total_plati_all) }}
Sold Final: {{ formatCurrency(computedTotals.soldFinal) }}{{ formatCurrency(treasuryStore.totals.sold_final_all) }}
@@ -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 ( diff --git a/reports-app/frontend/src/views/InvoicesView.vue b/reports-app/frontend/src/views/InvoicesView.vue index 607d97f..5de2ca0 100644 --- a/reports-app/frontend/src/views/InvoicesView.vue +++ b/reports-app/frontend/src/views/InvoicesView.vue @@ -128,6 +128,17 @@ + + +
+
+ Total Sold: + + {{ formatCurrency(invoicesStore.totalSoldAll) }} + +
+
+ - - -
- Total Sold: - {{ - formatCurrency(totalSold) - }} -
@@ -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; diff --git a/reports-app/frontend/src/views/TrialBalanceView.vue b/reports-app/frontend/src/views/TrialBalanceView.vue index 6160a62..4839076 100644 --- a/reports-app/frontend/src/views/TrialBalanceView.vue +++ b/reports-app/frontend/src/views/TrialBalanceView.vue @@ -97,6 +97,35 @@ + + +
+
+ Sume Prec. D: + {{ formatCurrency(trialBalanceStore.totals.total_sold_precedent_debit) }} +
+
+ Sume Prec. C: + {{ formatCurrency(trialBalanceStore.totals.total_sold_precedent_credit) }} +
+
+ Rulaj D: + {{ formatCurrency(trialBalanceStore.totals.total_rulaj_lunar_debit) }} +
+
+ Rulaj C: + {{ formatCurrency(trialBalanceStore.totals.total_rulaj_lunar_credit) }} +
+
+ Sold Final D: + {{ formatCurrency(trialBalanceStore.totals.total_sold_final_debit) }} +
+
+ Sold Final C: + {{ formatCurrency(trialBalanceStore.totals.total_sold_final_credit) }} +
+
+