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,373 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { apiService } from "../services/api";
export const useDashboardStore = defineStore("dashboard", () => {
// State existent
const summary = ref(null);
const trends = ref(null);
const isLoading = ref(false);
const error = ref(null);
// State nou pentru carduri
const performanceData = ref({});
const cashflowData = ref({});
const maturityData = ref({});
const currentPeriod = ref(null);
// State pentru detailed data pagination
const detailedDataTotal = ref(0);
// Cache pentru date
const dataCache = new Map();
const loadDashboardSummary = async (companyId) => {
isLoading.value = true;
error.value = null;
try {
const response = await apiService.get('/dashboard/summary', {
params: { company: companyId }
});
summary.value = response.data;
return { success: true };
} catch (err) {
error.value = err.response?.data?.detail || "Failed to load dashboard";
console.error("Failed to load dashboard:", err);
return { success: false, error: error.value };
} finally {
isLoading.value = false;
}
};
const loadTrendData = async (companyId, period = '12m', chartType = 'line') => {
isLoading.value = true;
error.value = null;
try {
console.log(`Loading trend data for company ${companyId}, period: ${period}`);
const response = await apiService.get('/dashboard/trends', {
params: {
company: companyId,
period: period
}
});
// Validate response structure
if (!response.data) {
throw new Error('Empty response from trends API');
}
console.log('Raw trends response:', response.data);
// Transform backend response to Chart.js format
const backendData = response.data;
const transformedData = transformTrendsData(backendData);
if (!transformedData) {
throw new Error('Failed to transform trends data - invalid format');
}
trends.value = transformedData;
console.log('Transformed trends data:', transformedData);
return { success: true, data: transformedData };
} catch (err) {
const errorMessage = err.response?.data?.detail || err.message || "Failed to load trend data";
error.value = errorMessage;
console.error("Failed to load trend data:", err);
console.error("Error details:", {
status: err.response?.status,
statusText: err.response?.statusText,
data: err.response?.data
});
// Clear trends data and return error - no more mock data
trends.value = null;
return { success: false, error: error.value };
} finally {
isLoading.value = false;
}
};
// Transform backend trends data to Chart.js format AND preserve raw data
const transformTrendsData = (backendData) => {
if (!backendData || !backendData.periods || !Array.isArray(backendData.periods) || backendData.periods.length === 0) {
console.warn('Invalid trends data received:', backendData);
return null;
}
// Validate that we have all required data
const requiredFields = ['trezorerie_sold', 'clienti_sold', 'furnizori_sold', 'clienti_incasat', 'furnizori_achitat'];
for (const field of requiredFields) {
if (!backendData[field] || !Array.isArray(backendData[field])) {
console.warn(`Missing ${field} data`);
return null;
}
}
// Data is already in ASC order from backend
const periods = [...backendData.periods];
// Format labels for monthly data (YYYY-MM -> MM/YYYY)
const formattedPeriods = periods.map(period => {
const [year, month] = period.split('-');
const date = new Date(year, month - 1);
return date.toLocaleDateString('ro-RO', { month: '2-digit', year: 'numeric' });
});
// Preserve all raw data from backend for card calculations
return {
labels: formattedPeriods,
raw: {
// Current period data
periods: backendData.periods,
clienti_facturat: backendData.clienti_facturat || [],
clienti_incasat: backendData.clienti_incasat || [],
clienti_sold: backendData.clienti_sold || [],
furnizori_facturat: backendData.furnizori_facturat || [],
furnizori_achitat: backendData.furnizori_achitat || [],
furnizori_sold: backendData.furnizori_sold || [],
trezorerie_sold: backendData.trezorerie_sold || [],
// Previous period data (year-over-year comparison)
previous_periods: backendData.previous_periods || [],
clienti_facturat_prev: backendData.clienti_facturat_prev || [],
clienti_incasat_prev: backendData.clienti_incasat_prev || [],
clienti_sold_prev: backendData.clienti_sold_prev || [],
furnizori_facturat_prev: backendData.furnizori_facturat_prev || [],
furnizori_achitat_prev: backendData.furnizori_achitat_prev || [],
furnizori_sold_prev: backendData.furnizori_sold_prev || [],
trezorerie_sold_prev: backendData.trezorerie_sold_prev || [],
},
datasets: [
{
label: 'Trezorerie - Sold Net',
data: [...backendData.trezorerie_sold].map(val => Number(val) || 0),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: false,
pointBackgroundColor: 'rgb(59, 130, 246)',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6
}
]
};
};
const loadDetailedData = async (dataType, companyId, page = 1, pageSize = 25, search = '') => {
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
}
});
// Store total for pagination
detailedDataTotal.value = response.data.total || 0;
return {
success: true,
data: response.data.data || [], // Backend returns 'data' not 'items'
total: response.data.total || 0,
page: response.data.page || 1
};
} catch (err) {
error.value = err.response?.data?.detail || "Failed to load detailed data";
console.error("Failed to load detailed data:", err);
// Return mock data structure for testing
const mockData = generateMockDetailedData(dataType);
detailedDataTotal.value = mockData.length;
return {
success: false,
error: error.value,
data: mockData,
total: mockData.length,
page: 1
};
} finally {
isLoading.value = false;
}
};
// Generate mock data for testing until backend endpoint is implemented
const generateMockDetailedData = (dataType) => {
switch(dataType) {
case 'clients':
return [
{ id: 1, client: 'SC ALPHA SRL', facturat: 15000, incasat: 12000, sold: 3000, status: 'Activ' },
{ id: 2, client: 'SC BETA SRL', facturat: 8500, incasat: 8500, sold: 0, status: 'Activ' },
{ id: 3, client: 'SC GAMMA SRL', facturat: 22000, incasat: 15000, sold: 7000, status: 'Activ' },
{ id: 4, client: 'SC DELTA SRL', facturat: 5500, incasat: 2000, sold: 3500, status: 'Întârziere' },
{ id: 5, client: 'SC EPSILON SRL', facturat: 18000, incasat: 18000, sold: 0, status: 'Activ' }
];
case 'suppliers':
return [
{ id: 1, furnizor: 'SC SUPPLIER A SRL', facturat: 12000, achitat: 10000, sold: 2000, status: 'Activ' },
{ id: 2, furnizor: 'SC SUPPLIER B SRL', facturat: 7500, achitat: 7500, sold: 0, status: 'Activ' },
{ id: 3, furnizor: 'SC SUPPLIER C SRL', facturat: 19000, achitat: 12000, sold: 7000, status: 'Pendente' },
{ id: 4, furnizor: 'SC SUPPLIER D SRL', facturat: 4200, achitat: 4200, sold: 0, status: 'Activ' },
{ id: 5, furnizor: 'SC SUPPLIER E SRL', facturat: 16800, achitat: 8000, sold: 8800, status: 'Pendente' }
];
case 'treasury':
return [
{ id: 1, cont: '5121', nume_cont: 'Cont curent BCR', sold: 45000, valuta: 'RON', tip: 'Bancă' },
{ id: 2, cont: '5311', nume_cont: 'Casa RON', sold: 2500, valuta: 'RON', tip: 'Numerar' },
{ id: 3, cont: '5124', nume_cont: 'Cont curent BRD EUR', sold: 8500, valuta: 'EUR', tip: 'Bancă' },
{ id: 4, cont: '5125', nume_cont: 'Cont economii ING', sold: 125000, valuta: 'RON', tip: 'Economii' },
{ id: 5, cont: '5312', nume_cont: 'Casa valută', sold: 500, valuta: 'EUR', tip: 'Numerar' }
];
default:
return [];
}
};
// Funcții noi pentru carduri
const loadPerformanceData = async (companyId, period = '7d') => {
const cacheKey = `performance-${companyId}-${period}`;
// Check cache
if (dataCache.has(cacheKey)) {
performanceData.value[period] = dataCache.get(cacheKey);
return { success: true, data: dataCache.get(cacheKey) };
}
try {
const response = await apiService.get('/dashboard/performance', {
params: { company: companyId, period }
});
performanceData.value[period] = response.data;
dataCache.set(cacheKey, response.data);
return { success: true, data: response.data };
} catch (err) {
console.error('Failed to load performance data:', err);
return { success: false, error: err.message };
}
};
const loadCashFlowData = async (companyId, period = '7d') => {
const cacheKey = `cashflow-${companyId}-${period}`;
if (dataCache.has(cacheKey)) {
cashflowData.value[period] = dataCache.get(cacheKey);
return { success: true, data: dataCache.get(cacheKey) };
}
try {
const response = await apiService.get('/dashboard/cashflow', {
params: { company: companyId, period }
});
cashflowData.value[period] = response.data;
dataCache.set(cacheKey, response.data);
return { success: true, data: response.data };
} catch (err) {
console.error('Failed to load cashflow data:', err);
return { success: false, error: err.message };
}
};
const loadMaturityData = async (companyId, period = '7d') => {
const cacheKey = `maturity-${companyId}-${period}`;
if (dataCache.has(cacheKey)) {
maturityData.value[period] = dataCache.get(cacheKey);
return { success: true, data: dataCache.get(cacheKey) };
}
try {
const response = await apiService.get('/dashboard/maturity', {
params: { company: companyId, period }
});
maturityData.value[period] = response.data;
dataCache.set(cacheKey, response.data);
return { success: true, data: response.data };
} catch (err) {
console.error('Failed to load maturity data:', err);
return { success: false, error: err.message };
}
};
const loadCurrentPeriod = async (companyId) => {
try {
const response = await apiService.get('/dashboard/current-period', {
params: { company: companyId }
});
currentPeriod.value = response.data;
return { success: true, data: response.data };
} catch (err) {
console.error('Failed to load current period:', err);
// Fallback to current date if API fails
const now = new Date();
const fallbackPeriod = {
year: now.getFullYear(),
month: now.getMonth() + 1,
period: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
};
currentPeriod.value = fallbackPeriod;
return { success: false, error: err.message, data: fallbackPeriod };
}
};
// Clear cache
const clearCache = () => {
dataCache.clear();
};
const reset = () => {
summary.value = null;
trends.value = null;
isLoading.value = false;
error.value = null;
// Clear new data as well
performanceData.value = {};
cashflowData.value = {};
maturityData.value = {};
currentPeriod.value = null;
clearCache();
};
return {
// Existing
summary,
trends,
isLoading,
error,
loadDashboardSummary,
loadTrendData,
loadDetailedData,
reset,
// New
performanceData,
cashflowData,
maturityData,
currentPeriod,
loadPerformanceData,
loadCashFlowData,
loadMaturityData,
loadCurrentPeriod,
clearCache,
// Detailed data pagination
detailedDataTotal
};
});