feat: Add Trial Balance (Balanță de Verificare) feature
Comprehensive implementation of Trial Balance page with filtering,
pagination, and sorting capabilities.
Backend Changes:
- Added Pydantic models for Trial Balance (trial_balance.py)
- TrialBalanceItem: Individual balance record
- TrialBalanceFilters: Filter parameters
- TrialBalancePagination: Pagination metadata
- TrialBalanceResponse: Complete API response
- Created FastAPI router (/api/trial-balance) with:
- Filtering by account number (cont) and description (denumire)
- Pagination support (configurable page size)
- Sorting on all columns (ascendent/descendent)
- Company-based access control via JWT
- Query against Oracle VBAL table
- Registered router in main.py
Frontend Changes:
- Created Pinia store (trialBalanceStore.js) with:
- State management for trial balance data
- Filters (luna, an, cont, denumire)
- Pagination controls
- Sorting functionality
- Error handling and loading states
- Built TrialBalanceView.vue component featuring:
- PrimeVue DataTable with responsive design
- Period display (month/year)
- Dual input filters (account number + description)
- Debounced search (500ms)
- Clear filters functionality
- Formatted currency display (Romanian locale)
- Balance columns (Debit/Credit) for:
- Sold Precedent (Previous Balance)
- Rulaj Lunar (Monthly Movement)
- Sold Final (Final Balance)
- Loading spinner and empty state
- Mobile-friendly responsive layout
- Added route: /trial-balance with auth guard
- Added menu item in HamburgerMenu (Navigation section)
- Icon: pi-calculator
- Label: "Balanță de Verificare"
Technical Details:
- Follows established CSS architecture (no :deep(), uses design tokens)
- Consistent with InvoicesView patterns
- Implements proper error handling
- Uses Oracle NVL for null value handling
- ROW_NUMBER pagination for Oracle compatibility
Testing: Manual testing required (Phase 5)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
191
reports-app/frontend/src/stores/trialBalance.js
Normal file
191
reports-app/frontend/src/stores/trialBalance.js
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Pinia Store for Trial Balance (Balanță de Verificare)
|
||||
*/
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { apiService } from "../services/api";
|
||||
|
||||
export const useTrialBalanceStore = defineStore("trialBalance", () => {
|
||||
// State
|
||||
const trialBalanceData = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
const filters = ref({
|
||||
luna: new Date().getMonth() + 1, // Current month (1-12)
|
||||
an: new Date().getFullYear(), // Current year
|
||||
cont: "",
|
||||
denumire: "",
|
||||
});
|
||||
|
||||
const pagination = ref({
|
||||
currentPage: 1,
|
||||
pageSize: 50,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
});
|
||||
|
||||
const sorting = ref({
|
||||
sortBy: "CONT",
|
||||
sortOrder: "asc",
|
||||
});
|
||||
|
||||
// Getters
|
||||
const hasData = computed(() => trialBalanceData.value.length > 0);
|
||||
|
||||
const currentPeriod = computed(() => {
|
||||
return {
|
||||
luna: filters.value.luna,
|
||||
an: filters.value.an,
|
||||
};
|
||||
});
|
||||
|
||||
// Actions
|
||||
const fetchTrialBalance = async (companyCode) => {
|
||||
if (!companyCode) {
|
||||
error.value = "Company code is required";
|
||||
return { success: false, error: error.value };
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
company: companyCode,
|
||||
luna: filters.value.luna,
|
||||
an: filters.value.an,
|
||||
page: pagination.value.currentPage,
|
||||
page_size: pagination.value.pageSize,
|
||||
sort_by: sorting.value.sortBy,
|
||||
sort_order: sorting.value.sortOrder,
|
||||
};
|
||||
|
||||
// Add optional filters
|
||||
if (filters.value.cont) {
|
||||
params.cont_filter = filters.value.cont;
|
||||
}
|
||||
if (filters.value.denumire) {
|
||||
params.denumire_filter = filters.value.denumire;
|
||||
}
|
||||
|
||||
const response = await apiService.get("/trial-balance/", { params });
|
||||
|
||||
if (response.data.success) {
|
||||
trialBalanceData.value = response.data.data.items || [];
|
||||
|
||||
// Update pagination
|
||||
const paginationData = response.data.data.pagination;
|
||||
pagination.value = {
|
||||
currentPage: paginationData.current_page,
|
||||
pageSize: paginationData.page_size,
|
||||
totalItems: paginationData.total_items,
|
||||
totalPages: paginationData.total_pages,
|
||||
};
|
||||
|
||||
return { success: true };
|
||||
} else {
|
||||
throw new Error("Invalid response format");
|
||||
}
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err.response?.data?.detail || "Failed to load trial balance data";
|
||||
console.error("Failed to load trial balance:", err);
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = async (newFilters, companyCode) => {
|
||||
filters.value = { ...filters.value, ...newFilters };
|
||||
pagination.value.currentPage = 1; // Reset to first page when filtering
|
||||
await fetchTrialBalance(companyCode);
|
||||
};
|
||||
|
||||
const clearFilters = async (companyCode) => {
|
||||
filters.value = {
|
||||
luna: new Date().getMonth() + 1,
|
||||
an: new Date().getFullYear(),
|
||||
cont: "",
|
||||
denumire: "",
|
||||
};
|
||||
pagination.value.currentPage = 1;
|
||||
await fetchTrialBalance(companyCode);
|
||||
};
|
||||
|
||||
const changePage = async (page, companyCode) => {
|
||||
pagination.value.currentPage = page;
|
||||
await fetchTrialBalance(companyCode);
|
||||
};
|
||||
|
||||
const changePageSize = async (pageSize, companyCode) => {
|
||||
pagination.value.pageSize = pageSize;
|
||||
pagination.value.currentPage = 1; // Reset to first page
|
||||
await fetchTrialBalance(companyCode);
|
||||
};
|
||||
|
||||
const sort = async (sortBy, sortOrder, companyCode) => {
|
||||
sorting.value = { sortBy, sortOrder };
|
||||
pagination.value.currentPage = 1; // Reset to first page when sorting
|
||||
await fetchTrialBalance(companyCode);
|
||||
};
|
||||
|
||||
const changePeriod = async (luna, an, companyCode) => {
|
||||
filters.value.luna = luna;
|
||||
filters.value.an = an;
|
||||
pagination.value.currentPage = 1;
|
||||
await fetchTrialBalance(companyCode);
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
trialBalanceData.value = [];
|
||||
isLoading.value = false;
|
||||
error.value = null;
|
||||
filters.value = {
|
||||
luna: new Date().getMonth() + 1,
|
||||
an: new Date().getFullYear(),
|
||||
cont: "",
|
||||
denumire: "",
|
||||
};
|
||||
pagination.value = {
|
||||
currentPage: 1,
|
||||
pageSize: 50,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
};
|
||||
sorting.value = {
|
||||
sortBy: "CONT",
|
||||
sortOrder: "asc",
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
trialBalanceData,
|
||||
isLoading,
|
||||
error,
|
||||
filters,
|
||||
pagination,
|
||||
sorting,
|
||||
|
||||
// Getters
|
||||
hasData,
|
||||
currentPeriod,
|
||||
|
||||
// Actions
|
||||
fetchTrialBalance,
|
||||
applyFilters,
|
||||
clearFilters,
|
||||
changePage,
|
||||
changePageSize,
|
||||
sort,
|
||||
changePeriod,
|
||||
clearError,
|
||||
reset,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user