Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot

Modern ERP Reports Application with microservices architecture

Tech Stack:
- Backend: FastAPI + python-oracledb (Oracle DB integration)
- Frontend: Vue.js 3 + PrimeVue + Vite
- Telegram Bot: python-telegram-bot + SQLite
- Infrastructure: Shared database pool, JWT authentication, SSH tunnel

Features:
- FastAPI backend with async Oracle connection pool
- Vue.js 3 responsive frontend with PrimeVue components
- Telegram bot alternative interface
- Microservices architecture with shared components
- Complete deployment support (Linux Docker + Windows IIS)
- Comprehensive testing (Playwright E2E + pytest)

Repository Structure:
- reports-app/ - Main application (backend, frontend, telegram-bot)
- shared/ - Shared components (database pool, auth, utils)
- deployment/ - Deployment scripts (Linux & Windows)
- docs/ - Project documentation
- security/ - Security scanning and git hooks
This commit is contained in:
2025-10-25 14:55:08 +03:00
commit 6b13ffa183
237 changed files with 70035 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
import { BasePage } from './BasePage.js';
export class PaymentsPage extends BasePage {
constructor(page) {
super(page);
// Page selectors
this.pageTitle = '.page-title';
this.pageSubtitle = '.page-subtitle';
// Company selection
this.companySelectionCard = '.company-selection-card';
this.companyDropdown = '.company-selection .p-dropdown';
this.companyDropdownTrigger = '.company-selection .p-dropdown-trigger';
this.companyOptions = '.p-dropdown-item';
// Search and filters
this.searchInput = '.search-input input';
this.methodFilter = '.method-filter .p-dropdown';
this.methodFilterTrigger = '.method-filter .p-dropdown-trigger';
this.dateRangeFilter = '.date-range-filter .p-dropdown';
this.dateRangeFilterTrigger = '.date-range-filter .p-dropdown-trigger';
this.refreshButton = '.refresh-button';
this.exportButton = '.export-button';
// View toggles
this.tableViewButton = '.table-view-button';
this.summaryViewButton = '.summary-view-button';
// Table selectors
this.paymentsTable = '.payments-table';
this.tableRows = '.payments-table tbody tr';
this.tableHeaders = '.payments-table thead th';
this.loadingSpinner = '.p-datatable-loading';
// Summary view
this.summaryView = '.payments-summary-view';
this.methodSummaryCards = '.method-summary-card';
// Totals card
this.totalsCard = '.payments-totals-card';
this.totalAmount = '.total-amount .amount-value';
this.totalCount = '.total-count .count-value';
// Pagination
this.pagination = '.p-paginator';
this.nextPageButton = '.p-paginator-next';
this.prevPageButton = '.p-paginator-prev';
this.currentPageSpan = '.p-paginator-current';
// Payment details
this.paymentDetailsModal = '.payment-details-modal';
this.paymentDetailsPanel = '.payment-details-panel';
// Specific table columns (adjust based on actual implementation)
this.referenceColumn = 'td:nth-child(1)';
this.dateColumn = 'td:nth-child(2)';
this.clientColumn = 'td:nth-child(3)';
this.amountColumn = 'td:nth-child(4)';
this.methodColumn = 'td:nth-child(5)';
}
async navigate() {
await this.page.goto('/payments');
await this.page.waitForSelector(this.pageTitle);
}
async isOnPaymentsPage() {
return await this.page.locator(this.pageTitle).isVisible();
}
async getPageTitle() {
return await this.page.locator(this.pageTitle).textContent();
}
async isCompanySelectionVisible() {
return await this.page.locator(this.companySelectionCard).isVisible();
}
async isPaymentsTableVisible() {
return await this.page.locator(this.paymentsTable).isVisible();
}
async selectCompany(companyName) {
await this.page.click(this.companyDropdownTrigger);
await this.page.waitForSelector(this.companyOptions);
await this.page.click(`${this.companyOptions}:has-text("${companyName}")`);
await this.waitForLoadingToFinish();
}
async searchPayments(searchTerm) {
await this.page.fill(this.searchInput, searchTerm);
await this.page.press(this.searchInput, 'Enter');
}
async filterByMethod(method) {
await this.page.click(this.methodFilterTrigger);
await this.page.waitForSelector(this.companyOptions);
// Map method to Romanian text (adjust based on actual implementation)
const methodMap = {
'bank_transfer': 'Transfer bancar',
'cash': 'Numerar',
'card': 'Card',
'check': 'Cec'
};
const methodText = methodMap[method] || method;
await this.page.click(`${this.companyOptions}:has-text("${methodText}")`);
}
async filterByDateRange(range) {
await this.page.click(this.dateRangeFilterTrigger);
await this.page.waitForSelector(this.companyOptions);
// Map range to Romanian text
const rangeMap = {
'thisMonth': 'Această lună',
'lastMonth': 'Luna trecută',
'thisYear': 'Acest an',
'custom': 'Personalizat'
};
const rangeText = rangeMap[range] || range;
await this.page.click(`${this.companyOptions}:has-text("${rangeText}")`);
}
async sortByColumn(columnName) {
// Map column names to actual header text
const columnMap = {
'reference': 'Referință',
'date': 'Data',
'client': 'Client',
'amount': 'Sumă',
'method': 'Metodă'
};
const headerText = columnMap[columnName] || columnName;
await this.page.click(`${this.tableHeaders}:has-text("${headerText}")`);
}
async getVisiblePaymentsCount() {
return await this.page.locator(this.tableRows).count();
}
async getFirstPaymentData() {
const firstRow = this.page.locator(this.tableRows).first();
return {
reference: await firstRow.locator(this.referenceColumn).textContent(),
date: await firstRow.locator(this.dateColumn).textContent(),
client: await firstRow.locator(this.clientColumn).textContent(),
amount: await firstRow.locator(this.amountColumn).textContent(),
method: await firstRow.locator(this.methodColumn).textContent()
};
}
async clickFirstPaymentRow() {
await this.page.locator(this.tableRows).first().click();
}
async isPaymentDetailsVisible() {
const modalVisible = await this.page.locator(this.paymentDetailsModal).isVisible();
const panelVisible = await this.page.locator(this.paymentDetailsPanel).isVisible();
return modalVisible || panelVisible;
}
async clickExportButton() {
await this.page.click(this.exportButton);
}
async clickRefreshButton() {
await this.page.click(this.refreshButton);
}
async isTotalsCardVisible() {
return await this.page.locator(this.totalsCard).isVisible();
}
async getTotalsData() {
return {
totalAmount: await this.page.locator(this.totalAmount).textContent(),
totalCount: await this.page.locator(this.totalCount).textContent()
};
}
async isSummaryViewAvailable() {
return await this.page.locator(this.summaryViewButton).isVisible();
}
async switchToSummaryView() {
await this.page.click(this.summaryViewButton);
}
async switchToTableView() {
await this.page.click(this.tableViewButton);
}
async isSummaryViewVisible() {
return await this.page.locator(this.summaryView).isVisible();
}
async isPaginationVisible() {
return await this.page.locator(this.pagination).isVisible();
}
async goToNextPage() {
await this.page.click(this.nextPageButton);
}
async goToPrevPage() {
await this.page.click(this.prevPageButton);
}
async getCurrentPage() {
const pageText = await this.page.locator(this.currentPageSpan).textContent();
// Extract page number from text like "Page 2 of 5"
const match = pageText.match(/(\d+)/);
return match ? parseInt(match[1]) : 1;
}
async waitForPageLoad() {
await Promise.race([
this.page.waitForSelector(this.companySelectionCard, { timeout: 10000 }),
this.page.waitForSelector(this.paymentsTable, { timeout: 10000 })
]);
}
async waitForTableLoad() {
await this.page.waitForSelector(this.paymentsTable, { timeout: 10000 });
await this.waitForLoadingToFinish();
}
async getPaymentByReference(reference) {
const rows = this.page.locator(this.tableRows);
const rowCount = await rows.count();
for (let i = 0; i < rowCount; i++) {
const row = rows.nth(i);
const ref = await row.locator(this.referenceColumn).textContent();
if (ref.trim() === reference) {
return row;
}
}
return null;
}
async clickPaymentByReference(reference) {
const row = await this.getPaymentByReference(reference);
if (row) {
await row.click();
}
}
async getMethodSummaryData() {
const summaryCards = this.page.locator(this.methodSummaryCards);
const cardCount = await summaryCards.count();
const data = [];
for (let i = 0; i < cardCount; i++) {
const card = summaryCards.nth(i);
const method = await card.locator('.method-name').textContent();
const amount = await card.locator('.method-amount').textContent();
const count = await card.locator('.method-count').textContent();
data.push({ method, amount, count });
}
return data;
}
}