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:
@@ -25,7 +25,7 @@ from auth.middleware import AuthenticationMiddleware
|
|||||||
# from auth.routes import create_auth_router # Fixed inline
|
# from auth.routes import create_auth_router # Fixed inline
|
||||||
|
|
||||||
# Import routere locale
|
# Import routere locale
|
||||||
from app.routers import invoices, dashboard, treasury, companies, telegram, cache
|
from app.routers import invoices, dashboard, treasury, companies, telegram, cache, trial_balance
|
||||||
|
|
||||||
# Auth endpoints pentru test
|
# Auth endpoints pentru test
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
@@ -317,6 +317,7 @@ app.include_router(dashboard.router, prefix="/api/dashboard", tags=["dashboard"]
|
|||||||
app.include_router(treasury.router, prefix="/api/treasury", tags=["treasury"])
|
app.include_router(treasury.router, prefix="/api/treasury", tags=["treasury"])
|
||||||
app.include_router(telegram.router, prefix="/api/telegram", tags=["telegram"])
|
app.include_router(telegram.router, prefix="/api/telegram", tags=["telegram"])
|
||||||
app.include_router(cache.router, prefix="/api", tags=["cache"])
|
app.include_router(cache.router, prefix="/api", tags=["cache"])
|
||||||
|
app.include_router(trial_balance.router, prefix="/api/trial-balance", tags=["trial-balance"])
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
|
|||||||
84
reports-app/backend/app/models/trial_balance.py
Normal file
84
reports-app/backend/app/models/trial_balance.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"""
|
||||||
|
Pydantic models for Trial Balance (Balanță de Verificare)
|
||||||
|
Maps to Oracle VBAL table
|
||||||
|
"""
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
class TrialBalanceItem(BaseModel):
|
||||||
|
"""
|
||||||
|
Individual trial balance record from VBAL table
|
||||||
|
"""
|
||||||
|
cont: str = Field(description="Număr cont contabil")
|
||||||
|
dcont: str = Field(description="Denumire cont")
|
||||||
|
sold_precedent_debit: Decimal = Field(description="Sold precedent debit", decimal_places=2)
|
||||||
|
sold_precedent_credit: Decimal = Field(description="Sold precedent credit", decimal_places=2)
|
||||||
|
rulaj_lunar_debit: Decimal = Field(description="Rulaj lunar debit", decimal_places=2)
|
||||||
|
rulaj_lunar_credit: Decimal = Field(description="Rulaj lunar credit", decimal_places=2)
|
||||||
|
sold_final_debit: Decimal = Field(description="Sold final debit", decimal_places=2)
|
||||||
|
sold_final_credit: Decimal = Field(description="Sold final credit", decimal_places=2)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class TrialBalanceFilters(BaseModel):
|
||||||
|
"""
|
||||||
|
Filters applied to trial balance data
|
||||||
|
"""
|
||||||
|
luna: int = Field(description="Luna (1-12)")
|
||||||
|
an: int = Field(description="An")
|
||||||
|
cont_filter: Optional[str] = Field(default=None, description="Filtru număr cont (partial match)")
|
||||||
|
denumire_filter: Optional[str] = Field(default=None, description="Filtru denumire cont (partial match, case-insensitive)")
|
||||||
|
|
||||||
|
|
||||||
|
class TrialBalancePagination(BaseModel):
|
||||||
|
"""
|
||||||
|
Pagination metadata
|
||||||
|
"""
|
||||||
|
total_items: int = Field(description="Total number of items")
|
||||||
|
total_pages: int = Field(description="Total number of pages")
|
||||||
|
current_page: int = Field(description="Current page number")
|
||||||
|
page_size: int = Field(description="Items per page")
|
||||||
|
|
||||||
|
|
||||||
|
class TrialBalanceResponse(BaseModel):
|
||||||
|
"""
|
||||||
|
Complete response for trial balance endpoint
|
||||||
|
"""
|
||||||
|
success: bool = Field(default=True, description="Request success status")
|
||||||
|
data: dict = Field(description="Trial balance data with items, pagination, and filters")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"cont": "4111",
|
||||||
|
"dcont": "Furnizori interni",
|
||||||
|
"sold_precedent_debit": 0.00,
|
||||||
|
"sold_precedent_credit": 15000.00,
|
||||||
|
"rulaj_lunar_debit": 5000.00,
|
||||||
|
"rulaj_lunar_credit": 8000.00,
|
||||||
|
"sold_final_debit": 0.00,
|
||||||
|
"sold_final_credit": 18000.00
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"total_items": 150,
|
||||||
|
"total_pages": 3,
|
||||||
|
"current_page": 1,
|
||||||
|
"page_size": 50
|
||||||
|
},
|
||||||
|
"filters_applied": {
|
||||||
|
"luna": 11,
|
||||||
|
"an": 2025,
|
||||||
|
"cont_filter": None,
|
||||||
|
"denumire_filter": "furnizori"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
reports-app/backend/app/routers/trial_balance.py
Normal file
202
reports-app/backend/app/routers/trial_balance.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
"""
|
||||||
|
API Router for Trial Balance (Balanță de Verificare)
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import math
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||||
|
|
||||||
|
from auth.dependencies import get_current_user
|
||||||
|
from auth.models import CurrentUser
|
||||||
|
from database.oracle_pool import oracle_pool
|
||||||
|
from ..models.trial_balance import (
|
||||||
|
TrialBalanceItem,
|
||||||
|
TrialBalanceFilters,
|
||||||
|
TrialBalancePagination,
|
||||||
|
TrialBalanceResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=TrialBalanceResponse)
|
||||||
|
async def get_trial_balance(
|
||||||
|
company: str = Query(description="Codul firmei (ID)"),
|
||||||
|
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna (1-12), default: luna curentă"),
|
||||||
|
an: Optional[int] = Query(None, ge=2000, le=2100, description="An, default: anul curent"),
|
||||||
|
cont_filter: Optional[str] = Query(None, description="Filtru număr cont (ex: '512', '4111')"),
|
||||||
|
denumire_filter: Optional[str] = Query(None, description="Filtru denumire cont (partial match, case-insensitive)"),
|
||||||
|
sort_by: str = Query("CONT", description="Coloană pentru sortare"),
|
||||||
|
sort_order: str = Query("asc", description="Ordinea sortării (asc | desc)"),
|
||||||
|
page: int = Query(1, ge=1, description="Pagina"),
|
||||||
|
page_size: int = Query(50, ge=1, le=500, description="Mărimea paginii"),
|
||||||
|
current_user: CurrentUser = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Obține balanța de verificare sintetică pentru o firmă
|
||||||
|
|
||||||
|
- Necesită autentificare JWT
|
||||||
|
- Utilizatorul trebuie să aibă acces la firma specificată
|
||||||
|
- Suportă filtrare după cont și denumire
|
||||||
|
- Suportă paginare și sortare
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Verifică dacă utilizatorul are acces la firma specificată
|
||||||
|
if company not in current_user.companies:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail=f"Nu aveți acces la firma {company}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Setează valorile implicite pentru lună și an (luna și anul curent)
|
||||||
|
current_date = date.today()
|
||||||
|
if luna is None:
|
||||||
|
luna = current_date.month
|
||||||
|
if an is None:
|
||||||
|
an = current_date.year
|
||||||
|
|
||||||
|
# Validează sort_order
|
||||||
|
if sort_order.lower() not in ['asc', 'desc']:
|
||||||
|
sort_order = 'asc'
|
||||||
|
|
||||||
|
# Validează sort_by (previne SQL injection)
|
||||||
|
valid_sort_columns = ['CONT', 'DCONT', 'SD_PREC', 'SC_PREC', 'RD_LUNA',
|
||||||
|
'RC_LUNA', 'SD_FINAL', 'SC_FINAL']
|
||||||
|
if sort_by.upper() not in valid_sort_columns:
|
||||||
|
sort_by = 'CONT'
|
||||||
|
|
||||||
|
# Obține datele din Oracle
|
||||||
|
async with oracle_pool.get_connection() as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# Obține schema din v_nom_firme
|
||||||
|
company_id = int(company)
|
||||||
|
schema_query = """
|
||||||
|
SELECT schema
|
||||||
|
FROM CONTAFIN_ORACLE.v_nom_firme
|
||||||
|
WHERE id_firma = :company_id
|
||||||
|
"""
|
||||||
|
cursor.execute(schema_query, {'company_id': company_id})
|
||||||
|
schema_result = cursor.fetchone()
|
||||||
|
|
||||||
|
if not schema_result:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Schema nu a fost găsită pentru firma {company}"
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = schema_result[0]
|
||||||
|
|
||||||
|
# Construiește query-ul de bază pentru VBAL
|
||||||
|
base_query = f"""
|
||||||
|
SELECT
|
||||||
|
CONT,
|
||||||
|
DCONT,
|
||||||
|
NVL(SD_PREC, 0) as SD_PREC,
|
||||||
|
NVL(SC_PREC, 0) as SC_PREC,
|
||||||
|
NVL(RD_LUNA, 0) as RD_LUNA,
|
||||||
|
NVL(RC_LUNA, 0) as RC_LUNA,
|
||||||
|
NVL(SD_FINAL, 0) as SD_FINAL,
|
||||||
|
NVL(SC_FINAL, 0) as SC_FINAL
|
||||||
|
FROM {schema}.VBAL
|
||||||
|
WHERE COD_FIRMA = :cod_firma
|
||||||
|
AND AN = :an
|
||||||
|
AND LUNA = :luna
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'cod_firma': company,
|
||||||
|
'an': an,
|
||||||
|
'luna': luna
|
||||||
|
}
|
||||||
|
|
||||||
|
# Adaugă filtre dinamice
|
||||||
|
if cont_filter:
|
||||||
|
base_query += " AND CONT LIKE :cont_filter"
|
||||||
|
params['cont_filter'] = f"{cont_filter}%"
|
||||||
|
|
||||||
|
if denumire_filter:
|
||||||
|
base_query += " AND UPPER(DCONT) LIKE UPPER(:denumire_filter)"
|
||||||
|
params['denumire_filter'] = f"%{denumire_filter}%"
|
||||||
|
|
||||||
|
# Count total pentru paginare
|
||||||
|
count_query = f"SELECT COUNT(*) FROM ({base_query})"
|
||||||
|
cursor.execute(count_query, params)
|
||||||
|
total_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# Adaugă sortare
|
||||||
|
base_query += f" ORDER BY {sort_by.upper()} {sort_order.upper()}"
|
||||||
|
|
||||||
|
# Paginare Oracle (ROW_NUMBER în loc de ROWNUM pentru a funcționa cu ORDER BY)
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
limit = offset + page_size
|
||||||
|
|
||||||
|
paginated_query = f"""
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT a.*, ROWNUM rnum FROM (
|
||||||
|
{base_query}
|
||||||
|
) a WHERE ROWNUM <= :limit
|
||||||
|
) WHERE rnum > :offset
|
||||||
|
"""
|
||||||
|
params['offset'] = offset
|
||||||
|
params['limit'] = limit
|
||||||
|
|
||||||
|
cursor.execute(paginated_query, params)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
# Procesează rezultatele
|
||||||
|
items = []
|
||||||
|
for row in rows:
|
||||||
|
item = TrialBalanceItem(
|
||||||
|
cont=row[0] or '',
|
||||||
|
dcont=row[1] or '',
|
||||||
|
sold_precedent_debit=Decimal(str(row[2] or 0)),
|
||||||
|
sold_precedent_credit=Decimal(str(row[3] or 0)),
|
||||||
|
rulaj_lunar_debit=Decimal(str(row[4] or 0)),
|
||||||
|
rulaj_lunar_credit=Decimal(str(row[5] or 0)),
|
||||||
|
sold_final_debit=Decimal(str(row[6] or 0)),
|
||||||
|
sold_final_credit=Decimal(str(row[7] or 0))
|
||||||
|
)
|
||||||
|
items.append(item.dict())
|
||||||
|
|
||||||
|
# Calculează paginarea
|
||||||
|
total_pages = math.ceil(total_count / page_size) if page_size > 0 else 0
|
||||||
|
|
||||||
|
# Construiește răspunsul
|
||||||
|
response_data = {
|
||||||
|
"items": items,
|
||||||
|
"pagination": {
|
||||||
|
"total_items": total_count,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"current_page": page,
|
||||||
|
"page_size": page_size
|
||||||
|
},
|
||||||
|
"filters_applied": {
|
||||||
|
"luna": luna,
|
||||||
|
"an": an,
|
||||||
|
"cont_filter": cont_filter,
|
||||||
|
"denumire_filter": denumire_filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrialBalanceResponse(
|
||||||
|
success=True,
|
||||||
|
data=response_data
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
# Re-raise HTTP exceptions
|
||||||
|
raise
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
# Log the error for debugging
|
||||||
|
import logging
|
||||||
|
logging.error(f"Error fetching trial balance: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Eroare la obținerea balanței de verificare: {str(e)}"
|
||||||
|
)
|
||||||
@@ -46,6 +46,17 @@
|
|||||||
<span>Bank & Cash</span>
|
<span>Bank & Cash</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="menu-item">
|
||||||
|
<router-link
|
||||||
|
to="/trial-balance"
|
||||||
|
class="menu-link"
|
||||||
|
:class="{ active: $route.name === 'TrialBalance' }"
|
||||||
|
@click="closeMenu"
|
||||||
|
>
|
||||||
|
<i class="menu-icon pi pi-calculator"></i>
|
||||||
|
<span>Balanță de Verificare</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import InvoicesView from "../views/InvoicesView.vue";
|
|||||||
import BankCashRegisterView from "../views/BankCashRegisterView.vue";
|
import BankCashRegisterView from "../views/BankCashRegisterView.vue";
|
||||||
import TelegramView from "../views/TelegramView.vue";
|
import TelegramView from "../views/TelegramView.vue";
|
||||||
import CacheStatsView from "../views/CacheStatsView.vue";
|
import CacheStatsView from "../views/CacheStatsView.vue";
|
||||||
|
import TrialBalanceView from "../views/TrialBalanceView.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -68,6 +69,15 @@ const routes = [
|
|||||||
title: "Cache Statistics - ROA Reports",
|
title: "Cache Statistics - ROA Reports",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/trial-balance",
|
||||||
|
name: "TrialBalance",
|
||||||
|
component: TrialBalanceView,
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
title: "Balanță de Verificare - ROA Reports",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
name: "NotFound",
|
name: "NotFound",
|
||||||
|
|||||||
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,
|
||||||
|
};
|
||||||
|
});
|
||||||
436
reports-app/frontend/src/views/TrialBalanceView.vue
Normal file
436
reports-app/frontend/src/views/TrialBalanceView.vue
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="trial-balance">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">
|
||||||
|
<i class="pi pi-calculator"></i>
|
||||||
|
Balanță de Verificare
|
||||||
|
</h1>
|
||||||
|
<p class="page-subtitle">
|
||||||
|
{{ currentPeriodText }} - {{ companyStore.selectedCompany?.firma || "Selectați companie" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Company Selection -->
|
||||||
|
<Card v-if="!companyStore.selectedCompany" class="company-selection-card">
|
||||||
|
<template #content>
|
||||||
|
<div class="company-selection">
|
||||||
|
<p class="text-color-secondary mb-3">
|
||||||
|
Selectați o companie pentru a vizualiza balanța de verificare:
|
||||||
|
</p>
|
||||||
|
<Dropdown
|
||||||
|
v-model="selectedCompanyId"
|
||||||
|
:options="companyStore.companyListFormatted"
|
||||||
|
option-label="displayName"
|
||||||
|
option-value="id_firma"
|
||||||
|
placeholder="Alegeți compania"
|
||||||
|
class="w-full"
|
||||||
|
@change="handleCompanyChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Filters Section -->
|
||||||
|
<Card v-if="companyStore.selectedCompany" class="filters-card">
|
||||||
|
<template #content>
|
||||||
|
<div class="form">
|
||||||
|
<div class="form-row">
|
||||||
|
<!-- Cont Filter -->
|
||||||
|
<div class="form-col">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Număr Cont</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFilters.cont"
|
||||||
|
placeholder="Ex: 512, 4111"
|
||||||
|
class="w-full"
|
||||||
|
@input="handleFilterChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Denumire Filter -->
|
||||||
|
<div class="form-col search-col">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Denumire Cont</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFilters.denumire"
|
||||||
|
placeholder="Căutare după denumire..."
|
||||||
|
class="w-full"
|
||||||
|
@input="handleSearchChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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-refresh"
|
||||||
|
label="Actualizează"
|
||||||
|
:loading="trialBalanceStore.isLoading"
|
||||||
|
@click="refreshData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Trial Balance Table -->
|
||||||
|
<Card v-if="companyStore.selectedCompany" class="table-card">
|
||||||
|
<template #content>
|
||||||
|
<DataTable
|
||||||
|
:value="trialBalanceStore.trialBalanceData"
|
||||||
|
:loading="trialBalanceStore.isLoading"
|
||||||
|
:paginator="true"
|
||||||
|
:rows="trialBalanceStore.pagination.pageSize"
|
||||||
|
:total-records="trialBalanceStore.pagination.totalItems"
|
||||||
|
:lazy="true"
|
||||||
|
paginator-template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
|
:rows-per-page-options="[25, 50, 100]"
|
||||||
|
current-page-report-template="Afișare {first} - {last} din {totalRecords} înregistrări"
|
||||||
|
responsive-layout="scroll"
|
||||||
|
@page="onPageChange"
|
||||||
|
@sort="onSort"
|
||||||
|
>
|
||||||
|
<template #empty>
|
||||||
|
<div class="no-data">
|
||||||
|
<i class="pi pi-info-circle"></i>
|
||||||
|
<p>Nu au fost găsite date pentru perioada selectată</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #loading>
|
||||||
|
<div class="loading-table">
|
||||||
|
<ProgressSpinner />
|
||||||
|
<p>Se încarcă balanța de verificare...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Column field="cont" header="Cont" sortable :style="{ width: '10%' }">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<strong>{{ slotProps.data.cont }}</strong>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="dcont" header="Denumire Cont" sortable :style="{ width: '25%' }" />
|
||||||
|
|
||||||
|
<Column header="Sold Precedent" :style="{ width: '15%' }">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column header="Rulaj Lunar" :style="{ width: '15%' }">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column header="Sold Final" :style="{ width: '15%' }">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from "vue";
|
||||||
|
import { useToast } from "primevue/usetoast";
|
||||||
|
import { useCompanyStore } from "../stores/companies";
|
||||||
|
import { useTrialBalanceStore } from "../stores/trialBalance";
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const companyStore = useCompanyStore();
|
||||||
|
const trialBalanceStore = useTrialBalanceStore();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null);
|
||||||
|
|
||||||
|
const localFilters = ref({
|
||||||
|
cont: "",
|
||||||
|
denumire: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const currentPeriodText = computed(() => {
|
||||||
|
const months = [
|
||||||
|
"Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie",
|
||||||
|
"Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie"
|
||||||
|
];
|
||||||
|
const monthName = months[trialBalanceStore.filters.luna - 1] || "";
|
||||||
|
return `${monthName} ${trialBalanceStore.filters.an}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const formatCurrency = (amount) => {
|
||||||
|
if (!amount || amount === 0) return "0,00";
|
||||||
|
return new Intl.NumberFormat("ro-RO", {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompanyChange = async () => {
|
||||||
|
if (!selectedCompanyId.value) return;
|
||||||
|
|
||||||
|
const company = companyStore.getCompanyById(selectedCompanyId.value);
|
||||||
|
if (company) {
|
||||||
|
companyStore.setSelectedCompany(company);
|
||||||
|
await loadTrialBalance();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = async () => {
|
||||||
|
await applyFilters();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (() => {
|
||||||
|
let timeout;
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(async () => {
|
||||||
|
await applyFilters();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const applyFilters = async () => {
|
||||||
|
if (!companyStore.selectedCompany) return;
|
||||||
|
|
||||||
|
await trialBalanceStore.applyFilters(
|
||||||
|
{
|
||||||
|
cont: localFilters.value.cont,
|
||||||
|
denumire: localFilters.value.denumire,
|
||||||
|
},
|
||||||
|
companyStore.selectedCompany.id_firma
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFilters = async () => {
|
||||||
|
localFilters.value = {
|
||||||
|
cont: "",
|
||||||
|
denumire: "",
|
||||||
|
};
|
||||||
|
await trialBalanceStore.clearFilters(companyStore.selectedCompany.id_firma);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
await loadTrialBalance();
|
||||||
|
toast.add({
|
||||||
|
severity: "success",
|
||||||
|
summary: "Actualizare reușită",
|
||||||
|
detail: "Balanța de verificare a fost actualizată cu succes",
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTrialBalance = async () => {
|
||||||
|
if (!companyStore.selectedCompany) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await trialBalanceStore.fetchTrialBalance(
|
||||||
|
companyStore.selectedCompany.id_firma
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load trial balance:", error);
|
||||||
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: "Eroare",
|
||||||
|
detail: "Nu s-a putut încărca balanța de verificare",
|
||||||
|
life: 5000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPageChange = async (event) => {
|
||||||
|
if (!companyStore.selectedCompany) return;
|
||||||
|
|
||||||
|
await trialBalanceStore.changePage(
|
||||||
|
event.page + 1,
|
||||||
|
companyStore.selectedCompany.id_firma
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSort = async (event) => {
|
||||||
|
if (!companyStore.selectedCompany) return;
|
||||||
|
|
||||||
|
const sortBy = event.sortField?.toUpperCase() || "CONT";
|
||||||
|
const sortOrder = event.sortOrder === 1 ? "asc" : "desc";
|
||||||
|
|
||||||
|
await trialBalanceStore.sort(
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
companyStore.selectedCompany.id_firma
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(async () => {
|
||||||
|
// Load companies if not loaded
|
||||||
|
if (!companyStore.hasCompanies) {
|
||||||
|
await companyStore.loadCompanies();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load trial balance if company is selected
|
||||||
|
if (companyStore.selectedCompany) {
|
||||||
|
await loadTrialBalance();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for company changes
|
||||||
|
watch(
|
||||||
|
() => companyStore.selectedCompany,
|
||||||
|
async (newCompany) => {
|
||||||
|
if (newCompany) {
|
||||||
|
await loadTrialBalance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trial-balance {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-selection-card,
|
||||||
|
.filters-card {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-col {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--surface-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
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;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data i,
|
||||||
|
.loading-table i {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.trial-balance {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-col {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-group {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user