fix: Standardize Trial Balance table styling and add export functionality

- Refactor table columns from grouped (Debit+Credit vertical) to separate columns for better scannability
- Replace custom HTML buttons with PrimeVue Button components (icon + label)
- Move filter action buttons to separate row below filters (matches InvoicesView pattern)
- Add Excel and PDF export functionality that fetches ALL data (not just current page)
- Update CSS_PATTERNS.md with unified table column structure and filter button patterns
- Update CLAUDE.md with table styling requirements and anti-patterns

This ensures visual consistency across all table views in the application.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 01:50:41 +02:00
parent fff430acf0
commit 86900d7750
3 changed files with 386 additions and 59 deletions

View File

@@ -71,6 +71,20 @@
class="p-button-outlined p-button-secondary"
@click="clearFilters"
/>
<Button
icon="pi pi-file-excel"
label="Export Excel"
class="p-button-outlined p-button-success"
@click="exportExcel"
:disabled="!trialBalanceStore.hasData"
/>
<Button
icon="pi pi-file-pdf"
label="Export PDF"
class="p-button-outlined p-button-danger"
@click="exportPDF"
:disabled="!trialBalanceStore.hasData"
/>
<Button
icon="pi pi-refresh"
label="Actualizează"
@@ -113,50 +127,47 @@
</div>
</template>
<Column field="cont" header="Cont" sortable :style="{ width: '10%' }">
<Column field="cont" header="Cont" sortable :style="{ width: '8%' }">
<template #body="slotProps">
<strong>{{ slotProps.data.cont }}</strong>
</template>
</Column>
<Column field="denumire" header="Denumire Cont" sortable :style="{ width: '25%' }" />
<Column field="denumire" header="Denumire Cont" sortable :style="{ width: '20%' }" />
<Column header="Sold Precedent" :style="{ width: '15%' }">
<Column field="sold_precedent_debit" header="Sold Prec. D" sortable :style="{ width: '10%' }">
<template #body="slotProps">
<div class="balance-group">
<div class="balance-item balance-debit">
<small>D:</small> {{ formatCurrency(slotProps.data.sold_precedent_debit) }}
</div>
<div class="balance-item balance-credit">
<small>C:</small> {{ formatCurrency(slotProps.data.sold_precedent_credit) }}
</div>
</div>
{{ formatCurrency(slotProps.data.sold_precedent_debit) }}
</template>
</Column>
<Column header="Rulaj Lunar" :style="{ width: '15%' }">
<Column field="sold_precedent_credit" header="Sold Prec. C" sortable :style="{ width: '10%' }">
<template #body="slotProps">
<div class="balance-group">
<div class="balance-item balance-debit">
<small>D:</small> {{ formatCurrency(slotProps.data.rulaj_lunar_debit) }}
</div>
<div class="balance-item balance-credit">
<small>C:</small> {{ formatCurrency(slotProps.data.rulaj_lunar_credit) }}
</div>
</div>
{{ formatCurrency(slotProps.data.sold_precedent_credit) }}
</template>
</Column>
<Column header="Sold Final" :style="{ width: '15%' }">
<Column field="rulaj_lunar_debit" header="Rulaj D" sortable :style="{ width: '10%' }">
<template #body="slotProps">
<div class="balance-group">
<div class="balance-item balance-debit">
<small>D:</small> {{ formatCurrency(slotProps.data.sold_final_debit) }}
</div>
<div class="balance-item balance-credit">
<small>C:</small> {{ formatCurrency(slotProps.data.sold_final_credit) }}
</div>
</div>
{{ formatCurrency(slotProps.data.rulaj_lunar_debit) }}
</template>
</Column>
<Column field="rulaj_lunar_credit" header="Rulaj C" sortable :style="{ width: '10%' }">
<template #body="slotProps">
{{ formatCurrency(slotProps.data.rulaj_lunar_credit) }}
</template>
</Column>
<Column field="sold_final_debit" header="Sold Final D" sortable :style="{ width: '11%' }">
<template #body="slotProps">
{{ formatCurrency(slotProps.data.sold_final_debit) }}
</template>
</Column>
<Column field="sold_final_credit" header="Sold Final C" sortable :style="{ width: '11%' }">
<template #body="slotProps">
{{ formatCurrency(slotProps.data.sold_final_credit) }}
</template>
</Column>
</DataTable>
@@ -171,6 +182,7 @@ import { ref, computed, onMounted, watch } from "vue";
import { useToast } from "primevue/usetoast";
import { useCompanyStore } from "../stores/companies";
import { useTrialBalanceStore } from "../stores/trialBalance";
import { exportToExcel, exportToPDF } from "../utils/exportUtils";
const toast = useToast();
const companyStore = useCompanyStore();
@@ -297,6 +309,187 @@ const onSort = async (event) => {
);
};
// Export methods - Fetch ALL data (not just current page)
const fetchAllTrialBalanceData = async () => {
if (!companyStore.selectedCompany) return [];
try {
const params = {
company: companyStore.selectedCompany.id_firma,
luna: trialBalanceStore.filters.luna,
an: trialBalanceStore.filters.an,
page: 1,
page_size: 999999, // Get all data
sort_by: trialBalanceStore.sorting.sortBy,
sort_order: trialBalanceStore.sorting.sortOrder,
};
// Add optional filters
if (trialBalanceStore.filters.cont) {
params.cont_filter = trialBalanceStore.filters.cont;
}
if (trialBalanceStore.filters.denumire) {
params.denumire_filter = trialBalanceStore.filters.denumire;
}
const apiService = (await import("../services/api")).apiService;
const response = await apiService.get("/trial-balance/", { params });
if (response.data.success) {
return response.data.data.items || [];
}
return [];
} catch (error) {
console.error("Failed to fetch all trial balance data:", error);
return [];
}
};
const exportExcel = async () => {
if (!trialBalanceStore.hasData) {
toast.add({
severity: "warn",
summary: "Nu există date",
detail: "Nu există date de exportat",
life: 3000,
});
return;
}
// Fetch ALL data for export (not just current page)
toast.add({
severity: "info",
summary: "Se pregătește exportul",
detail: "Se încarcă toate datele...",
life: 2000,
});
const allData = await fetchAllTrialBalanceData();
if (allData.length === 0) {
toast.add({
severity: "error",
summary: "Eroare",
detail: "Nu s-au putut prelua datele pentru export",
life: 3000,
});
return;
}
// Prepare data for export
const exportData = allData.map((row) => ({
"Cont": row.cont,
"Denumire": row.denumire,
"Sold Precedent D": formatCurrency(row.sold_precedent_debit),
"Sold Precedent C": formatCurrency(row.sold_precedent_credit),
"Rulaj Lunar D": formatCurrency(row.rulaj_lunar_debit),
"Rulaj Lunar C": formatCurrency(row.rulaj_lunar_credit),
"Sold Final D": formatCurrency(row.sold_final_debit),
"Sold Final C": formatCurrency(row.sold_final_credit),
}));
const result = exportToExcel(
exportData,
`balanta_verificare_${currentPeriodText.value.replace(/\s+/g, "_")}`,
"Balanță de Verificare"
);
if (result.success) {
toast.add({
severity: "success",
summary: "Export reușit",
detail: `${allData.length} înregistrări exportate cu succes`,
life: 3000,
});
} else {
toast.add({
severity: "error",
summary: "Eroare la export",
detail: "Nu s-a putut genera fișierul Excel",
life: 3000,
});
}
};
const exportPDF = async () => {
if (!trialBalanceStore.hasData) {
toast.add({
severity: "warn",
summary: "Nu există date",
detail: "Nu există date de exportat",
life: 3000,
});
return;
}
// Fetch ALL data for export (not just current page)
toast.add({
severity: "info",
summary: "Se pregătește exportul",
detail: "Se încarcă toate datele...",
life: 2000,
});
const allData = await fetchAllTrialBalanceData();
if (allData.length === 0) {
toast.add({
severity: "error",
summary: "Eroare",
detail: "Nu s-au putut prelua datele pentru export",
life: 3000,
});
return;
}
// Prepare data for export
const exportData = allData.map((row) => ({
cont: row.cont,
denumire: row.denumire,
sold_precedent_debit: row.sold_precedent_debit,
sold_precedent_credit: row.sold_precedent_credit,
rulaj_lunar_debit: row.rulaj_lunar_debit,
rulaj_lunar_credit: row.rulaj_lunar_credit,
sold_final_debit: row.sold_final_debit,
sold_final_credit: row.sold_final_credit,
}));
// Define columns for PDF
const columns = [
{ field: "cont", header: "Cont", type: "text" },
{ field: "denumire", header: "Denumire", type: "text" },
{ field: "sold_precedent_debit", header: "Sold Prec. D", type: "currency" },
{ field: "sold_precedent_credit", header: "Sold Prec. C", type: "currency" },
{ field: "rulaj_lunar_debit", header: "Rulaj D", type: "currency" },
{ field: "rulaj_lunar_credit", header: "Rulaj C", type: "currency" },
{ field: "sold_final_debit", header: "Sold Final D", type: "currency" },
{ field: "sold_final_credit", header: "Sold Final C", type: "currency" },
];
const result = exportToPDF(
exportData,
columns,
`balanta_verificare_${currentPeriodText.value.replace(/\s+/g, "_")}`,
`Balanță de Verificare - ${currentPeriodText.value} - ${companyStore.selectedCompany?.firma || ""}`
);
if (result.success) {
toast.add({
severity: "success",
summary: "Export reușit",
detail: `${allData.length} înregistrări exportate cu succes`,
life: 3000,
});
} else {
toast.add({
severity: "error",
summary: "Eroare la export",
detail: "Nu s-a putut genera fișierul PDF",
life: 3000,
});
}
};
// Lifecycle
onMounted(async () => {
// Load companies if not loaded
@@ -369,32 +562,6 @@ watch(
margin-bottom: 2rem;
}
.balance-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.balance-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.balance-item small {
font-weight: 600;
min-width: 1.2rem;
}
.balance-debit {
color: var(--text-color);
}
.balance-credit {
color: var(--text-color-secondary);
}
.no-data,
.loading-table {
display: flex;
@@ -428,9 +595,5 @@ watch(
.filters-actions {
flex-direction: column;
}
.balance-group {
font-size: 0.85rem;
}
}
</style>