From c75e896a84d3270c23d3e492eb76b56dbbad9d6b Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Tue, 9 Dec 2025 12:14:35 +0200 Subject: [PATCH] feat: Add accounting period selector for all views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PeriodSelectorMini component for global period selection - Add accountingPeriod store for shared period state - Add calendar service/router/model for available periods API - Update Dashboard, Invoices, Trial Balance, Bank/Cash Register views to respect selected period - Fix Trial Balance navigation sync bug (period now syncs on mount) - Update backend services to accept luna/an parameters 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- reports-app/backend/app/cache/config.py | 3 + reports-app/backend/app/main.py | 3 +- reports-app/backend/app/models/calendar.py | 19 + reports-app/backend/app/models/invoice.py | 4 +- reports-app/backend/app/models/treasury.py | 2 + reports-app/backend/app/routers/calendar.py | 33 ++ reports-app/backend/app/routers/dashboard.py | 38 +- reports-app/backend/app/routers/invoices.py | 34 +- reports-app/backend/app/routers/treasury.py | 4 + .../backend/app/services/calendar_service.py | 78 ++++ .../backend/app/services/dashboard_service.py | 281 +++++++++----- .../backend/app/services/invoice_service.py | 60 +-- .../backend/app/services/treasury_service.py | 42 +- .../dashboard/PeriodSelectorMini.vue | 366 ++++++++++++++++++ .../cards/MaturityAndDetailsCard.vue | 27 ++ .../src/components/layout/DashboardHeader.vue | 15 +- .../frontend/src/stores/accountingPeriod.js | 138 +++++++ reports-app/frontend/src/stores/dashboard.js | 62 +-- .../src/views/BankCashRegisterView.vue | 122 ++---- .../frontend/src/views/DashboardView.vue | 79 +++- .../frontend/src/views/InvoicesView.vue | 135 ++----- .../frontend/src/views/TrialBalanceView.vue | 42 +- 22 files changed, 1162 insertions(+), 425 deletions(-) create mode 100644 reports-app/backend/app/models/calendar.py create mode 100644 reports-app/backend/app/routers/calendar.py create mode 100644 reports-app/backend/app/services/calendar_service.py create mode 100644 reports-app/frontend/src/components/dashboard/PeriodSelectorMini.vue create mode 100644 reports-app/frontend/src/stores/accountingPeriod.js diff --git a/reports-app/backend/app/cache/config.py b/reports-app/backend/app/cache/config.py index 8d88a11..08e6257 100644 --- a/reports-app/backend/app/cache/config.py +++ b/reports-app/backend/app/cache/config.py @@ -26,6 +26,7 @@ class CacheConfig: ttl_invoices_summary: int ttl_treasury: int ttl_trial_balance: int + ttl_calendar_periods: int # Maintenance cleanup_interval: int @@ -58,6 +59,7 @@ class CacheConfig: ttl_invoices_summary=int(os.getenv('CACHE_TTL_INVOICES_SUMMARY', '900')), ttl_treasury=int(os.getenv('CACHE_TTL_TREASURY', '600')), ttl_trial_balance=int(os.getenv('CACHE_TTL_TRIAL_BALANCE', '600')), + ttl_calendar_periods=int(os.getenv('CACHE_TTL_CALENDAR_PERIODS', '3600')), # Maintenance cleanup_interval=int(os.getenv('CACHE_CLEANUP_INTERVAL', '3600')), @@ -82,5 +84,6 @@ class CacheConfig: 'invoices_summary': self.ttl_invoices_summary, 'treasury': self.ttl_treasury, 'trial_balance': self.ttl_trial_balance, + 'calendar_periods': self.ttl_calendar_periods, } return ttl_map.get(cache_type, self.default_ttl) diff --git a/reports-app/backend/app/main.py b/reports-app/backend/app/main.py index c9f261f..64ffd6c 100644 --- a/reports-app/backend/app/main.py +++ b/reports-app/backend/app/main.py @@ -25,7 +25,7 @@ from auth.middleware import AuthenticationMiddleware # from auth.routes import create_auth_router # Fixed inline # Import routere locale -from app.routers import invoices, dashboard, treasury, companies, telegram, cache, trial_balance +from app.routers import invoices, dashboard, treasury, companies, telegram, cache, trial_balance, calendar # Auth endpoints pentru test from fastapi import APIRouter, HTTPException @@ -318,6 +318,7 @@ app.include_router(treasury.router, prefix="/api/treasury", tags=["treasury"]) app.include_router(telegram.router, prefix="/api/telegram", tags=["telegram"]) app.include_router(cache.router, prefix="/api", tags=["cache"]) app.include_router(trial_balance.router, prefix="/api/trial-balance", tags=["trial-balance"]) +app.include_router(calendar.router, prefix="/api/calendar", tags=["calendar"]) @app.get("/") async def root(): diff --git a/reports-app/backend/app/models/calendar.py b/reports-app/backend/app/models/calendar.py new file mode 100644 index 0000000..6b35f88 --- /dev/null +++ b/reports-app/backend/app/models/calendar.py @@ -0,0 +1,19 @@ +""" +Calendar period models for accounting period selector +""" +from pydantic import BaseModel +from typing import List, Optional + + +class CalendarPeriod(BaseModel): + """Model for an accounting period""" + an: int # Year + luna: int # Month (1-12) + display_name: str # Format: "Decembrie 2025" + + +class CalendarPeriodsResponse(BaseModel): + """Response model for calendar periods list""" + periods: List[CalendarPeriod] + current_period: Optional[CalendarPeriod] = None # Most recent period + total_count: int diff --git a/reports-app/backend/app/models/invoice.py b/reports-app/backend/app/models/invoice.py index 7059b76..4d57f73 100644 --- a/reports-app/backend/app/models/invoice.py +++ b/reports-app/backend/app/models/invoice.py @@ -44,8 +44,8 @@ class InvoiceFilter(BaseModel): """Filtru pentru căutarea facturilor""" company: str = Field(description="Codul firmei (schema Oracle)") partner_type: Literal["CLIENTI", "FURNIZORI"] = Field(description="Tipul partenerului") - date_from: Optional[date] = Field(description="Data de început") - date_to: Optional[date] = Field(description="Data de sfârșit") + luna: Optional[int] = Field(default=None, ge=1, le=12, description="Luna contabilă (1-12)") + an: Optional[int] = Field(default=None, ge=2000, le=2100, description="Anul contabil") partner_name: Optional[str] = Field(description="Filtru după nume") cont: Optional[str] = Field(description="Filtru după cont contabil") only_unpaid: bool = Field(default=True, description="Doar neachitate") diff --git a/reports-app/backend/app/models/treasury.py b/reports-app/backend/app/models/treasury.py index c1e8b2e..3208868 100644 --- a/reports-app/backend/app/models/treasury.py +++ b/reports-app/backend/app/models/treasury.py @@ -25,6 +25,8 @@ class RegisterFilter(BaseModel): """Filtre pentru registrul de casă și bancă""" company: str register_type: Optional[str] = None # BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA sau None pentru toate + luna: Optional[int] = None # Luna contabilă (1-12) pentru PACK_SESIUNE + an: Optional[int] = None # Anul contabil pentru PACK_SESIUNE date_from: Optional[datetime] = None date_to: Optional[datetime] = None partner_name: Optional[str] = None diff --git a/reports-app/backend/app/routers/calendar.py b/reports-app/backend/app/routers/calendar.py new file mode 100644 index 0000000..4153a21 --- /dev/null +++ b/reports-app/backend/app/routers/calendar.py @@ -0,0 +1,33 @@ +""" +API Router for calendar/accounting periods +""" +from fastapi import APIRouter, Depends, HTTPException, Query +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) + +from auth.dependencies import get_current_user +from auth.models import CurrentUser +from ..models.calendar import CalendarPeriodsResponse +from ..services.calendar_service import CalendarService + +router = APIRouter(redirect_slashes=False) + + +@router.get("/periods", response_model=CalendarPeriodsResponse) +async def get_calendar_periods( + company: int = Query(..., description="Company ID"), + current_user: CurrentUser = Depends(get_current_user) +) -> CalendarPeriodsResponse: + """ + Get available accounting periods for a company. + Returns periods ordered by year DESC, month DESC with Romanian month names. + """ + # Validate company access + if str(company) not in current_user.companies: + raise HTTPException( + status_code=403, + detail=f"Nu aveți acces la firma {company}" + ) + + return await CalendarService.get_available_periods(company) diff --git a/reports-app/backend/app/routers/dashboard.py b/reports-app/backend/app/routers/dashboard.py index 9d18d70..8a55f7c 100644 --- a/reports-app/backend/app/routers/dashboard.py +++ b/reports-app/backend/app/routers/dashboard.py @@ -18,6 +18,8 @@ router = APIRouter() async def get_dashboard_summary( request: Request, company: str = Query(description="Codul firmei"), + luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"), + an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"), current_user: CurrentUser = Depends(get_current_user) ): """ @@ -26,13 +28,14 @@ async def get_dashboard_summary( - Necesită autentificare JWT - Returnează statistici clienți/furnizori și trezorerie - Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header) + - Suportă filtrare pe luna/an contabil (dacă nu sunt specificate, folosește ultima perioadă) """ 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}") - result = await DashboardService.get_complete_summary(company, current_user.username, request=request) + result = await DashboardService.get_complete_summary(company, current_user.username, luna=luna, an=an, request=request) # Convert Pydantic model to dict for JSON serialization result_dict = result.dict() if hasattr(result, 'dict') else result @@ -60,6 +63,8 @@ async def get_dashboard_trends( request: Request, company: str = Query(description="Codul firmei"), period: str = Query(default="30d", description="Perioada pentru trends: 7d, 30d, ytd, 12m"), + luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"), + an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"), compare_previous: bool = Query(default=True, description="Compară cu perioada anterioară"), current_user: CurrentUser = Depends(get_current_user) ): @@ -67,6 +72,7 @@ async def get_dashboard_trends( Obține trenduri pentru indicatorii principali (clienți/furnizori) - period: "7d" (7 zile), "30d" (30 zile), "ytd" (year to date), "12m" (12 luni) + - luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă) - compare_previous: dacă să compare cu perioada anterioară - Necesită autentificare JWT - Returnează date pentru grafice de trenduri @@ -85,7 +91,7 @@ async def get_dashboard_trends( ) # Obține datele de trenduri - result = await DashboardService.get_trends(int(company), period, request=request) + result = await DashboardService.get_trends(int(company), period, luna=luna, an=an, request=request) # Convert to dict if needed result_dict = result.dict() if hasattr(result, 'dict') else result @@ -115,6 +121,8 @@ async def get_dashboard_trends( async def get_detailed_data( company: str = Query(description="Codul firmei"), data_type: str = Query(description="Tipul de date: clients, suppliers, treasury"), + luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"), + an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"), page: int = Query(default=1, ge=1), page_size: int = Query(default=25, ge=1, le=100), search: str = Query(default="", description="Termen de căutare"), @@ -132,6 +140,8 @@ async def get_detailed_data( result = await DashboardService.get_detailed_data( company=company, data_type=data_type, + luna=luna, + an=an, page=page, page_size=page_size, search=search @@ -225,6 +235,8 @@ async def get_maturity_analysis( request: Request, company: int = Query(..., description="ID-ul firmei"), period: str = Query("7d", regex="^(7d|1m|3m|6m|12m|all)$", description="Orizont de planificare pentru analiza scadențelor"), + luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"), + an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"), current_user: CurrentUser = Depends(get_current_user) ): """ @@ -232,6 +244,7 @@ async def get_maturity_analysis( - Necesită autentificare JWT - Logică: Include TOATE restanțele + scadențele viitoare din perioada selectată + - luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă) - Perioade disponibile: * 7d: Toate restanțele + scadențe următoarelor 7 zile * 1m: Toate restanțele + scadențe următoarelor 30 zile @@ -249,7 +262,7 @@ async def get_maturity_analysis( if str(company) not in current_user.companies: raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}") - result = await DashboardService.get_maturity_analysis(company, period, request=request) + result = await DashboardService.get_maturity_analysis(company, period, luna=luna, an=an, request=request) # Convert to dict if needed result_dict = result.dict() if hasattr(result, 'dict') else result @@ -276,20 +289,23 @@ async def get_maturity_analysis( @router.get("/monthly-flows") async def get_monthly_flows( company: int = Query(..., description="ID-ul firmei"), + luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"), + an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"), current_user: CurrentUser = Depends(get_current_user) ): """ Returnează fluxurile lunare pentru firma selectată - + - Necesită autentificare JWT - Returnează date pentru analiza fluxurilor lunare + - luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă) """ try: # Verifică dacă utilizatorul are acces la firma specificată if str(company) not in current_user.companies: raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}") - - result = await DashboardService.get_monthly_flows(company) + + result = await DashboardService.get_monthly_flows(company, luna=luna, an=an) return result except ValueError as e: @@ -302,6 +318,8 @@ async def get_monthly_flows( async def get_treasury_breakdown( request: Request, company: int = Query(..., description="ID-ul firmei"), + luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"), + an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"), current_user: CurrentUser = Depends(get_current_user) ): """ @@ -309,6 +327,7 @@ async def get_treasury_breakdown( - Necesită autentificare JWT - Returnează distribuția soldurilor pe conturi și tipuri + - luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă) - Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header) """ try: @@ -316,7 +335,7 @@ async def get_treasury_breakdown( if str(company) not in current_user.companies: raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}") - result = await DashboardService.get_treasury_breakdown(company, request=request) + result = await DashboardService.get_treasury_breakdown(company, luna=luna, an=an, request=request) # Convert to dict if needed result_dict = result.dict() if hasattr(result, 'dict') else result @@ -344,6 +363,8 @@ async def get_treasury_breakdown( async def get_net_balance_breakdown( request: Request, company: int = Query(..., description="ID-ul firmei"), + luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"), + an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"), current_user: CurrentUser = Depends(get_current_user) ): """ @@ -351,6 +372,7 @@ async def get_net_balance_breakdown( - Necesită autentificare JWT - Returnează analiza detaliată a balanței nete + - luna/an: perioada contabilă de referință (dacă nu sunt specificate, folosește ultima perioadă) - Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header) """ try: @@ -358,7 +380,7 @@ async def get_net_balance_breakdown( if str(company) not in current_user.companies: raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}") - result = await DashboardService.get_net_balance_breakdown(company, request=request) + result = await DashboardService.get_net_balance_breakdown(company, luna=luna, an=an, request=request) # Convert to dict if needed result_dict = result.dict() if hasattr(result, 'dict') else result diff --git a/reports-app/backend/app/routers/invoices.py b/reports-app/backend/app/routers/invoices.py index 3260270..54d581b 100644 --- a/reports-app/backend/app/routers/invoices.py +++ b/reports-app/backend/app/routers/invoices.py @@ -19,8 +19,8 @@ router = APIRouter() async def get_invoices( company: str = Query(description="Codul firmei"), partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"), - date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"), - date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"), + luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"), + an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"), partner_name: Optional[str] = Query(None, description="Filtru nume partener"), cont: Optional[str] = Query(None, description="Filtru după cont contabil"), only_unpaid: bool = Query(True, description="Doar facturile neachitate"), @@ -32,37 +32,21 @@ async def get_invoices( ): """ Obține lista de facturi pentru o firmă - + - Necesită autentificare JWT - Utilizatorul trebuie să aibă acces la firma specificată - - Suportă filtrare și paginare + - Suportă filtrare după luna/an contabil și paginare """ 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}") - - # Convertește string-urile de date în obiecte date - date_from_obj = None - date_to_obj = None - - if date_from: - try: - date_from_obj = date.fromisoformat(date_from) - except ValueError: - raise HTTPException(status_code=400, detail="Formatul datei de început este invalid. Folosiți YYYY-MM-DD") - - if date_to: - try: - date_to_obj = date.fromisoformat(date_to) - except ValueError: - raise HTTPException(status_code=400, detail="Formatul datei de sfârșit este invalid. Folosiți YYYY-MM-DD") - + filter_params = InvoiceFilter( company=company, partner_type=partner_type, - date_from=date_from_obj, - date_to=date_to_obj, + luna=luna, + an=an, partner_name=partner_name, cont=cont, only_unpaid=only_unpaid, @@ -71,10 +55,10 @@ async def get_invoices( page=page, page_size=page_size ) - + result = await InvoiceService.get_invoices(filter_params, current_user.username) return result - + except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: diff --git a/reports-app/backend/app/routers/treasury.py b/reports-app/backend/app/routers/treasury.py index 97913fb..f39cd1e 100644 --- a/reports-app/backend/app/routers/treasury.py +++ b/reports-app/backend/app/routers/treasury.py @@ -16,6 +16,8 @@ router = APIRouter() async def get_bank_cash_register( company: str = Query(description="Codul firmei"), register_type: Optional[str] = Query(None, description="Tipul registrului: BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA sau None pentru toate"), + luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"), + an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"), date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"), date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"), partner_name: Optional[str] = Query(None, description="Filtru nume partener"), @@ -63,6 +65,8 @@ async def get_bank_cash_register( filter_params = RegisterFilter( company=company, register_type=register_type, + luna=luna, + an=an, date_from=date_from_obj, date_to=date_to_obj, partner_name=partner_name, diff --git a/reports-app/backend/app/services/calendar_service.py b/reports-app/backend/app/services/calendar_service.py new file mode 100644 index 0000000..e1dd318 --- /dev/null +++ b/reports-app/backend/app/services/calendar_service.py @@ -0,0 +1,78 @@ +""" +Calendar service for fetching available accounting periods +""" +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared')) + +from database.oracle_pool import oracle_pool +from ..models.calendar import CalendarPeriod, CalendarPeriodsResponse +from ..cache.decorators import cached +import logging + +logger = logging.getLogger(__name__) + + +class CalendarService: + """Service for calendar/accounting period operations""" + + # Romanian month names for display + MONTH_NAMES_RO = [ + "Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie", + "Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie" + ] + + @staticmethod + @cached(cache_type='schema', key_params=['company_id']) + async def _get_schema(company_id: int) -> str: + """Get schema for company (CACHED 24h)""" + async with oracle_pool.get_connection() as connection: + with connection.cursor() as cursor: + cursor.execute(""" + SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme + WHERE id_firma = :company_id + """, {'company_id': company_id}) + result = cursor.fetchone() + return result[0] if result else None + + @staticmethod + @cached(cache_type='calendar_periods', key_params=['company_id']) + async def get_available_periods(company_id: int) -> CalendarPeriodsResponse: + """ + Get all available accounting periods for a company (CACHED 1h) + + Returns periods ordered by year DESC, month DESC with Romanian month names. + """ + schema = await CalendarService._get_schema(company_id) + if not schema: + logger.warning(f"Schema not found for company {company_id}") + return CalendarPeriodsResponse(periods=[], current_period=None, total_count=0) + + async with oracle_pool.get_connection() as connection: + with connection.cursor() as cursor: + cursor.execute(f""" + SELECT anul, luna + FROM {schema}.calendar + ORDER BY anul DESC, luna DESC + """) + rows = cursor.fetchall() + + periods = [] + for row in rows: + an, luna = row[0], row[1] + month_name = CalendarService.MONTH_NAMES_RO[luna - 1] + periods.append(CalendarPeriod( + an=an, + luna=luna, + display_name=f"{month_name} {an}" + )) + + current_period = periods[0] if periods else None + + logger.info(f"Loaded {len(periods)} accounting periods for company {company_id}") + + return CalendarPeriodsResponse( + periods=periods, + current_period=current_period, + total_count=len(periods) + ) diff --git a/reports-app/backend/app/services/dashboard_service.py b/reports-app/backend/app/services/dashboard_service.py index c137ab8..91c9c29 100644 --- a/reports-app/backend/app/services/dashboard_service.py +++ b/reports-app/backend/app/services/dashboard_service.py @@ -16,6 +16,34 @@ logger = logging.getLogger(__name__) class DashboardService: """Service pentru dashboard - date agregate""" + @staticmethod + def _build_period_cte(schema: str, luna: Optional[int] = None, an: Optional[int] = None) -> tuple[str, dict]: + """ + Construiește CTE pentru luna curentă. + + Dacă luna și an sunt specificate, le folosește. + Altfel, folosește MAX(anul*12+luna) din calendar. + + Returns: + tuple: (cte_sql, params_dict) + """ + if luna is not None and an is not None: + # Folosește parametrii specificați + cte_sql = f""" + WITH luna_curenta AS ( + SELECT :param_an as anul, :param_luna as luna FROM DUAL + )""" + params = {'param_an': an, 'param_luna': luna} + else: + # Folosește MAX din calendar + cte_sql = f""" + WITH luna_curenta AS ( + SELECT anul, luna FROM {schema}.calendar + WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) + )""" + params = {} + return cte_sql, params + @staticmethod @cached(cache_type='schema', key_params=['company_id']) async def _get_schema(company_id: int) -> str: @@ -41,24 +69,31 @@ class DashboardService: return schema_result[0] @staticmethod - @cached(cache_type='dashboard_summary', key_params=['company', 'username']) - async def get_complete_summary(company: str, username: str, request: Optional[Request] = None) -> DashboardSummary: + @cached(cache_type='dashboard_summary', key_params=['company', 'username', 'luna', 'an']) + async def get_complete_summary(company: str, username: str, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> DashboardSummary: """ Obține toate datele pentru dashboard într-un singur apel (CACHED 30 min) Execută 2 query-uri separate: facturi și trezorerie + + Args: + company: ID-ul firmei + username: Numele utilizatorului + luna: Luna contabilă (1-12), opțional + an: Anul contabil, opțional + request: Request object pentru cache metadata """ company_id = int(company) schema = await DashboardService._get_schema(company_id) + # Construiește CTE pentru perioada curentă + period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) + async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: - + # Query 1: Statistici facturi cu breakdown pe perioade - FIXED ORA-00937 facturi_query = f""" - WITH luna_curenta AS ( - SELECT anul, luna FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) - ), + {period_cte}, perioada_stats AS ( SELECT an, luna, @@ -347,19 +382,16 @@ class DashboardService: FROM facturi_stats fs """ - cursor.execute(facturi_query) + cursor.execute(facturi_query, period_params) facturi_row = cursor.fetchone() - - # Query 2: Trezorerie + + # Query 2: Trezorerie (folosește același period_cte) treasury_query = f""" - WITH luna_curenta AS ( - SELECT anul, luna FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) - ) - SELECT + {period_cte} + SELECT cont, nume as nume_banca, - CASE + CASE WHEN cont = '5121' THEN 'Bancă LEI' WHEN cont = '5124' THEN 'Bancă VALUTA' WHEN cont = '5311' THEN 'Casă LEI' @@ -372,26 +404,23 @@ class DashboardService: CASE WHEN cont IN ('5121','5311') THEN 'RON' WHEN cont IN ('5124','5314') THEN NVL(nume_val, 'EUR') - END as valuta + END as valuta FROM {schema}.vbalanta_parteneri WHERE an = (SELECT anul FROM luna_curenta) AND luna = (SELECT luna FROM luna_curenta) AND cont IN ('5121', '5124', '5311', '5314') AND ((cont IN ('5121','5311') AND soldcred - solddeb != 0) OR (cont IN ('5124','5314') AND soldvalcred - soldvaldeb != 0)) - GROUP BY cont, nume, nume_val + GROUP BY cont, nume, nume_val ORDER BY cont, nume """ - - cursor.execute(treasury_query) + + cursor.execute(treasury_query, period_params) treasury_rows = cursor.fetchall() - # Query 3: Solduri TVA din tabelul vbal + # Query 3: Solduri TVA din tabelul vbal (folosește același period_cte) tva_query = f""" - WITH luna_curenta AS ( - SELECT anul, luna FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) - ) + {period_cte} SELECT cont, precdeb, @@ -405,7 +434,7 @@ class DashboardService: ORDER BY cont """ - cursor.execute(tva_query) + cursor.execute(tva_query, period_params) tva_rows = cursor.fetchall() # Procesare solduri TVA @@ -537,34 +566,47 @@ class DashboardService: ) @staticmethod - @cached(cache_type='dashboard_trends', key_params=['company_id', 'period']) - async def get_trends(company_id: int, period: str = "12m", request: Optional[Request] = None) -> Dict[str, Any]: - """Get comprehensive trend analysis data for all dashboard indicators (CACHED 30 min)""" + @cached(cache_type='dashboard_trends', key_params=['company_id', 'period', 'luna', 'an']) + async def get_trends(company_id: int, period: str = "12m", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]: + """Get comprehensive trend analysis data for all dashboard indicators (CACHED 30 min) + + Args: + company_id: ID-ul firmei + period: Perioada pentru trends (7d, 30d, ytd, 12m) + luna: Luna contabilă (1-12), opțional - dacă nu e specificată, folosește MAX + an: Anul contabil, opțional - dacă nu e specificat, folosește MAX + request: Request object pentru cache metadata + """ try: schema = await DashboardService._get_schema(company_id) async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: - - # Get current period - current_period_query = f""" - WITH luna_curenta AS ( - SELECT anul, luna FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) - ) - SELECT anul, luna FROM luna_curenta - """ - - cursor.execute(current_period_query) - current_period = cursor.fetchone() - - if not current_period: - # Fallback to current system date - current_year = 2024 - current_month = 12 + + # Determine current period from params or database + if luna is not None and an is not None: + current_year = an + current_month = luna else: - current_year = current_period[0] - current_month = current_period[1] + # Get current period from database + current_period_query = f""" + WITH luna_curenta AS ( + SELECT anul, luna FROM {schema}.calendar + WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) + ) + SELECT anul, luna FROM luna_curenta + """ + + cursor.execute(current_period_query) + current_period = cursor.fetchone() + + if not current_period: + # Fallback to current system date + current_year = 2024 + current_month = 12 + else: + current_year = current_period[0] + current_month = current_period[1] # Determine period parameters if period == 'ytd': @@ -921,12 +963,21 @@ class DashboardService: raise @staticmethod - async def get_detailed_data(company: str, data_type: str, page: int = 1, page_size: int = 25, search: str = ""): + async def get_detailed_data(company: str, data_type: str, luna: Optional[int] = None, an: Optional[int] = None, page: int = 1, page_size: int = 25, search: str = ""): """ Obține date detaliate pentru tabelele din dashboard Fixed to use existing vireg_parteneri view instead of missing tables + + Args: + company: ID-ul firmei + data_type: Tipul datelor (clients, suppliers, treasury) + luna: Luna contabilă (1-12), opțional + an: Anul contabil, opțional + page: Pagina curentă + page_size: Mărimea paginii + search: Termen de căutare """ - logger.info(f"get_detailed_data called: company={company}, data_type={data_type}, page={page}") + logger.info(f"get_detailed_data called: company={company}, data_type={data_type}, luna={luna}, an={an}, page={page}") async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: try: @@ -945,7 +996,10 @@ class DashboardService: schema = schema_result[0] logger.info(f"Found schema: {schema}") - + + # Construiește CTE pentru perioada curentă + period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) + # Calculate offset for pagination offset = (page - 1) * page_size logger.info(f"Pagination params: page={page}, page_size={page_size}, offset={offset}") @@ -965,10 +1019,7 @@ class DashboardService: if data_type == "clients": # Query cu paginare pe CLIENȚI (nu pe facturi individuale) base_query = f""" - WITH luna_curenta AS ( - SELECT anul, luna FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) - ), + {period_cte}, clienti_cu_sold AS ( -- Pasul 1: Identifică TOȚI clienții cu sold != 0 SELECT DISTINCT vp.nume as client_name @@ -1012,10 +1063,7 @@ class DashboardService: elif data_type == "suppliers": # Query cu paginare pe FURNIZORI (nu pe facturi individuale) base_query = f""" - WITH luna_curenta AS ( - SELECT anul, luna FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) - ), + {period_cte}, furnizori_cu_sold AS ( -- Pasul 1: Identifică TOȚI furnizorii cu sold != 0 SELECT DISTINCT vp.nume as furnizor_name @@ -1061,9 +1109,9 @@ class DashboardService: # Get total count of CLIENȚI/FURNIZORI (not individual invoices) if data_type == "clients": count_query = f""" + {period_cte} SELECT COUNT(DISTINCT vp.nume) - FROM {schema}.vireg_parteneri vp, - (SELECT anul, luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar)) lc + FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('4111','461') @@ -1073,9 +1121,9 @@ class DashboardService: """ elif data_type == "suppliers": count_query = f""" + {period_cte} SELECT COUNT(DISTINCT vp.nume) - FROM {schema}.vireg_parteneri vp, - (SELECT anul, luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar)) lc + FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('401','404','462') @@ -1084,13 +1132,13 @@ class DashboardService: AND (UPPER(vp.nume) LIKE UPPER('%{search}%') OR '{search}' = '') """ - cursor.execute(count_query) + cursor.execute(count_query, period_params) total = cursor.fetchone()[0] logger.info(f"Total {data_type}: {total}") # Execute base query directly (pagination already included in CTE) logger.info(f"Executing query with offset={offset}, page_size={page_size}") - cursor.execute(base_query) + cursor.execute(base_query, period_params) columns = [desc[0].lower() for desc in cursor.description] data = [] @@ -1300,14 +1348,16 @@ class DashboardService: raise @staticmethod - @cached(cache_type='maturity_analysis', key_params=['company_id', 'period']) - async def get_maturity_analysis(company_id: int, period: str = "7d", request: Optional[Request] = None) -> Dict[str, Any]: + @cached(cache_type='maturity_analysis', key_params=['company_id', 'period', 'luna', 'an']) + async def get_maturity_analysis(company_id: int, period: str = "7d", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]: """ Analizează scadențele clienți vs furnizori cu date reale din Oracle (CACHED 30 min) Args: company_id: ID-ul companiei period: Perioada ("7d", "1m", "3m", "6m", "12m", "over12m") + luna: Luna contabilă (1-12), opțional + an: Anul contabil, opțional Returns: { @@ -1330,6 +1380,9 @@ class DashboardService: schema = schema_result[0] + # Construiește CTE pentru perioada curentă + period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) + # Determină filtrele pentru perioada selectată (orizont de planificare) # Logică: Include TOATE restanțele + scadențele viitoare din perioada selectată if period == "7d": @@ -1347,11 +1400,7 @@ class DashboardService: # Query pentru clienți (facturi de încasat) clients_query = f""" - WITH luna_curenta AS - (SELECT anul, luna - FROM {schema}.calendar - WHERE anul * 12 + luna = - (SELECT MAX(anul * 12 + luna) FROM {schema}.calendar)) + {period_cte} SELECT client_name, SUM(amount) as amount, MAX(due_date) as due_date, @@ -1372,7 +1421,7 @@ class DashboardService: ORDER BY days_overdue desc """ - cursor.execute(clients_query) + cursor.execute(clients_query, period_params) clients_rows = cursor.fetchall() clients = [] @@ -1394,11 +1443,7 @@ class DashboardService: # Query pentru furnizori (facturi de plătit) suppliers_query = f""" - WITH luna_curenta AS - (SELECT anul, luna - FROM {schema}.calendar - WHERE anul * 12 + luna = - (SELECT MAX(anul * 12 + luna) FROM {schema}.calendar)) + {period_cte} SELECT client_name, SUM(amount) as amount, MIN(due_date) as due_date, @@ -1419,7 +1464,7 @@ class DashboardService: ORDER BY days_overdue desc """ - cursor.execute(suppliers_query) + cursor.execute(suppliers_query, period_params) suppliers_rows = cursor.fetchall() suppliers = [] @@ -1502,9 +1547,14 @@ class DashboardService: raise @staticmethod - async def get_monthly_flows(company: int) -> Dict[str, Any]: + async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None) -> Dict[str, Any]: """ Obține fluxurile lunare de intrare și ieșire pentru luna curentă + + Args: + company: ID-ul firmei + luna: Luna contabilă (1-12), opțional + an: Anul contabil, opțional """ try: async with oracle_pool.get_connection() as connection: @@ -1520,14 +1570,27 @@ class DashboardService: schema = schema_result[0] + # Construiește CTE pentru perioada curentă (cu period column) + if luna is not None and an is not None: + period_cte = f""" + WITH luna_curenta AS ( + SELECT :param_an as anul, :param_luna as luna, + :param_an || '-' || LPAD(:param_luna, 2, '0') as period + FROM DUAL + )""" + period_params = {'param_an': an, 'param_luna': luna} + else: + period_cte = f""" + WITH luna_curenta AS ( + SELECT anul, luna, anul || '-' || LPAD(luna, 2, '0') as period + FROM {schema}.calendar + WHERE anul * 12 + luna = (SELECT MAX(c2.anul * 12 + c2.luna) FROM {schema}.calendar c2) + )""" + period_params = {} + # Query pentru fluxuri lunare flows_query = f""" - WITH luna_curenta AS - (SELECT anul, luna, anul || '-' || LPAD(luna, 2, '0') as period - FROM {schema}.calendar - WHERE anul * 12 + luna = - (SELECT MAX(c2.anul * 12 + c2.luna) - FROM {schema}.calendar c2)) + {period_cte} SELECT SUM(CASE WHEN vp.cont IN ('4111', '461') THEN @@ -1552,14 +1615,16 @@ class DashboardService: AND vp.cont IN ('4111', '461', '419', '401', '404', '462', '409') """ - cursor.execute(flows_query) + cursor.execute(flows_query, period_params) flow_row = cursor.fetchone() if not flow_row: + # Default period from params or current date + default_period = f"{an}-{str(luna).zfill(2)}" if luna and an else "2025-12" return { "inflows": 0, "outflows": 0, - "period": "2025-08", + "period": default_period, "currency": "RON" } @@ -1574,10 +1639,15 @@ class DashboardService: raise @staticmethod - @cached(cache_type='treasury_breakdown', key_params=['company']) - async def get_treasury_breakdown(company: int, request: Optional[Request] = None) -> Dict[str, Any]: + @cached(cache_type='treasury_breakdown', key_params=['company', 'luna', 'an']) + async def get_treasury_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]: """ Obține breakdown-ul trezoreriei pe casă și bancă (CACHED 30 min) + + Args: + company: ID-ul firmei + luna: Luna contabilă (1-12), opțional + an: Anul contabil, opțional """ try: async with oracle_pool.get_connection() as connection: @@ -1593,13 +1663,12 @@ class DashboardService: schema = schema_result[0] + # Construiește CTE pentru perioada curentă + period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) + # Query pentru breakdown trezorerie - cu nume reale și sub-breakdown treasury_query = f""" - WITH luna_curenta AS - (SELECT anul, luna - FROM {schema}.calendar - WHERE anul * 12 + luna = - (SELECT MAX(c2.anul * 12 + c2.luna) FROM {schema}.calendar c2)) + {period_cte} SELECT vb.cont, vb.nume as nume_real, @@ -1616,7 +1685,7 @@ class DashboardService: ORDER BY tip, vb.cont """ - cursor.execute(treasury_query) + cursor.execute(treasury_query, period_params) treasury_rows = cursor.fetchall() if not treasury_rows: @@ -1675,10 +1744,15 @@ class DashboardService: raise @staticmethod - @cached(cache_type='net_balance_breakdown', key_params=['company']) - async def get_net_balance_breakdown(company: int, request: Optional[Request] = None) -> Dict[str, Any]: + @cached(cache_type='net_balance_breakdown', key_params=['company', 'luna', 'an']) + async def get_net_balance_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]: """ Obține breakdown-ul balanței nete pe clienți și furnizori cu detaliere pe perioade (CACHED 30 min) + + Args: + company: ID-ul firmei + luna: Luna contabilă (1-12), opțional + an: Anul contabil, opțional """ try: async with oracle_pool.get_connection() as connection: @@ -1694,13 +1768,12 @@ class DashboardService: schema = schema_result[0] + # Construiește CTE pentru perioada curentă + period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) + # Query extins pentru breakdown detaliat pe perioade balance_query = f""" - WITH luna_curenta AS ( - SELECT anul, luna - FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(c2.anul*12+c2.luna) FROM {schema}.calendar c2) - ) + {period_cte} SELECT -- CLIENȚI - Sold total SUM(CASE @@ -1794,7 +1867,7 @@ class DashboardService: AND vp.cont IN ('4111', '461', '419', '401', '404', '462','409') """ - cursor.execute(balance_query) + cursor.execute(balance_query, period_params) row = cursor.fetchone() if not row: diff --git a/reports-app/backend/app/services/invoice_service.py b/reports-app/backend/app/services/invoice_service.py index f6568b8..fe6d134 100644 --- a/reports-app/backend/app/services/invoice_service.py +++ b/reports-app/backend/app/services/invoice_service.py @@ -56,6 +56,15 @@ class InvoiceService: else: conturi = "'4111'" # default + # Determine period to use: from params or MAX from calendar + if filter_params.luna and filter_params.an: + period_condition = "vp.an = :an AND vp.luna = :luna" + use_param_period = True + else: + period_condition = f"""vp.an = (SELECT anul FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar)) + AND vp.luna = (SELECT luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar))""" + use_param_period = False + # Query cu calculele corecte pentru solduri base_query = f""" SELECT @@ -87,25 +96,20 @@ class InvoiceService: ELSE 'in_termen' END as status FROM {schema}.vireg_parteneri vp - WHERE vp.an = (SELECT anul FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar)) - AND vp.luna = (SELECT luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar)) + WHERE {period_condition} AND ( (:partner_type = 'CLIENTI' AND vp.cont IN ('4111', '461')) - OR + OR (:partner_type = 'FURNIZORI' AND vp.cont IN ('401', '404', '462')) ) """ - + params = {'partner_type': filter_params.partner_type} - - # Adaugă filtre dinamice - if filter_params.date_from: - base_query += " AND vp.dataact >= :date_from" - params['date_from'] = filter_params.date_from - - if filter_params.date_to: - base_query += " AND vp.dataact <= :date_to" - params['date_to'] = filter_params.date_to + + # Add period params if using explicit period + if use_param_period: + params['an'] = filter_params.an + params['luna'] = filter_params.luna if filter_params.partner_name: base_query += " AND UPPER(vp.nume) LIKE UPPER(:partner_name)" @@ -139,18 +143,24 @@ class InvoiceService: cursor.execute(count_query, params) total_count = cursor.fetchone()[0] - # Get accounting period (luna, an) from calendar - period_query = f""" - SELECT anul, luna - FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) - """ - cursor.execute(period_query) - period_result = cursor.fetchone() - accounting_period = { - 'an': period_result[0] if period_result else None, - 'luna': period_result[1] if period_result else None - } + # Get accounting period - use params if provided, else from calendar + if use_param_period: + accounting_period = { + 'an': filter_params.an, + 'luna': filter_params.luna + } + else: + period_query = f""" + SELECT anul, luna + FROM {schema}.calendar + WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) + """ + cursor.execute(period_query) + period_result = cursor.fetchone() + accounting_period = { + 'an': period_result[0] if period_result else None, + 'luna': period_result[1] if period_result else None + } # Adaugă ORDER BY și paginare - Ordonare cronologică (DATAACT, NRACT, NUME) base_query += " ORDER BY vp.DATAACT ASC, vp.NRACT ASC, vp.NUME" diff --git a/reports-app/backend/app/services/treasury_service.py b/reports-app/backend/app/services/treasury_service.py index 8c3f6b4..3cb3fab 100644 --- a/reports-app/backend/app/services/treasury_service.py +++ b/reports-app/backend/app/services/treasury_service.py @@ -153,8 +153,23 @@ class TreasuryService: offset = (filter_params.page - 1) * filter_params.page_size limit_val = filter_params.page_size + # Determine period to use: from params or MAX from calendar + if filter_params.luna and filter_params.an: + use_param_period = True + period_select = f""" + v_an := :param_an; + v_luna := :param_luna; + """ + else: + use_param_period = False + period_select = f""" + SELECT anul, luna INTO v_an, v_luna + FROM {schema}.calendar + WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar); + """ + # Bloc PL/SQL anonim care face totul într-o singură tranzacție: - # 1. Obține anul și luna din calendar + # 1. Obține anul și luna din params sau calendar # 2. Setează PACK_SESIUNE.SETAN și SETLUNA # 3. Returnează datele prin REF CURSOR # IMPORTANT: Folosim ROW_NUMBER() pentru paginare corectă cu ORDER BY NULLS FIRST @@ -164,10 +179,8 @@ class TreasuryService: v_luna NUMBER; v_cursor SYS_REFCURSOR; BEGIN - -- Obține anul și luna curentă din calendar - SELECT anul, luna INTO v_an, v_luna - FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar); + -- Obține anul și luna din parametri sau calendar + {period_select} -- Setează contextul de sesiune (OBLIGATORIU înainte de SELECT din vbancasa*) {schema}.PACK_SESIUNE.SETAN(v_an); @@ -198,8 +211,14 @@ class TreasuryService: out_an = cursor.var(int) out_luna = cursor.var(int) + # Build params dict + exec_params = {'result_cursor': result_cursor, 'out_an': out_an, 'out_luna': out_luna} + if use_param_period: + exec_params['param_an'] = filter_params.an + exec_params['param_luna'] = filter_params.luna + # Execută blocul PL/SQL cu REF CURSOR - cursor.execute(plsql_block, {'result_cursor': result_cursor, 'out_an': out_an, 'out_luna': out_luna}) + cursor.execute(plsql_block, exec_params) # Get accounting period values accounting_year = out_an.getvalue() @@ -216,9 +235,8 @@ class TreasuryService: v_an NUMBER; v_luna NUMBER; BEGIN - SELECT anul, luna INTO v_an, v_luna - FROM {schema}.calendar - WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar); + -- Obține anul și luna din parametri sau calendar + {period_select} {schema}.PACK_SESIUNE.SETAN(v_an); {schema}.PACK_SESIUNE.SETLUNA(v_luna); @@ -228,7 +246,11 @@ class TreasuryService: """ total_count_var = cursor.var(int) - cursor.execute(count_plsql, {'total_count': total_count_var}) + count_params = {'total_count': total_count_var} + if use_param_period: + count_params['param_an'] = filter_params.an + count_params['param_luna'] = filter_params.luna + cursor.execute(count_plsql, count_params) total_count = total_count_var.getvalue() # Procesare rezultate diff --git a/reports-app/frontend/src/components/dashboard/PeriodSelectorMini.vue b/reports-app/frontend/src/components/dashboard/PeriodSelectorMini.vue new file mode 100644 index 0000000..c3e6d15 --- /dev/null +++ b/reports-app/frontend/src/components/dashboard/PeriodSelectorMini.vue @@ -0,0 +1,366 @@ + + + + + diff --git a/reports-app/frontend/src/components/dashboard/cards/MaturityAndDetailsCard.vue b/reports-app/frontend/src/components/dashboard/cards/MaturityAndDetailsCard.vue index d09b91f..e2e03d1 100644 --- a/reports-app/frontend/src/components/dashboard/cards/MaturityAndDetailsCard.vue +++ b/reports-app/frontend/src/components/dashboard/cards/MaturityAndDetailsCard.vue @@ -499,6 +499,7 @@ import { ref, computed, watch, onMounted } from "vue"; import { useDashboardStore } from "../../../stores/dashboard"; import { useCompanyStore } from "../../../stores/companies"; +import { useAccountingPeriodStore } from "../../../stores/accountingPeriod"; import { useToast } from "primevue/usetoast"; import Paginator from "primevue/paginator"; import * as XLSX from "xlsx"; @@ -519,6 +520,7 @@ const emit = defineEmits(["periodChanged"]); // Stores const dashboardStore = useDashboardStore(); const companyStore = useCompanyStore(); +const periodStore = useAccountingPeriodStore(); const toast = useToast(); // State - Maturity Analysis @@ -819,10 +821,15 @@ const loadMaturityData = async () => { isLoading.value = true; error.value = null; + const luna = periodStore.selectedPeriod?.luna || null; + const an = periodStore.selectedPeriod?.an || null; + try { const response = await dashboardStore.loadMaturityData( props.companyId, selectedPeriod.value, + luna, + an, ); if (response && response.success) { @@ -854,12 +861,17 @@ const loadDetailedData = async () => { // Calculate page number from firstRow const page = Math.floor(firstRow.value / rowsPerPage.value) + 1; + const luna = periodStore.selectedPeriod?.luna || null; + const an = periodStore.selectedPeriod?.an || null; + const response = await dashboardStore.loadDetailedData( selectedType.value, companyStore.selectedCompany.id_firma, page, rowsPerPage.value, searchTerm.value, + luna, + an, ); detailedData.value = response.data; expandedGroups.value.clear(); @@ -925,6 +937,21 @@ watch( }, ); +// Watch for accounting period changes - reload data when period changes +watch( + () => periodStore.selectedPeriod, + (newPeriod, oldPeriod) => { + if (newPeriod && (newPeriod.luna !== oldPeriod?.luna || newPeriod.an !== oldPeriod?.an)) { + console.log("Period changed in MaturityAndDetailsCard:", newPeriod); + loadMaturityData(); + if (isDetailsExpanded.value) { + loadDetailedData(); + } + } + }, + { deep: true } +); + // Watch for pagination changes watch([firstRow, rowsPerPage], () => { if (isDetailsExpanded.value && detailedData.value.length > 0) { diff --git a/reports-app/frontend/src/components/layout/DashboardHeader.vue b/reports-app/frontend/src/components/layout/DashboardHeader.vue index 92c34d5..57bdd0b 100644 --- a/reports-app/frontend/src/components/layout/DashboardHeader.vue +++ b/reports-app/frontend/src/components/layout/DashboardHeader.vue @@ -19,8 +19,12 @@ - +
+ { + emit("period-changed", period); + }; + const navigateToTelegram = async () => { try { closeUserMenu(); @@ -147,6 +157,7 @@ export default { toggleUserMenu, closeUserMenu, onCompanyChanged, + onPeriodChanged, navigateToTelegram, handleLogout, }; diff --git a/reports-app/frontend/src/stores/accountingPeriod.js b/reports-app/frontend/src/stores/accountingPeriod.js new file mode 100644 index 0000000..21f112a --- /dev/null +++ b/reports-app/frontend/src/stores/accountingPeriod.js @@ -0,0 +1,138 @@ +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; +import { apiService } from "../services/api"; +import { useAuthStore } from "./auth"; +import { useCompanyStore } from "./companies"; + +export const useAccountingPeriodStore = defineStore("accountingPeriod", () => { + // State + const periods = ref([]); + const selectedPeriod = ref(null); + const isLoading = ref(false); + const error = ref(null); + + // Getters + const hasPeriods = computed(() => periods.value.length > 0); + const currentPeriod = computed(() => selectedPeriod.value); + + // Computed date range for current period (first/last day of month) + const dateRange = computed(() => { + if (!selectedPeriod.value) return { dateFrom: null, dateTo: null }; + + const { an, luna } = selectedPeriod.value; + const firstDay = new Date(an, luna - 1, 1); + const lastDay = new Date(an, luna, 0); + + return { + dateFrom: firstDay, + dateTo: lastDay, + }; + }); + + // Actions + const loadPeriods = async (companyId) => { + if (!companyId) return { success: false }; + + isLoading.value = true; + error.value = null; + + try { + const response = await apiService.get("/calendar/periods", { + params: { company: companyId }, + }); + + periods.value = response.data.periods || []; + + // Try to restore saved period or use most recent + const saved = initializeSelectedPeriod(); + if (saved) { + const exists = periods.value.find( + (p) => p.an === saved.an && p.luna === saved.luna + ); + if (exists) { + selectedPeriod.value = exists; + } else if (response.data.current_period) { + setSelectedPeriod(response.data.current_period); + } + } else if (response.data.current_period) { + setSelectedPeriod(response.data.current_period); + } + + return { success: true }; + } catch (err) { + error.value = err.response?.data?.detail || "Failed to load periods"; + return { success: false, error: error.value }; + } finally { + isLoading.value = false; + } + }; + + const setSelectedPeriod = (period) => { + selectedPeriod.value = period; + persistSelectedPeriod(period); + }; + + const resetToLatest = () => { + if (periods.value.length > 0) { + setSelectedPeriod(periods.value[0]); + } + }; + + const reset = () => { + periods.value = []; + selectedPeriod.value = null; + isLoading.value = false; + error.value = null; + }; + + // localStorage helpers + const getStorageKey = () => { + const authStore = useAuthStore(); + const companyStore = useCompanyStore(); + const username = authStore.user?.username; + const companyId = companyStore.selectedCompany?.id_firma; + if (!username || !companyId) return null; + return `selected_period_${username}_${companyId}`; + }; + + const initializeSelectedPeriod = () => { + const key = getStorageKey(); + if (!key) return null; + + const saved = localStorage.getItem(key); + if (saved) { + try { + return JSON.parse(saved); + } catch (e) { + localStorage.removeItem(key); + } + } + return null; + }; + + const persistSelectedPeriod = (period) => { + const key = getStorageKey(); + if (key && period) { + localStorage.setItem(key, JSON.stringify(period)); + } + }; + + return { + // State + periods, + selectedPeriod, + isLoading, + error, + + // Getters + hasPeriods, + currentPeriod, + dateRange, + + // Actions + loadPeriods, + setSelectedPeriod, + resetToLatest, + reset, + }; +}); diff --git a/reports-app/frontend/src/stores/dashboard.js b/reports-app/frontend/src/stores/dashboard.js index 8a0154e..0d28d5b 100644 --- a/reports-app/frontend/src/stores/dashboard.js +++ b/reports-app/frontend/src/stores/dashboard.js @@ -21,14 +21,16 @@ export const useDashboardStore = defineStore("dashboard", () => { // Cache pentru date const dataCache = new Map(); - const loadDashboardSummary = async (companyId) => { + const loadDashboardSummary = async (companyId, luna = null, an = null) => { isLoading.value = true; error.value = null; try { - const response = await apiService.get("/dashboard/summary", { - params: { company: companyId }, - }); + const params = { company: companyId }; + if (luna !== null) params.luna = luna; + if (an !== null) params.an = an; + + const response = await apiService.get("/dashboard/summary", { params }); summary.value = response.data; return { success: true }; } catch (err) { @@ -44,21 +46,25 @@ export const useDashboardStore = defineStore("dashboard", () => { companyId, period = "12m", chartType = "line", + luna = null, + an = null, ) => { isLoading.value = true; error.value = null; try { console.log( - `Loading trend data for company ${companyId}, period: ${period}`, + `Loading trend data for company ${companyId}, period: ${period}, luna: ${luna}, an: ${an}`, ); - const response = await apiService.get("/dashboard/trends", { - params: { - company: companyId, - period: period, - }, - }); + const params = { + company: companyId, + period: period, + }; + if (luna !== null) params.luna = luna; + if (an !== null) params.an = an; + + const response = await apiService.get("/dashboard/trends", { params }); // Validate response structure if (!response.data) { @@ -188,20 +194,24 @@ export const useDashboardStore = defineStore("dashboard", () => { page = 1, pageSize = 25, search = "", + luna = null, + an = null, ) => { isLoading.value = true; error.value = null; try { - const response = await apiService.get("/dashboard/detailed-data", { - params: { - company: companyId, - data_type: dataType, - page: page, - page_size: pageSize, - search: search, - }, - }); + const params = { + company: companyId, + data_type: dataType, + page: page, + page_size: pageSize, + search: search, + }; + if (luna !== null) params.luna = luna; + if (an !== null) params.an = an; + + const response = await apiService.get("/dashboard/detailed-data", { params }); // Store total for pagination detailedDataTotal.value = response.data.total || 0; @@ -417,8 +427,8 @@ export const useDashboardStore = defineStore("dashboard", () => { } }; - const loadMaturityData = async (companyId, period = "7d") => { - const cacheKey = `maturity-${companyId}-${period}`; + const loadMaturityData = async (companyId, period = "7d", luna = null, an = null) => { + const cacheKey = `maturity-${companyId}-${period}-${luna}-${an}`; if (dataCache.has(cacheKey)) { maturityData.value[period] = dataCache.get(cacheKey); @@ -426,9 +436,11 @@ export const useDashboardStore = defineStore("dashboard", () => { } try { - const response = await apiService.get("/dashboard/maturity", { - params: { company: companyId, period }, - }); + const params = { company: companyId, period }; + if (luna !== null) params.luna = luna; + if (an !== null) params.an = an; + + const response = await apiService.get("/dashboard/maturity", { params }); maturityData.value[period] = response.data; dataCache.set(cacheKey, response.data); diff --git a/reports-app/frontend/src/views/BankCashRegisterView.vue b/reports-app/frontend/src/views/BankCashRegisterView.vue index 75a24a8..c0871b0 100644 --- a/reports-app/frontend/src/views/BankCashRegisterView.vue +++ b/reports-app/frontend/src/views/BankCashRegisterView.vue @@ -68,30 +68,6 @@ />
-
-
- - -
-
-
-
- - -
-
@@ -287,12 +263,14 @@ import { ref, computed, onMounted, watch } from "vue"; import { useToast } from "primevue/usetoast"; import { useTreasuryStore } from "../stores/treasury"; import { useCompanyStore } from "../stores/companies"; +import { useAccountingPeriodStore } from "../stores/accountingPeriod"; import { format } from "date-fns"; import { exportToExcel, exportBankCashRegisterPDF } from "../utils/exportUtils"; const toast = useToast(); const treasuryStore = useTreasuryStore(); const companyStore = useCompanyStore(); +const periodStore = useAccountingPeriodStore(); // State for company selection const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null); @@ -307,8 +285,6 @@ const registerTypeOptions = [ const filters = ref({ registerType: "BANCA_LEI", // Default: Registrul de Banca Lei - dateFrom: null, - dateTo: null, partnerName: "", bankAccount: null, // Filter for specific bank/cash account }); @@ -400,25 +376,8 @@ const contColumnHeader = computed(() => { // Accounting period text for PDF export const accountingPeriodText = computed(() => { - const months = [ - "Ianuarie", - "Februarie", - "Martie", - "Aprilie", - "Mai", - "Iunie", - "Iulie", - "August", - "Septembrie", - "Octombrie", - "Noiembrie", - "Decembrie", - ]; - const luna = treasuryStore.accountingPeriod.luna; - const an = treasuryStore.accountingPeriod.an; - if (!luna || !an) return ""; - const monthName = months[luna - 1] || ""; - return `${monthName} ${an}`; + // Use the global period store + return periodStore.selectedPeriod?.display_name || ""; }); // Helper to remove diacritics from text @@ -504,8 +463,6 @@ const onPage = async (event) => { const resetFilters = async () => { filters.value = { registerType: "BANCA_LEI", // Reset la default: Registrul de Banca Lei - dateFrom: null, - dateTo: null, partnerName: "", bankAccount: null, // Reset bank account filter }; @@ -559,12 +516,18 @@ const refreshData = async () => { // Fetch ALL data for export (not just current page) const fetchAllData = async () => { if (!companyStore.selectedCompany) return []; + if (!periodStore.selectedPeriod) return []; try { + // Get luna/an from period store + const { luna, an } = periodStore.selectedPeriod; + const params = { company: companyStore.selectedCompany.id_firma, page: 1, page_size: 999999, // Get all data + luna: luna, + an: an, }; // Add register_type filter @@ -572,25 +535,6 @@ const fetchAllData = async () => { params.register_type = filters.value.registerType; } - // Add optional filters (use LOCAL date, not UTC) - if (filters.value.dateFrom) { - const year = filters.value.dateFrom.getFullYear(); - const month = String(filters.value.dateFrom.getMonth() + 1).padStart( - 2, - "0", - ); - const day = String(filters.value.dateFrom.getDate()).padStart(2, "0"); - params.date_from = `${year}-${month}-${day}`; - } - if (filters.value.dateTo) { - const year = filters.value.dateTo.getFullYear(); - const month = String(filters.value.dateTo.getMonth() + 1).padStart( - 2, - "0", - ); - const day = String(filters.value.dateTo.getDate()).padStart(2, "0"); - params.date_to = `${year}-${month}-${day}`; - } if (filters.value.partnerName) { params.partner_name = filters.value.partnerName; } @@ -756,33 +700,22 @@ const exportPDF = async () => { const loadData = async () => { if (!companyStore.selectedCompany) return; + if (!periodStore.selectedPeriod) return; // Wait for period to be loaded treasuryStore.setPagination(pagination.value); - // Build filter params - use LOCAL dates, not UTC + // Get luna/an from period store + const { luna, an } = periodStore.selectedPeriod; + + // Build filter params with luna/an instead of date_from/date_to const filterParams = { partner_name: filters.value.partnerName || undefined, register_type: filters.value.registerType || undefined, bank_account: filters.value.bankAccount || undefined, + luna: luna, + an: an, }; - // Format dates properly using local time - if (filters.value.dateFrom) { - const year = filters.value.dateFrom.getFullYear(); - const month = String(filters.value.dateFrom.getMonth() + 1).padStart( - 2, - "0", - ); - const day = String(filters.value.dateFrom.getDate()).padStart(2, "0"); - filterParams.date_from = `${year}-${month}-${day}`; - } - if (filters.value.dateTo) { - const year = filters.value.dateTo.getFullYear(); - const month = String(filters.value.dateTo.getMonth() + 1).padStart(2, "0"); - const day = String(filters.value.dateTo.getDate()).padStart(2, "0"); - filterParams.date_to = `${year}-${month}-${day}`; - } - await treasuryStore.loadBankCashRegister( companyStore.selectedCompany.id_firma, filterParams, @@ -795,23 +728,34 @@ onMounted(async () => { await companyStore.loadCompanies(); } - // Load data if company is selected + // Load bank accounts for initial register type if company is selected if (companyStore.selectedCompany) { - // Load bank accounts for initial register type await loadBankAccounts(); - await loadData(); } + // Don't load data here - let period watch handle it with immediate: true }); -// Watch for company changes - CRITICAL FIX +// Watch for company changes watch( () => companyStore.selectedCompany, async (newCompany) => { - if (newCompany) { + if (newCompany && periodStore.selectedPeriod) { + await loadBankAccounts(); await loadData(); } }, ); + +// Watch for period changes - reload data when period changes +watch( + () => periodStore.selectedPeriod, + async (newPeriod) => { + if (newPeriod && companyStore.selectedCompany) { + await loadData(); + } + }, + { immediate: true }, +);