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,37 @@
export class BasePage {
constructor(page) {
this.page = page;
}
async waitForApiResponse(url, status = 200) {
return await this.page.waitForResponse(response =>
response.url().includes(url) && response.status() === status
);
}
async waitForLoadingToFinish() {
// Wait for any loading spinners to disappear
await this.page.waitForFunction(() => {
const loadingElements = document.querySelectorAll('[data-testid="loading"], .p-progress-spinner');
return loadingElements.length === 0;
}, { timeout: 10000 });
}
async checkErrorMessage(expectedMessage) {
const errorElement = this.page.locator('.p-message-error, [data-testid="error"]');
await errorElement.waitFor({ state: 'visible', timeout: 5000 });
const actualMessage = await errorElement.textContent();
return actualMessage.includes(expectedMessage);
}
async checkSuccessMessage(expectedMessage) {
const successElement = this.page.locator('.p-message-success, [data-testid="success"]');
await successElement.waitFor({ state: 'visible', timeout: 5000 });
const actualMessage = await successElement.textContent();
return actualMessage.includes(expectedMessage);
}
async waitForNavigation() {
await this.page.waitForLoadState('networkidle');
}
}

View File

@@ -0,0 +1,129 @@
import { BasePage } from './BasePage.js';
export class DashboardPage extends BasePage {
constructor(page) {
super(page);
// Header selectors
this.pageTitle = '.page-title';
this.pageSubtitle = '.page-subtitle';
this.userWelcome = '.page-subtitle';
// Company selection selectors
this.companySelectionCard = '.company-selection-card';
this.companyDropdown = '.company-selection .p-dropdown';
this.companyDropdownTrigger = '.company-selection .p-dropdown-trigger';
this.companyOptions = '.p-dropdown-item';
// Stats cards selectors
this.statsGrid = '.stats-grid';
this.invoicesStatCard = '.stat-card.stat-invoices';
this.paymentsStatCard = '.stat-card.stat-payments';
this.companyStatCard = '.stat-card.stat-company';
// Stat values
this.invoicesTotal = '.stat-invoices .stat-value';
this.paymentsTotal = '.stat-payments .stat-value';
this.companyName = '.stat-company .stat-value';
// Quick actions
this.quickActionsCard = '.quick-actions-card';
this.invoicesActionButton = 'button:has-text("Facturi")';
this.paymentsActionButton = 'button:has-text("Încasări")';
// Navigation
this.dashboardContent = '.dashboard-content';
}
async navigate() {
await this.page.goto('/dashboard');
await this.page.waitForSelector(this.pageTitle);
}
async isOnDashboardPage() {
return await this.page.locator(this.pageTitle).isVisible();
}
async getPageTitle() {
return await this.page.locator(this.pageTitle).textContent();
}
async getWelcomeMessage() {
return await this.page.locator(this.userWelcome).textContent();
}
async isCompanySelectionVisible() {
return await this.page.locator(this.companySelectionCard).isVisible();
}
async isDashboardContentVisible() {
return await this.page.locator(this.dashboardContent).isVisible();
}
async selectCompany(companyName) {
// Click dropdown to open options
await this.page.click(this.companyDropdownTrigger);
// Wait for options to appear and select the company
await this.page.waitForSelector(this.companyOptions);
await this.page.click(`${this.companyOptions}:has-text("${companyName}")`);
// Wait for selection to be processed
await this.waitForLoadingToFinish();
}
async getSelectedCompanyName() {
if (await this.page.locator(this.companyName).isVisible()) {
return await this.page.locator(this.companyName).textContent();
}
return null;
}
async getInvoicesCount() {
if (await this.page.locator(this.invoicesTotal).isVisible()) {
return await this.page.locator(this.invoicesTotal).textContent();
}
return '0';
}
async getPaymentsCount() {
if (await this.page.locator(this.paymentsTotal).isVisible()) {
return await this.page.locator(this.paymentsTotal).textContent();
}
return '0';
}
async clickInvoicesAction() {
await this.page.click(this.invoicesActionButton);
await this.waitForNavigation();
}
async clickPaymentsAction() {
await this.page.click(this.paymentsActionButton);
await this.waitForNavigation();
}
async areStatsCardsVisible() {
const invoicesVisible = await this.page.locator(this.invoicesStatCard).isVisible();
const paymentsVisible = await this.page.locator(this.paymentsStatCard).isVisible();
const companyVisible = await this.page.locator(this.companyStatCard).isVisible();
return invoicesVisible && paymentsVisible && companyVisible;
}
async getStatsData() {
return {
invoices: await this.getInvoicesCount(),
payments: await this.getPaymentsCount(),
company: await this.getSelectedCompanyName()
};
}
async waitForDashboardLoad() {
// Wait for either company selection or dashboard content to appear
await Promise.race([
this.page.waitForSelector(this.companySelectionCard, { timeout: 10000 }),
this.page.waitForSelector(this.dashboardContent, { timeout: 10000 })
]);
}
}

View File

@@ -0,0 +1,195 @@
import { BasePage } from './BasePage.js';
export class InvoicesPage 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.statusFilter = '.status-filter .p-dropdown';
this.statusFilterTrigger = '.status-filter .p-dropdown-trigger';
this.refreshButton = '.refresh-button';
this.exportButton = '.export-button';
// Table selectors
this.invoicesTable = '.invoices-table';
this.tableRows = '.invoices-table tbody tr';
this.tableHeaders = '.invoices-table thead th';
this.loadingSpinner = '.p-datatable-loading';
// Pagination
this.pagination = '.p-paginator';
this.nextPageButton = '.p-paginator-next';
this.prevPageButton = '.p-paginator-prev';
this.currentPageSpan = '.p-paginator-current';
// Invoice details
this.invoiceDetailsModal = '.invoice-details-modal';
this.invoiceDetailsPanel = '.invoice-details-panel';
// Specific table columns (adjust based on actual implementation)
this.numberColumn = 'td:nth-child(1)';
this.dateColumn = 'td:nth-child(2)';
this.clientColumn = 'td:nth-child(3)';
this.amountColumn = 'td:nth-child(4)';
this.statusColumn = 'td:nth-child(5)';
}
async navigate() {
await this.page.goto('/invoices');
await this.page.waitForSelector(this.pageTitle);
}
async isOnInvoicesPage() {
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 isInvoicesTableVisible() {
return await this.page.locator(this.invoicesTable).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 searchInvoices(searchTerm) {
await this.page.fill(this.searchInput, searchTerm);
await this.page.press(this.searchInput, 'Enter');
}
async filterByStatus(status) {
await this.page.click(this.statusFilterTrigger);
await this.page.waitForSelector(this.companyOptions);
// Map status to Romanian text (adjust based on actual implementation)
const statusMap = {
'paid': 'Plătit',
'unpaid': 'Neplătit',
'overdue': 'Întârziat'
};
const statusText = statusMap[status] || status;
await this.page.click(`${this.companyOptions}:has-text("${statusText}")`);
}
async sortByColumn(columnName) {
// Map column names to actual header text
const columnMap = {
'number': 'Număr',
'date': 'Data',
'client': 'Client',
'amount': 'Sumă',
'status': 'Status'
};
const headerText = columnMap[columnName] || columnName;
await this.page.click(`${this.tableHeaders}:has-text("${headerText}")`);
}
async getVisibleInvoicesCount() {
return await this.page.locator(this.tableRows).count();
}
async getFirstInvoiceData() {
const firstRow = this.page.locator(this.tableRows).first();
return {
number: await firstRow.locator(this.numberColumn).textContent(),
date: await firstRow.locator(this.dateColumn).textContent(),
client: await firstRow.locator(this.clientColumn).textContent(),
amount: await firstRow.locator(this.amountColumn).textContent(),
status: await firstRow.locator(this.statusColumn).textContent()
};
}
async clickFirstInvoiceRow() {
await this.page.locator(this.tableRows).first().click();
}
async isInvoiceDetailsVisible() {
const modalVisible = await this.page.locator(this.invoiceDetailsModal).isVisible();
const panelVisible = await this.page.locator(this.invoiceDetailsPanel).isVisible();
return modalVisible || panelVisible;
}
async clickExportButton() {
await this.page.click(this.exportButton);
}
async clickRefreshButton() {
await this.page.click(this.refreshButton);
}
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.invoicesTable, { timeout: 10000 })
]);
}
async waitForTableLoad() {
await this.page.waitForSelector(this.invoicesTable, { timeout: 10000 });
await this.waitForLoadingToFinish();
}
async getInvoiceByNumber(invoiceNumber) {
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 number = await row.locator(this.numberColumn).textContent();
if (number.trim() === invoiceNumber) {
return row;
}
}
return null;
}
async clickInvoiceByNumber(invoiceNumber) {
const row = await this.getInvoiceByNumber(invoiceNumber);
if (row) {
await row.click();
}
}
}

View File

@@ -0,0 +1,99 @@
import { BasePage } from './BasePage.js';
export class LoginPage extends BasePage {
constructor(page) {
super(page);
// Selectors
this.usernameInput = '#username';
this.passwordInput = '#password input';
this.loginButton = 'button[type="submit"]';
this.errorMessage = '.error-message';
this.loadingSpinner = '.p-button-loading';
this.loginTitle = '.login-title';
this.loginCard = '.login-card';
// Form validation selectors
this.usernameError = '.field:has(#username) .p-error';
this.passwordError = '.field:has(#password) .p-error';
this.invalidField = '.p-invalid';
}
async navigate() {
await this.page.goto('/');
await this.page.waitForSelector(this.loginCard);
}
async fillCredentials(username, password) {
await this.page.fill(this.usernameInput, username);
await this.page.fill(this.passwordInput, password);
}
async clickLogin() {
await this.page.click(this.loginButton);
}
async login(username, password) {
await this.fillCredentials(username, password);
await this.clickLogin();
}
async waitForLoginResult() {
// Wait for either redirect to dashboard or error message
try {
await Promise.race([
this.page.waitForURL('/dashboard', { timeout: 5000 }),
this.page.waitForSelector(this.errorMessage, { timeout: 5000 })
]);
} catch (error) {
// Continue - we'll check the state separately
}
}
async isOnLoginPage() {
return await this.page.locator(this.loginTitle).isVisible();
}
async isLoginButtonDisabled() {
return await this.page.locator(this.loginButton).isDisabled();
}
async isLoading() {
return await this.page.locator(this.loadingSpinner).isVisible();
}
async getErrorMessage() {
const errorElement = this.page.locator(this.errorMessage);
if (await errorElement.isVisible()) {
return await errorElement.textContent();
}
return null;
}
async getFieldError(field) {
const selector = field === 'username' ? this.usernameError : this.passwordError;
const errorElement = this.page.locator(selector);
if (await errorElement.isVisible()) {
return await errorElement.textContent();
}
return null;
}
async hasInvalidField() {
return await this.page.locator(this.invalidField).count() > 0;
}
async clearForm() {
await this.page.fill(this.usernameInput, '');
await this.page.fill(this.passwordInput, '');
}
async validateFormFields() {
// Trigger validation by clicking outside fields
await this.page.click(this.loginCard);
}
async getPageTitle() {
return await this.page.locator(this.loginTitle).textContent();
}
}

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;
}
}