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:
12
CLAUDE.md
12
CLAUDE.md
@@ -189,6 +189,18 @@ ttl_your_data: int = int(os.getenv('CACHE_TTL_YOUR_DATA', '600')) # 10 min defa
|
||||
- ❌ Never use `:deep()` in components (use `src/assets/css/vendor/` for PrimeVue overrides)
|
||||
- ❌ Never duplicate CSS (write once, use everywhere)
|
||||
|
||||
**Tables - Unified Column Structure & Filter Buttons**:
|
||||
- ✅ **ALWAYS use separate columns** for related data (Debit | Credit, not Debit+Credit stacked)
|
||||
- ✅ **Use PrimeVue DataTable** with one value per `<Column>` component
|
||||
- ✅ **Add filter/action buttons** (clear, export Excel, export PDF, refresh) **on separate row below filters**
|
||||
- ✅ **PrimeVue Button** components with **icon + label** (not icon-only!)
|
||||
- ✅ **Export ALL data** from backend (page_size: 999999), not just current page
|
||||
- ❌ **Never group multiple values** vertically in a single column
|
||||
- ❌ **Never use HTML `<button>` for filter actions** (use PrimeVue `<Button>`)
|
||||
- ❌ **Never put action buttons on same row as filters** (separate row!)
|
||||
- ❌ **Never export only current page** (must fetch all data for export)
|
||||
- 📖 **See**: `CSS_PATTERNS.md` → Table Patterns → Unified Table Column Structure & Filter/Action Buttons
|
||||
|
||||
### Adding a New Telegram Bot Command
|
||||
**IMPORTANT**: Follow established command patterns and formatting.
|
||||
|
||||
|
||||
@@ -461,6 +461,158 @@ Drag-and-drop file upload area.
|
||||
- `.invoice-paid` - Light green for paid invoices
|
||||
- `.invoice-overdue` - Light red for overdue invoices
|
||||
|
||||
### ⚠️ Important: Unified Table Column Structure
|
||||
|
||||
**All tables in the application MUST follow this structure for consistency:**
|
||||
|
||||
✅ **DO: One value per column**
|
||||
```html
|
||||
<!-- CORRECT: Separate columns for related data -->
|
||||
<DataTable :value="data">
|
||||
<Column field="account" header="Account" sortable></Column>
|
||||
<Column field="debit" header="Debit" sortable>
|
||||
<template #body="slotProps">
|
||||
{{ formatCurrency(slotProps.data.debit) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="credit" header="Credit" sortable>
|
||||
<template #body="slotProps">
|
||||
{{ formatCurrency(slotProps.data.credit) }}
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
```
|
||||
|
||||
❌ **DON'T: Multiple values stacked vertically in single column**
|
||||
```html
|
||||
<!-- WRONG: Grouping debit/credit in one column -->
|
||||
<Column header="Balance">
|
||||
<template #body="slotProps">
|
||||
<div class="balance-group">
|
||||
<div>D: {{ slotProps.data.debit }}</div>
|
||||
<div>C: {{ slotProps.data.credit }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Maintains visual consistency across all views
|
||||
- Improves scannability and data comparison
|
||||
- Better for sorting and filtering
|
||||
- Follows established patterns (see InvoicesView, MaturityAndDetailsCard)
|
||||
|
||||
### Table Filter and Action Buttons
|
||||
|
||||
**Standard pattern: PrimeVue buttons with icon + label, separate row below filters**
|
||||
|
||||
All filter-related buttons (clear filters, export, refresh) MUST be:
|
||||
- ✅ **PrimeVue Button** components (not HTML `<button>`)
|
||||
- ✅ **Icon + label** (both icon and text visible)
|
||||
- ✅ **Separate row below filters** (not on same row with inputs)
|
||||
- ✅ **Standard PrimeVue styling** (outlined buttons with contextual colors)
|
||||
|
||||
```vue
|
||||
<div class="form">
|
||||
<div class="form-row">
|
||||
<!-- Filter inputs -->
|
||||
<div class="form-col">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Filter Name</label>
|
||||
<InputText v-model="filter" placeholder="Filter..." class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- More filter inputs... -->
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons (separate row below filters!) -->
|
||||
<div class="filters-actions">
|
||||
<Button
|
||||
icon="pi pi-filter-slash"
|
||||
label="Resetează Filtre"
|
||||
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="!hasData"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file-pdf"
|
||||
label="Export PDF"
|
||||
class="p-button-outlined p-button-danger"
|
||||
@click="exportPDF"
|
||||
:disabled="!hasData"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
label="Actualizează"
|
||||
:loading="isLoading"
|
||||
@click="refresh"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**CSS Styles:**
|
||||
|
||||
```css
|
||||
.filters-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.filters-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**CRITICAL: Export ALL Data, Not Just Current Page**
|
||||
|
||||
Export functions MUST fetch ALL data from the backend, not just the current page:
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG: Only exports current page
|
||||
const exportExcel = () => {
|
||||
const data = store.currentPageData; // Only current page!
|
||||
exportToExcel(data, 'filename');
|
||||
};
|
||||
|
||||
// ✅ CORRECT: Exports ALL data
|
||||
const exportExcel = async () => {
|
||||
// Fetch ALL data with large page_size or no pagination
|
||||
const params = {
|
||||
...filters,
|
||||
page: 1,
|
||||
page_size: 999999 // Get all data
|
||||
};
|
||||
|
||||
const response = await apiService.get('/endpoint', { params });
|
||||
const allData = response.data.items;
|
||||
|
||||
exportToExcel(allData, 'filename');
|
||||
|
||||
toast.add({
|
||||
summary: "Export reușit",
|
||||
detail: `${allData.length} înregistrări exportate`
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Financial reports (invoices, trial balance, etc.)
|
||||
- Detailed data tables with multiple columns
|
||||
- Any table with filters and pagination
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Patterns
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user