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:
188
reports-app/frontend/src/stores/companies.js
Normal file
188
reports-app/frontend/src/stores/companies.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { apiService } from "../services/api";
|
||||
import { useAuthStore } from "./auth";
|
||||
|
||||
export const useCompanyStore = defineStore("companies", () => {
|
||||
// Initialize from localStorage - per user
|
||||
const initializeSelectedCompany = () => {
|
||||
// Get current username from auth store
|
||||
const authStore = useAuthStore();
|
||||
const username = authStore.user?.username;
|
||||
|
||||
if (!username) {
|
||||
console.log('[Companies] No username available for initialization');
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = `selected_company_${username}`;
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
try {
|
||||
const company = JSON.parse(saved);
|
||||
console.log(`[Companies] Loaded saved company for user ${username}:`, company.name);
|
||||
return company;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved company', e);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// State
|
||||
const companies = ref([]);
|
||||
const selectedCompany = ref(initializeSelectedCompany());
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
// Watch for auth user changes to restore selected company
|
||||
const authStore = useAuthStore();
|
||||
watch(
|
||||
() => authStore.user,
|
||||
(newUser) => {
|
||||
if (newUser && newUser.username && !selectedCompany.value) {
|
||||
console.log('[Companies] User became available, attempting to restore selected company');
|
||||
const restoredCompany = initializeSelectedCompany();
|
||||
if (restoredCompany) {
|
||||
selectedCompany.value = restoredCompany;
|
||||
console.log('[Companies] Successfully restored selected company:', restoredCompany.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Getters
|
||||
const companyList = computed(() => companies.value);
|
||||
const hasCompanies = computed(() => companies.value.length > 0);
|
||||
const selectedCompanyId = computed(
|
||||
() => selectedCompany.value?.id_firma || null,
|
||||
);
|
||||
|
||||
// Computed property for formatted company list display
|
||||
const companyListFormatted = computed(() => {
|
||||
return companies.value.map(company => ({
|
||||
...company,
|
||||
displayName: company.fiscal_code
|
||||
? `${company.name} (${company.fiscal_code})`
|
||||
: company.name
|
||||
}));
|
||||
});
|
||||
|
||||
// Actions
|
||||
const loadCompanies = async () => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
console.log('[COMPANY STORE DEBUG] Loading companies...');
|
||||
const response = await apiService.get("/companies");
|
||||
console.log('[COMPANY STORE DEBUG] API Response:', response.data);
|
||||
companies.value = response.data.companies || [];
|
||||
console.log('[COMPANY STORE DEBUG] Companies array:', companies.value);
|
||||
|
||||
// Security validation: Check if saved company is accessible to current user
|
||||
if (selectedCompany.value) {
|
||||
const exists = companies.value.find(
|
||||
c => c.id_firma === selectedCompany.value.id_firma
|
||||
);
|
||||
if (!exists) {
|
||||
console.warn('[Companies][Security] Saved company not accessible to current user, clearing');
|
||||
clearSelectedCompany();
|
||||
} else {
|
||||
console.log('[Companies][Security] Saved company validated successfully');
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.detail || "Failed to load companies";
|
||||
console.error("Failed to load companies:", err);
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setSelectedCompany = (company) => {
|
||||
selectedCompany.value = company;
|
||||
|
||||
// Get current username from auth store
|
||||
const authStore = useAuthStore();
|
||||
const username = authStore.user?.username;
|
||||
|
||||
if (!username) {
|
||||
console.warn('[Companies] Cannot save company - no username available');
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `selected_company_${username}`;
|
||||
if (company) {
|
||||
localStorage.setItem(key, JSON.stringify(company));
|
||||
console.log(`[Companies] Saved company for user ${username}:`, company.name);
|
||||
} else {
|
||||
localStorage.removeItem(key);
|
||||
console.log(`[Companies] Cleared company for user ${username}`);
|
||||
}
|
||||
};
|
||||
|
||||
const clearSelectedCompany = () => {
|
||||
selectedCompany.value = null;
|
||||
|
||||
// Get current username from auth store
|
||||
const authStore = useAuthStore();
|
||||
const username = authStore.user?.username;
|
||||
|
||||
if (username) {
|
||||
const key = `selected_company_${username}`;
|
||||
localStorage.removeItem(key);
|
||||
console.log(`[Companies] Cleared company for user ${username}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getCompanyById = (id_firma) => {
|
||||
return companies.value.find((company) => company.id_firma === parseInt(id_firma));
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
companies.value = [];
|
||||
selectedCompany.value = null;
|
||||
isLoading.value = false;
|
||||
error.value = null;
|
||||
|
||||
// Clear saved company for current user
|
||||
const authStore = useAuthStore();
|
||||
const username = authStore.user?.username;
|
||||
if (username) {
|
||||
const key = `selected_company_${username}`;
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
companies,
|
||||
selectedCompany,
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// Getters
|
||||
companyList,
|
||||
companyListFormatted,
|
||||
hasCompanies,
|
||||
selectedCompanyId,
|
||||
|
||||
// Actions
|
||||
loadCompanies,
|
||||
setSelectedCompany,
|
||||
clearSelectedCompany,
|
||||
getCompanyById,
|
||||
clearError,
|
||||
reset,
|
||||
};
|
||||
});
|
||||
373
reports-app/frontend/src/stores/dashboard.js
Normal file
373
reports-app/frontend/src/stores/dashboard.js
Normal 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
|
||||
};
|
||||
});
|
||||
5
reports-app/frontend/src/stores/index.js
Normal file
5
reports-app/frontend/src/stores/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { useAuthStore } from "./auth";
|
||||
export { useCompanyStore } from "./companies";
|
||||
export { useInvoicesStore } from "./invoices";
|
||||
export { useDashboardStore } from "./dashboard";
|
||||
export { useTreasuryStore } from "./treasury";
|
||||
165
reports-app/frontend/src/stores/invoices.js
Normal file
165
reports-app/frontend/src/stores/invoices.js
Normal file
@@ -0,0 +1,165 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { apiService } from "../services/api";
|
||||
|
||||
export const useInvoicesStore = defineStore("invoices", () => {
|
||||
// State
|
||||
const invoices = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
const filters = ref({
|
||||
company: null,
|
||||
type: "CLIENTI", // CLIENTI or FURNIZORI
|
||||
dateFrom: null,
|
||||
dateTo: null,
|
||||
searchTerm: "",
|
||||
});
|
||||
const pagination = ref({
|
||||
page: 0,
|
||||
rows: 50,
|
||||
totalRecords: 0,
|
||||
});
|
||||
|
||||
// Getters
|
||||
const invoiceList = computed(() => invoices.value);
|
||||
const hasInvoices = computed(() => invoices.value.length > 0);
|
||||
const totalInvoices = computed(() => pagination.value.totalRecords);
|
||||
|
||||
const paidInvoices = computed(() =>
|
||||
invoices.value.filter((invoice) => invoice.css_class === "invoice-paid"),
|
||||
);
|
||||
|
||||
const overdueInvoices = computed(() =>
|
||||
invoices.value.filter((invoice) => invoice.css_class === "invoice-overdue"),
|
||||
);
|
||||
|
||||
const totalAmountPaid = computed(() =>
|
||||
paidInvoices.value.reduce((sum, invoice) => sum + (invoice.suma || 0), 0),
|
||||
);
|
||||
|
||||
const totalAmountOverdue = computed(() =>
|
||||
overdueInvoices.value.reduce(
|
||||
(sum, invoice) => sum + (invoice.suma || 0),
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
// Actions
|
||||
const loadInvoices = async (companyCode, options = {}) => {
|
||||
if (!companyCode) {
|
||||
error.value = "Company code is required";
|
||||
return { success: false, error: error.value };
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
partner_type: filters.value.type,
|
||||
page: pagination.value.page + 1,
|
||||
size: pagination.value.rows,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (filters.value.dateFrom) {
|
||||
params.date_from = filters.value.dateFrom;
|
||||
}
|
||||
if (filters.value.dateTo) {
|
||||
params.date_to = filters.value.dateTo;
|
||||
}
|
||||
if (filters.value.searchTerm) {
|
||||
params.search = filters.value.searchTerm;
|
||||
}
|
||||
|
||||
// Fixed: Use company as query parameter instead of path parameter
|
||||
const response = await apiService.get(`/invoices/`, {
|
||||
params: {
|
||||
company: companyCode,
|
||||
...params
|
||||
}
|
||||
});
|
||||
|
||||
invoices.value = response.data.invoices || [];
|
||||
pagination.value.totalRecords = response.data.total_count || 0;
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.detail || "Failed to load invoices";
|
||||
console.error("Failed to load invoices:", err);
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setFilters = (newFilters) => {
|
||||
filters.value = { ...filters.value, ...newFilters };
|
||||
};
|
||||
|
||||
const setPagination = (newPagination) => {
|
||||
pagination.value = { ...pagination.value, ...newPagination };
|
||||
};
|
||||
|
||||
const setInvoiceType = (type) => {
|
||||
filters.value.type = type;
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
filters.value = {
|
||||
company: null,
|
||||
type: "CLIENTI",
|
||||
dateFrom: null,
|
||||
dateTo: null,
|
||||
searchTerm: "",
|
||||
};
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
invoices.value = [];
|
||||
isLoading.value = false;
|
||||
error.value = null;
|
||||
clearFilters();
|
||||
pagination.value = {
|
||||
page: 0,
|
||||
rows: 50,
|
||||
totalRecords: 0,
|
||||
};
|
||||
};
|
||||
|
||||
const getInvoiceById = (id) => {
|
||||
return invoices.value.find((invoice) => invoice.id === id);
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
invoices,
|
||||
isLoading,
|
||||
error,
|
||||
filters,
|
||||
pagination,
|
||||
|
||||
// Getters
|
||||
invoiceList,
|
||||
hasInvoices,
|
||||
totalInvoices,
|
||||
paidInvoices,
|
||||
overdueInvoices,
|
||||
totalAmountPaid,
|
||||
totalAmountOverdue,
|
||||
|
||||
// Actions
|
||||
loadInvoices,
|
||||
setFilters,
|
||||
setPagination,
|
||||
setInvoiceType,
|
||||
clearFilters,
|
||||
clearError,
|
||||
reset,
|
||||
getInvoiceById,
|
||||
};
|
||||
});
|
||||
77
reports-app/frontend/src/stores/treasury.js
Normal file
77
reports-app/frontend/src/stores/treasury.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { apiService } from "../services/api";
|
||||
|
||||
export const useTreasuryStore = defineStore("treasury", () => {
|
||||
const registers = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
const pagination = ref({
|
||||
page: 0,
|
||||
rows: 50,
|
||||
totalRecords: 0,
|
||||
});
|
||||
const totals = ref({
|
||||
total_incasari: 0,
|
||||
total_plati: 0
|
||||
});
|
||||
|
||||
const loadBankCashRegister = async (companyId, filters = {}) => {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
company: companyId,
|
||||
page: pagination.value.page + 1,
|
||||
page_size: pagination.value.rows,
|
||||
...filters
|
||||
};
|
||||
|
||||
const response = await apiService.get('/treasury/bank-cash-register', {
|
||||
params
|
||||
});
|
||||
|
||||
registers.value = response.data.registers || [];
|
||||
pagination.value.totalRecords = response.data.total_count || 0;
|
||||
totals.value = {
|
||||
total_incasari: response.data.total_incasari,
|
||||
total_plati: response.data.total_plati
|
||||
};
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.detail || "Failed to load register";
|
||||
console.error("Failed to load register:", err);
|
||||
return { success: false, error: error.value };
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setPagination = (newPagination) => {
|
||||
pagination.value = { ...pagination.value, ...newPagination };
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
registers.value = [];
|
||||
isLoading.value = false;
|
||||
error.value = null;
|
||||
pagination.value = {
|
||||
page: 0,
|
||||
rows: 50,
|
||||
totalRecords: 0,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
registers,
|
||||
isLoading,
|
||||
error,
|
||||
pagination,
|
||||
totals,
|
||||
loadBankCashRegister,
|
||||
setPagination,
|
||||
reset
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user