diff --git a/features/TRIAL_BALANCE_FEATURE.md b/features/TRIAL_BALANCE_FEATURE.md deleted file mode 100644 index 9aa9f69..0000000 --- a/features/TRIAL_BALANCE_FEATURE.md +++ /dev/null @@ -1,587 +0,0 @@ -# Feature: Balanță de Verificare Sintetică (Trial Balance) - -**Branch**: `feature/trial-balance` -**Status**: 📋 Planning -**Created**: 2025-11-20 -**Assigned to**: Claude Code - ---- - -## 📋 Overview - -Implementare pagină nouă în frontend pentru afișarea balanței de verificare sintetice simplificate din tabelul Oracle `VBAL`, cu filtrare dinamică și integrare completă în aplicație. - ---- - -## 🎯 Business Requirements - -### Functional Requirements -1. **Afișare Date Balanță de Verificare**: - - Solduri precedente (debit/credit) - - Rulaje lunare (debit/credit) - - Solduri finale (debit/credit) - - Cont contabil (cod) - - Denumire cont - -2. **Filtrare și Căutare**: - - Filtrare după **număr cont** (ex: "512", "4111") - - Filtrare după **denumire cont** (căutare parțială, case-insensitive) - - Filtrare combinată (și după cont și după denumire) - - Clear filters option - -3. **Navigare și UX**: - - Adăugare în meniul hamburger (secțiune Rapoarte / Reports) - - Ordonare pe coloane (ascendent/descendent) - - Paginare pentru volume mari de date - - Loading states și error handling - - Responsive design (mobile-friendly) - -4. **Date Contextualizate**: - - Afișare pentru compania activă curentă - - Afișare pentru luna/anul curent (sau filtru de perioadă - opțional faza 2) - -### Non-Functional Requirements -- **Performance**: Răspuns API < 2s pentru 10,000 înregistrări -- **Security**: Autentificare JWT obligatorie -- **Accessibility**: Conform design system existent -- **CSS**: Respectare strictă a arhitecturii CSS (ONBOARDING_CSS.md, CSS_PATTERNS.md) - ---- - -## 🏗️ Technical Architecture - -### Database Schema (Oracle `VBAL` table) -```sql --- Structura tabelului VBAL (să fie verificată în Oracle) -SELECT - CONT, -- Număr cont contabil - DCONT, -- Denumire cont - SD_PREC, -- Sold precedent debit - SC_PREC, -- Sold precedent credit - RD_LUNA, -- Rulaj lunar debit - RC_LUNA, -- Rulaj lunar credit - SD_FINAL, -- Sold final debit - SC_FINAL, -- Sold final credit - COD_FIRMA, -- Cod firmă (pentru filtering) - LUNA, -- Luna (1-12) - AN -- An -FROM VBAL -WHERE COD_FIRMA = :cod_firma - AND AN = :an - AND LUNA = :luna - AND (CONT LIKE :cont_filter OR DCONT LIKE :denumire_filter) -ORDER BY CONT -``` - -### Backend API Endpoint - -**Endpoint**: `GET /api/trial-balance` - -**Request Parameters**: -```json -{ - "cod_firma": "string (required, from JWT)", - "luna": "integer (1-12, optional, default: current month)", - "an": "integer (optional, default: current year)", - "cont_filter": "string (optional, partial match)", - "denumire_filter": "string (optional, partial match, case-insensitive)", - "sort_by": "string (optional, default: 'CONT')", - "sort_order": "string (optional, 'asc' | 'desc', default: 'asc')", - "page": "integer (optional, default: 1)", - "page_size": "integer (optional, default: 50)" -} -``` - -**Response**: -```json -{ - "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": null, - "denumire_filter": "furnizori" - } - } -} -``` - -**Error Responses**: -- `401 Unauthorized`: Missing/invalid JWT token -- `403 Forbidden`: User doesn't have access to company -- `500 Internal Server Error`: Database connection/query error - ---- - -### Frontend Architecture - -**Location**: `reports-app/frontend/src/views/TrialBalanceView.vue` - -**Store**: `reports-app/frontend/src/stores/trialBalanceStore.js` - -**Route**: `/trial-balance` (added to `router/index.js`) - -**Menu Integration**: -- Add to hamburger menu in `src/components/layout/HamburgerMenu.vue` -- Section: "Rapoarte" / "Reports" -- Icon: Calculator/Table icon (PrimeVue icon) -- Label: "Balanță de Verificare" - -**Component Structure**: -```vue - -``` - -**CSS Patterns to Use**: -- `.roa-page-header` - Page header (CSS_PATTERNS.md) -- `.roa-card` - Card container (CSS_PATTERNS.md) -- `.roa-filters-card` - Filters section (CSS_PATTERNS.md) -- Design tokens from DESIGN_TOKENS.md (colors, spacing) -- **NO `:deep()`** - Use `src/assets/css/vendor/primevue/datatable.css` for PrimeVue overrides - -**State Management** (Pinia store): -```javascript -// src/stores/trialBalanceStore.js -export const useTrialBalanceStore = defineStore('trialBalance', { - state: () => ({ - trialBalanceData: [], - loading: false, - error: null, - filters: { - cont: '', - denumire: '', - luna: new Date().getMonth() + 1, - an: new Date().getFullYear() - }, - pagination: { - totalItems: 0, - currentPage: 1, - pageSize: 50 - } - }), - - actions: { - async fetchTrialBalance() { /* ... */ }, - async applyFilters(filters) { /* ... */ }, - clearFilters() { /* ... */ } - } -}) -``` - ---- - -## 📝 Implementation Plan (Phases) - -### ✅ Phase 0: Preparation & Branch Setup -**Status**: 🔲 Not Started - -**Tasks**: -- [ ] Create feature branch `feature/trial-balance` from `main` -- [ ] Verify Oracle `VBAL` table structure and test queries -- [ ] Review existing code patterns (InvoicesView, DashboardView) -- [ ] Read CSS documentation (ONBOARDING_CSS.md, CSS_PATTERNS.md) - -**Estimated Time**: 30 minutes - ---- - -### ✅ Phase 1: Backend API Implementation -**Status**: 🔲 Not Started - -**Tasks**: -- [ ] Create Pydantic schema: `backend/app/schemas/trial_balance.py` - - `TrialBalanceItem` model - - `TrialBalanceRequest` model - - `TrialBalanceResponse` model -- [ ] Create router: `backend/app/routers/trial_balance.py` - - `GET /api/trial-balance` endpoint - - Query parameters validation - - Oracle stored procedure/query execution - - Pagination logic - - Filtering logic (cont, denumire) - - Sorting logic -- [ ] Register router in `backend/app/main.py` -- [ ] Test endpoint manually with Postman/curl - - Test with valid JWT token - - Test filtering by cont - - Test filtering by denumire - - Test pagination - - Test sorting - - Test error cases (invalid token, DB errors) - -**Files to Create/Modify**: -- `reports-app/backend/app/schemas/trial_balance.py` (NEW) -- `reports-app/backend/app/routers/trial_balance.py` (NEW) -- `reports-app/backend/app/main.py` (MODIFY) - -**Acceptance Criteria**: -- ✅ API returns correct data from `VBAL` table -- ✅ Filtering works for cont and denumire -- ✅ Pagination returns correct page counts -- ✅ Sorting works for all columns -- ✅ Proper error handling and status codes - -**Estimated Time**: 2-3 hours - ---- - -### ✅ Phase 2: Frontend Store Implementation -**Status**: 🔲 Not Started - -**Tasks**: -- [ ] Create Pinia store: `frontend/src/stores/trialBalanceStore.js` - - State definition (data, loading, error, filters, pagination) - - `fetchTrialBalance()` action with Axios - - `applyFilters()` action - - `clearFilters()` action - - Getters for computed values -- [ ] Add API service: `frontend/src/services/trialBalanceService.js` (optional) -- [ ] Test store in browser DevTools - -**Files to Create**: -- `reports-app/frontend/src/stores/trialBalanceStore.js` (NEW) -- `reports-app/frontend/src/services/trialBalanceService.js` (NEW, optional) - -**Acceptance Criteria**: -- ✅ Store successfully fetches data from API -- ✅ Filters update correctly -- ✅ Loading states work properly -- ✅ Error handling displays user-friendly messages - -**Estimated Time**: 1-2 hours - ---- - -### ✅ Phase 3: Frontend View Component -**Status**: 🔲 Not Started - -**Tasks**: -- [ ] Create Vue component: `frontend/src/views/TrialBalanceView.vue` - - Page header with title and period info - - Filters card (cont input, denumire input, clear button) - - PrimeVue DataTable with columns: - - Cont - - Denumire Cont - - Sold Precedent (Debit/Credit) - - Rulaj Lunar (Debit/Credit) - - Sold Final (Debit/Credit) - - Loading spinner - - Empty state (no data) - - Error state -- [ ] Use **existing CSS patterns** (NO new custom CSS unless necessary) - - `.roa-page-header` - - `.roa-card` - - `.roa-filters-card` - - Design tokens (spacing, colors) -- [ ] Implement responsive design (mobile breakpoints) -- [ ] Add number formatting (Romanian locale, 2 decimals) - -**Files to Create**: -- `reports-app/frontend/src/views/TrialBalanceView.vue` (NEW) - -**Acceptance Criteria**: -- ✅ Page displays correctly with data -- ✅ Filters work and update table -- ✅ Sorting on columns works -- ✅ Pagination works -- ✅ Loading/error states display correctly -- ✅ CSS follows design system (NO `:deep()`, NO custom colors) -- ✅ Responsive on mobile devices - -**Estimated Time**: 3-4 hours - ---- - -### ✅ Phase 4: Routing & Navigation -**Status**: 🔲 Not Started - -**Tasks**: -- [ ] Add route in `frontend/src/router/index.js`: - ```javascript - { - path: '/trial-balance', - name: 'TrialBalance', - component: () => import('@/views/TrialBalanceView.vue'), - meta: { requiresAuth: true } - } - ``` -- [ ] Add menu item in `frontend/src/components/layout/HamburgerMenu.vue`: - - Section: "Rapoarte" - - Icon: `pi-calculator` or `pi-table` - - Label: "Balanță de Verificare" - - Route: `/trial-balance` -- [ ] Test navigation from menu - -**Files to Modify**: -- `reports-app/frontend/src/router/index.js` (MODIFY) -- `reports-app/frontend/src/components/layout/HamburgerMenu.vue` (MODIFY) - -**Acceptance Criteria**: -- ✅ Route is accessible at `/trial-balance` -- ✅ Menu item appears in hamburger menu -- ✅ Clicking menu item navigates to page -- ✅ Auth guard prevents unauthorized access - -**Estimated Time**: 30 minutes - ---- - -### ✅ Phase 5: Testing & Refinement -**Status**: 🔲 Not Started - -**Tasks**: -- [ ] Manual testing: - - Test with different companies - - Test with different date ranges - - Test filtering edge cases (empty results, special characters) - - Test sorting on all columns - - Test pagination with large datasets - - Test on mobile devices (responsive) - - Test error scenarios (API down, invalid token) -- [ ] Add Playwright E2E tests (optional, recommended): - - `frontend/tests/e2e/trial-balance/trial-balance.spec.js` - - Test navigation - - Test filtering - - Test data display -- [ ] Code review & refinement: - - Check CSS adherence (no `:deep()`, use patterns) - - Check error handling - - Check loading states - - Check accessibility (keyboard navigation, screen readers) -- [ ] Performance testing: - - Test with 10,000+ rows - - Check API response time - - Check frontend rendering performance - -**Files to Create** (optional): -- `reports-app/frontend/tests/e2e/trial-balance/trial-balance.spec.js` (NEW) - -**Acceptance Criteria**: -- ✅ All manual tests pass -- ✅ E2E tests pass (if implemented) -- ✅ Performance benchmarks met (API < 2s) -- ✅ Code follows project conventions -- ✅ CSS follows design system strictly - -**Estimated Time**: 2-3 hours - ---- - -### ✅ Phase 6: Documentation & PR -**Status**: 🔲 Not Started - -**Tasks**: -- [ ] Update `README.md` - add Trial Balance to features list -- [ ] Update `CLAUDE.md` - add Trial Balance to documentation index (if needed) -- [ ] Add API documentation in backend README -- [ ] Create PR from `feature/trial-balance` to `main`: - - Title: "feat: Add Trial Balance (Balanță de Verificare) page" - - Description: Link to this feature file, list phases completed - - Screenshots of UI - - API endpoint documentation -- [ ] Request code review -- [ ] Address review comments -- [ ] Merge to `main` - -**Acceptance Criteria**: -- ✅ All documentation updated -- ✅ PR created with complete description -- ✅ Code review approved -- ✅ Branch merged successfully - -**Estimated Time**: 1 hour - ---- - -## 📊 Progress Tracking - -### Overall Progress -- **Phase 0**: 🔲 Not Started (0%) -- **Phase 1**: 🔲 Not Started (0%) -- **Phase 2**: 🔲 Not Started (0%) -- **Phase 3**: 🔲 Not Started (0%) -- **Phase 4**: 🔲 Not Started (0%) -- **Phase 5**: 🔲 Not Started (0%) -- **Phase 6**: 🔲 Not Started (0%) - -**Total Progress**: 0% Complete - -### Status Legend -- 🔲 Not Started -- 🔄 In Progress -- ✅ Completed -- ⚠️ Blocked -- ❌ Cancelled - ---- - -## 🚀 Getting Started - -### Prerequisites -1. SSH tunnel running (`./ssh_tunnel.sh status`) -2. Backend running (`cd reports-app/backend && uvicorn app.main:app --reload --port 8001`) -3. Frontend running (`cd reports-app/frontend && npm run dev`) -4. Valid JWT token for testing API - -### Starting Development -```bash -# 1. Create feature branch -git checkout main -git pull origin main -git checkout -b feature/trial-balance - -# 2. Verify Oracle VBAL table -# Connect to Oracle and run: -# SELECT * FROM VBAL WHERE ROWNUM <= 10; - -# 3. Start backend (Phase 1) -cd reports-app/backend -# Create schemas/trial_balance.py -# Create routers/trial_balance.py -# Test with curl/Postman - -# 4. Start frontend (Phases 2-4) -cd reports-app/frontend -# Create stores/trialBalanceStore.js -# Create views/TrialBalanceView.vue -# Update router and menu - -# 5. Test and refine (Phase 5) -# Manual testing -# E2E tests (optional) - -# 6. Create PR (Phase 6) -git add . -git commit -m "feat: Add Trial Balance page with filtering and pagination" -git push origin feature/trial-balance -# Create PR on GitHub/GitLab -``` - ---- - -## 📚 Reference Documentation - -**MUST READ before coding**: -- `docs/ONBOARDING_CSS.md` - CSS quick start (5 min read) -- `docs/CSS_PATTERNS.md` - All available CSS patterns -- `docs/DESIGN_TOKENS.md` - Colors, spacing, typography -- `reports-app/frontend/README.md` - Frontend architecture -- `reports-app/backend/README.md` - Backend architecture - -**Similar implementations to reference**: -- `reports-app/frontend/src/views/InvoicesView.vue` - Table + filters pattern -- `reports-app/frontend/src/views/DashboardView.vue` - Cards + data display -- `reports-app/backend/app/routers/invoices.py` - API with filtering/pagination - ---- - -## ⚠️ Important Notes - -### CSS Architecture Compliance -**CRITICAL**: This feature MUST follow the established CSS architecture: - -✅ **DO**: -- Use existing `.roa-*` patterns from `CSS_PATTERNS.md` -- Use design tokens (`var(--color-primary)`, `var(--spacing-4)`) -- Use vendor overrides in `src/assets/css/vendor/primevue/` for PrimeVue -- Keep component styles minimal and semantic -- Follow BEM naming if creating new components - -❌ **DON'T**: -- Use `:deep()` in Vue components -- Hardcode colors or spacing values -- Duplicate existing patterns -- Create custom CSS without checking patterns first -- Override PrimeVue styles in component ` diff --git a/reports-app/frontend/tests/e2e/invoices/invoices.spec.js b/reports-app/frontend/tests/e2e/invoices/invoices.spec.js index 8736227..fdbf6bc 100644 --- a/reports-app/frontend/tests/e2e/invoices/invoices.spec.js +++ b/reports-app/frontend/tests/e2e/invoices/invoices.spec.js @@ -36,12 +36,29 @@ test.describe('Invoices View', () => { }); }); - // Mock invoices endpoint - await page.route('**/api/invoices/COMP1', async route => { + // Mock invoices endpoint - FIX: Use query parameters instead of path parameter + await page.route('**/api/invoices**', async route => { + const url = route.request().url(); + const urlParams = new URL(url).searchParams; + const partnerType = urlParams.get('partner_type') || 'CLIENTI'; + + // Return different data based on partner_type + const invoicesData = partnerType === 'CLIENTI' + ? mockInvoices.filter(inv => inv.type === 'client') + : mockInvoices.filter(inv => inv.type === 'supplier'); + await route.fulfill({ status: 200, contentType: 'application/json', - body: JSON.stringify(mockInvoices), + body: JSON.stringify({ + invoices: invoicesData, + total_count: invoicesData.length, + filtered_count: invoicesData.length, + total_amount: invoicesData.reduce((sum, inv) => sum + inv.totctva, 0), + page: parseInt(urlParams.get('page') || '1'), + page_size: parseInt(urlParams.get('page_size') || '50'), + has_more: false + }), }); }); @@ -203,12 +220,225 @@ test.describe('Invoices View', () => { await invoicesPage.waitForPageLoad(); await invoicesPage.selectCompany('Compania Test 1'); await page.waitForSelector(invoicesPage.invoicesTable); - + // Click refresh button await invoicesPage.clickRefreshButton(); await invoicesPage.waitForLoadingToFinish(); - + // Table should still be visible after refresh expect(await invoicesPage.isInvoicesTableVisible()).toBe(true); }); + + // NEW TESTS for fixed issues + + test('should filter by invoice type (CLIENTI/FURNIZORI)', async ({ page }) => { + let capturedPartnerType = null; + + // Intercept API requests to verify partner_type parameter + await page.route('**/api/invoices**', async route => { + const url = route.request().url(); + const urlParams = new URL(url).searchParams; + capturedPartnerType = urlParams.get('partner_type'); + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + invoices: [], + total_count: 0, + filtered_count: 0, + total_amount: 0, + page: 1, + page_size: 50, + has_more: false + }), + }); + }); + + await invoicesPage.waitForPageLoad(); + await invoicesPage.selectCompany('Compania Test 1'); + await page.waitForSelector(invoicesPage.invoicesTable); + + // Select FURNIZORI from dropdown + await page.locator('[placeholder="Tip factură"]').click(); + await page.locator('.p-dropdown-item').filter({ hasText: 'Furnizori' }).click(); + await page.waitForTimeout(1000); // Wait for API call + + // Verify partner_type parameter was sent correctly + expect(capturedPartnerType).toBe('FURNIZORI'); + }); + + test('should filter by cont (account number)', async ({ page }) => { + let capturedCont = null; + + // Intercept API requests to verify cont parameter + await page.route('**/api/invoices**', async route => { + const url = route.request().url(); + const urlParams = new URL(url).searchParams; + capturedCont = urlParams.get('cont'); + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + invoices: [], + total_count: 0, + filtered_count: 0, + total_amount: 0, + page: 1, + page_size: 50, + has_more: false + }), + }); + }); + + await invoicesPage.waitForPageLoad(); + await invoicesPage.selectCompany('Compania Test 1'); + await page.waitForSelector(invoicesPage.invoicesTable); + + // Enter cont filter + await page.locator('[placeholder="Filtru cont (ex: 4111)"]').fill('4111'); + await page.waitForTimeout(1000); // Wait for debounced API call + + // Verify cont parameter was sent correctly + expect(capturedCont).toBe('4111'); + }); + + test('should use partner_name parameter for search', async ({ page }) => { + let capturedPartnerName = null; + let capturedSearchParam = null; + + // Intercept API requests to verify correct parameter name + await page.route('**/api/invoices**', async route => { + const url = route.request().url(); + const urlParams = new URL(url).searchParams; + capturedPartnerName = urlParams.get('partner_name'); + capturedSearchParam = urlParams.get('search'); + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + invoices: [], + total_count: 0, + filtered_count: 0, + total_amount: 0, + page: 1, + page_size: 50, + has_more: false + }), + }); + }); + + await invoicesPage.waitForPageLoad(); + await invoicesPage.selectCompany('Compania Test 1'); + await page.waitForSelector(invoicesPage.invoicesTable); + + // Search for partner name + await page.locator('[placeholder="Căutați după număr, partener..."]').fill('Test Partner'); + await page.waitForTimeout(1000); // Wait for debounced API call + + // Verify partner_name parameter was sent (not search) + expect(capturedPartnerName).toBe('Test Partner'); + expect(capturedSearchParam).toBeNull(); + }); + + test('should export XLSX with all filters applied', async ({ page }) => { + let exportRequestParams = null; + + // Intercept export API request + await page.route('**/api/invoices**', async route => { + const url = route.request().url(); + const urlParams = new URL(url).searchParams; + + // Capture params if it's the export request (page_size = 999999) + if (urlParams.get('page_size') === '999999') { + exportRequestParams = { + partner_type: urlParams.get('partner_type'), + partner_name: urlParams.get('partner_name'), + cont: urlParams.get('cont'), + page_size: urlParams.get('page_size') + }; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + invoices: [ + { + cont: '4111', + nract: 'INV001', + dataact: '2024-01-01', + datascad: '2024-02-01', + nume: 'Test Client', + totctva: 1000, + achitat: 500, + soldfinal: 500 + } + ], + total_count: 1, + filtered_count: 1, + total_amount: 1000, + page: 1, + page_size: 999999, + has_more: false + }), + }); + }); + + await invoicesPage.waitForPageLoad(); + await invoicesPage.selectCompany('Compania Test 1'); + await page.waitForSelector(invoicesPage.invoicesTable); + + // Apply filters before export + await page.locator('[placeholder="Tip factură"]').click(); + await page.locator('.p-dropdown-item').filter({ hasText: 'Furnizori' }).click(); + await page.locator('[placeholder="Filtru cont (ex: 4111)"]').fill('4111'); + await page.waitForTimeout(500); + + // Click Excel export + const downloadPromise = page.waitForEvent('download', { timeout: 10000 }).catch(() => null); + await page.locator('button:has-text("Export Excel")').click(); + await page.waitForTimeout(2000); // Wait for export to complete + + // Verify export request included all filters + expect(exportRequestParams).toBeTruthy(); + expect(exportRequestParams.partner_type).toBe('FURNIZORI'); + expect(exportRequestParams.cont).toBe('4111'); + expect(exportRequestParams.page_size).toBe('999999'); + + // Download may or may not occur due to mock, but we verified the API call + await downloadPromise; + }); + + test('should have hover effect on table rows', async ({ page }) => { + await invoicesPage.waitForPageLoad(); + await invoicesPage.selectCompany('Compania Test 1'); + await page.waitForSelector(invoicesPage.invoicesTable); + + // Wait for table rows to load + const firstRow = page.locator('.p-datatable-tbody tr').first(); + await firstRow.waitFor(); + + // Get initial background color + const initialBgColor = await firstRow.evaluate(el => + window.getComputedStyle(el).backgroundColor + ); + + // Hover over the row + await firstRow.hover(); + await page.waitForTimeout(300); // Wait for transition + + // Get background color after hover + const hoverBgColor = await firstRow.evaluate(el => + window.getComputedStyle(el).backgroundColor + ); + + // Background color should change on hover + expect(hoverBgColor).not.toBe(initialBgColor); + + // Verify hover color is the expected blue (#e3f2fd = rgb(227, 242, 253)) + expect(hoverBgColor).toBe('rgb(227, 242, 253)'); + }); }); \ No newline at end of file