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,376 @@
<template>
<div class="app-container">
<div class="register-view">
<!-- Header -->
<div class="view-header">
<h1 class="page-title">
<i class="pi pi-wallet"></i>
Registrul de Casă și Bancă
</h1>
<p class="page-subtitle">
Vizualizați toate mișcările din conturile de bancă și casă
</p>
</div>
<!-- Filters -->
<Card class="filters-card">
<template #content>
<div class="filters-grid">
<div class="filter-item">
<label>Data început</label>
<Calendar v-model="filters.dateFrom" dateFormat="dd/mm/yy" />
</div>
<div class="filter-item">
<label>Data sfârșit</label>
<Calendar v-model="filters.dateTo" dateFormat="dd/mm/yy" />
</div>
<div class="filter-item">
<label>Căutare partener</label>
<InputText v-model="filters.partnerName" placeholder="Nume partener..." />
</div>
<div class="filter-actions">
<Button
label="Aplică Filtre"
icon="pi pi-filter"
@click="applyFilters"
/>
<Button
label="Resetează"
icon="pi pi-times"
class="p-button-secondary"
@click="resetFilters"
/>
</div>
</div>
</template>
</Card>
<!-- Summary Stats -->
<div class="summary-stats">
<Card class="stat-card">
<template #content>
<div class="stat-content">
<div class="stat-icon green">
<i class="pi pi-arrow-down"></i>
</div>
<div class="stat-details">
<h3 class="stat-value">{{ formatCurrency(treasuryStore.totals.total_incasari) }}</h3>
<p class="stat-label">Total Încasări</p>
</div>
</div>
</template>
</Card>
<Card class="stat-card">
<template #content>
<div class="stat-content">
<div class="stat-icon red">
<i class="pi pi-arrow-up"></i>
</div>
<div class="stat-details">
<h3 class="stat-value">{{ formatCurrency(treasuryStore.totals.total_plati) }}</h3>
<p class="stat-label">Total Plăți</p>
</div>
</div>
</template>
</Card>
</div>
<!-- Data Table -->
<Card class="data-card">
<template #content>
<DataTable
:value="treasuryStore.registers"
:loading="treasuryStore.isLoading"
:paginator="true"
:rows="pagination.rows"
:total-records="treasuryStore.pagination.totalRecords"
:lazy="true"
@page="onPage"
class="p-datatable-sm"
:rowClass="getRowClass"
>
<Column field="dataact" header="Data">
<template #body="slotProps">
{{ formatDate(slotProps.data.dataact) }}
</template>
</Column>
<Column field="nract" header="Nr. Act" style="width: 100px" />
<Column field="nume" header="Partener" />
<Column field="nume_cont_bancar" header="Cont" />
<Column field="tip_registru" header="Tip">
<template #body="slotProps">
<Tag :value="slotProps.data.tip_registru" :severity="getRegisterSeverity(slotProps.data.tip_registru)" />
</template>
</Column>
<Column field="incasari" header="Încasări">
<template #body="slotProps">
<span class="amount-green" v-if="slotProps.data.incasari > 0">
{{ formatCurrency(slotProps.data.incasari, slotProps.data.valuta) }}
</span>
<span v-else>-</span>
</template>
</Column>
<Column field="plati" header="Plăți">
<template #body="slotProps">
<span class="amount-red" v-if="slotProps.data.plati > 0">
{{ formatCurrency(slotProps.data.plati, slotProps.data.valuta) }}
</span>
<span v-else>-</span>
</template>
</Column>
<Column field="sold" header="Sold">
<template #body="slotProps">
<span :class="slotProps.data.sold >= 0 ? 'amount-green' : 'amount-red'">
{{ formatCurrency(slotProps.data.sold, slotProps.data.valuta) }}
</span>
</template>
</Column>
<Column field="explicatia" header="Explicație" />
</DataTable>
</template>
</Card>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useTreasuryStore } from "../stores/treasury";
import { useCompanyStore } from "../stores/companies";
import { format } from "date-fns";
import { ro } from "date-fns/locale";
const treasuryStore = useTreasuryStore();
const companyStore = useCompanyStore();
const filters = ref({
dateFrom: null,
dateTo: null,
partnerName: ""
});
const pagination = ref({
page: 0,
rows: 50
});
const formatCurrency = (amount, currency = 'RON') => {
if (!amount) return "0,00 " + currency;
return new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: currency
}).format(amount);
};
const formatDate = (dateString) => {
if (!dateString) return "";
return format(new Date(dateString), "dd MMM yyyy", { locale: ro });
};
const getRowClass = (data) => {
return data.tip_registru.includes('BANCA') ? 'bank-row' : 'cash-row';
};
const getRegisterSeverity = (type) => {
if (type.includes('BANCA')) return 'info';
if (type.includes('CASA')) return 'warning';
return null;
};
const onPage = (event) => {
pagination.value = event;
loadData();
};
const applyFilters = () => {
pagination.value.page = 0;
loadData();
};
const resetFilters = () => {
filters.value = {
dateFrom: null,
dateTo: null,
partnerName: ""
};
loadData();
};
const loadData = async () => {
if (!companyStore.selectedCompany) return;
treasuryStore.setPagination(pagination.value);
await treasuryStore.loadBankCashRegister(
companyStore.selectedCompany.id_firma,
{
date_from: filters.value.dateFrom?.toISOString().split("T")[0],
date_to: filters.value.dateTo?.toISOString().split("T")[0],
partner_name: filters.value.partnerName
}
);
};
onMounted(() => {
if (companyStore.selectedCompany) {
loadData();
}
});
</script>
<style scoped>
.app-container {
padding: 1.5rem;
max-width: 1400px;
margin: 0 auto;
}
.view-header {
margin-bottom: 2rem;
}
.page-title {
color: var(--primary-color);
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-subtitle {
color: var(--text-color-secondary);
font-size: 1.1rem;
margin: 0;
}
.filters-card {
margin-bottom: 2rem;
}
.filters-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
align-items: end;
}
.filter-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-item label {
font-weight: 600;
color: var(--text-color);
}
.filter-actions {
display: flex;
gap: 0.75rem;
justify-content: flex-start;
}
.summary-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
border-radius: 12px;
border: 1px solid var(--surface-border);
overflow: hidden;
}
.stat-content {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
}
.stat-icon.green {
background: linear-gradient(135deg, var(--green-500), var(--green-600));
}
.stat-icon.red {
background: linear-gradient(135deg, var(--red-500), var(--red-600));
}
.stat-details h3 {
font-size: 1.5rem;
font-weight: 700;
margin: 0;
color: var(--text-color);
}
.stat-details p {
font-size: 0.875rem;
color: var(--text-color-secondary);
margin: 0.25rem 0 0 0;
}
.data-card {
border-radius: 12px;
border: 1px solid var(--surface-border);
}
.amount-green {
color: var(--green-600);
font-weight: 600;
}
.amount-red {
color: var(--red-600);
font-weight: 600;
}
:deep(.bank-row) {
background-color: var(--blue-50);
}
:deep(.cash-row) {
background-color: var(--yellow-50);
}
@media (max-width: 768px) {
.app-container {
padding: 1rem;
}
.page-title {
font-size: 1.75rem;
}
.filters-grid {
grid-template-columns: 1fr;
}
.summary-stats {
grid-template-columns: 1fr;
}
.filter-actions {
justify-content: stretch;
}
.filter-actions .p-button {
flex: 1;
}
}
</style>