// Re-export new services export { ReportsService } from './reports.service'; export { ReportExecutionService } from './report-execution.service'; export { ReportSchedulerService } from './report-scheduler.service'; export { DashboardsService } from './dashboards.service'; // Legacy types and service (kept for backwards compatibility) import { DataSource } from 'typeorm'; export interface ReportDateRange { startDate: Date; endDate: Date; } export interface SalesReportParams { tenantId: string; startDate: Date; endDate: Date; partnerId?: string; productId?: string; groupBy?: 'day' | 'week' | 'month' | 'partner' | 'product'; } export interface InventoryReportParams { tenantId: string; warehouseId?: string; productId?: string; categoryId?: string; lowStockOnly?: boolean; } export interface FinancialReportParams { tenantId: string; startDate: Date; endDate: Date; reportType: 'income' | 'expenses' | 'profit_loss' | 'cash_flow' | 'accounts_receivable' | 'accounts_payable'; } export interface SalesSummary { totalOrders: number; totalRevenue: number; averageOrderValue: number; totalItems: number; } export interface InventorySummary { totalProducts: number; totalValue: number; lowStockItems: number; outOfStockItems: number; } export interface FinancialSummary { totalIncome: number; totalExpenses: number; netProfit: number; margin: number; } export class LegacyReportsService { constructor(private readonly dataSource: DataSource) {} // ==================== Sales Reports ==================== async getSalesReport(params: SalesReportParams): Promise<{ summary: SalesSummary; data: any[]; period: ReportDateRange; }> { const { tenantId, startDate, endDate, partnerId, productId, groupBy = 'day' } = params; // Build query based on groupBy let query = ` SELECT COUNT(DISTINCT o.id) as order_count, SUM(o.total) as total_revenue, SUM(oi.quantity) as total_items FROM sales.sales_orders o LEFT JOIN sales.sales_order_items oi ON o.id = oi.order_id WHERE o.tenant_id = $1 AND o.status NOT IN ('cancelled', 'draft') AND o.order_date BETWEEN $2 AND $3 `; const queryParams: any[] = [tenantId, startDate, endDate]; let paramIndex = 4; if (partnerId) { query += ` AND o.partner_id = $${paramIndex}`; queryParams.push(partnerId); paramIndex++; } if (productId) { query += ` AND oi.product_id = $${paramIndex}`; queryParams.push(productId); } try { const result = await this.dataSource.query(query, queryParams); const row = result[0] || {}; const summary: SalesSummary = { totalOrders: parseInt(row.order_count) || 0, totalRevenue: parseFloat(row.total_revenue) || 0, averageOrderValue: row.order_count > 0 ? parseFloat(row.total_revenue) / parseInt(row.order_count) : 0, totalItems: parseInt(row.total_items) || 0, }; // Get detailed data grouped by period const detailQuery = this.buildSalesDetailQuery(groupBy); const detailParams = [tenantId, startDate, endDate]; const detailData = await this.dataSource.query(detailQuery, detailParams); return { summary, data: detailData, period: { startDate, endDate }, }; } catch (error) { // Return empty data if tables don't exist yet return { summary: { totalOrders: 0, totalRevenue: 0, averageOrderValue: 0, totalItems: 0 }, data: [], period: { startDate, endDate }, }; } } private buildSalesDetailQuery(groupBy: string): string { const groupByClause = { day: "DATE_TRUNC('day', o.order_date)", week: "DATE_TRUNC('week', o.order_date)", month: "DATE_TRUNC('month', o.order_date)", partner: 'o.partner_id', product: 'oi.product_id', }[groupBy] || "DATE_TRUNC('day', o.order_date)"; return ` SELECT ${groupByClause} as period, COUNT(DISTINCT o.id) as order_count, SUM(o.total) as total_revenue FROM sales.sales_orders o LEFT JOIN sales.sales_order_items oi ON o.id = oi.order_id WHERE o.tenant_id = $1 AND o.status NOT IN ('cancelled', 'draft') AND o.order_date BETWEEN $2 AND $3 GROUP BY ${groupByClause} ORDER BY period DESC `; } async getTopSellingProducts( tenantId: string, startDate: Date, endDate: Date, limit: number = 10 ): Promise { try { const query = ` SELECT oi.product_id, oi.product_name, SUM(oi.quantity) as total_quantity, SUM(oi.line_total) as total_revenue FROM sales.sales_order_items oi JOIN sales.sales_orders o ON oi.order_id = o.id WHERE o.tenant_id = $1 AND o.status NOT IN ('cancelled', 'draft') AND o.order_date BETWEEN $2 AND $3 GROUP BY oi.product_id, oi.product_name ORDER BY total_revenue DESC LIMIT $4 `; return await this.dataSource.query(query, [tenantId, startDate, endDate, limit]); } catch { return []; } } async getTopCustomers( tenantId: string, startDate: Date, endDate: Date, limit: number = 10 ): Promise { try { const query = ` SELECT o.partner_id, o.partner_name, COUNT(o.id) as order_count, SUM(o.total) as total_revenue FROM sales.sales_orders o WHERE o.tenant_id = $1 AND o.status NOT IN ('cancelled', 'draft') AND o.order_date BETWEEN $2 AND $3 GROUP BY o.partner_id, o.partner_name ORDER BY total_revenue DESC LIMIT $4 `; return await this.dataSource.query(query, [tenantId, startDate, endDate, limit]); } catch { return []; } } // ==================== Inventory Reports ==================== async getInventoryReport(params: InventoryReportParams): Promise<{ summary: InventorySummary; data: any[]; }> { const { tenantId, warehouseId, productId, categoryId, lowStockOnly } = params; try { let query = ` SELECT COUNT(DISTINCT sl.product_id) as total_products, SUM(sl.quantity_on_hand * COALESCE(sl.unit_cost, 0)) as total_value, COUNT(CASE WHEN sl.quantity_on_hand <= 0 THEN 1 END) as out_of_stock, COUNT(CASE WHEN sl.quantity_on_hand > 0 AND sl.quantity_on_hand <= 10 THEN 1 END) as low_stock FROM inventory.stock_levels sl WHERE sl.tenant_id = $1 `; const queryParams: any[] = [tenantId]; let paramIndex = 2; if (warehouseId) { query += ` AND sl.warehouse_id = $${paramIndex}`; queryParams.push(warehouseId); paramIndex++; } if (productId) { query += ` AND sl.product_id = $${paramIndex}`; queryParams.push(productId); } const result = await this.dataSource.query(query, queryParams); const row = result[0] || {}; const summary: InventorySummary = { totalProducts: parseInt(row.total_products) || 0, totalValue: parseFloat(row.total_value) || 0, lowStockItems: parseInt(row.low_stock) || 0, outOfStockItems: parseInt(row.out_of_stock) || 0, }; // Get detailed stock levels let detailQuery = ` SELECT sl.product_id, p.name as product_name, p.sku, sl.warehouse_id, w.name as warehouse_name, sl.quantity_on_hand, sl.quantity_reserved, sl.quantity_available, sl.unit_cost, (sl.quantity_on_hand * COALESCE(sl.unit_cost, 0)) as total_value FROM inventory.stock_levels sl LEFT JOIN products.products p ON sl.product_id = p.id LEFT JOIN inventory.warehouses w ON sl.warehouse_id = w.id WHERE sl.tenant_id = $1 `; const detailParams: any[] = [tenantId]; let detailIndex = 2; if (warehouseId) { detailQuery += ` AND sl.warehouse_id = $${detailIndex}`; detailParams.push(warehouseId); detailIndex++; } if (lowStockOnly) { detailQuery += ` AND sl.quantity_on_hand <= 10`; } detailQuery += ` ORDER BY sl.quantity_on_hand ASC LIMIT 100`; const detailData = await this.dataSource.query(detailQuery, detailParams); return { summary, data: detailData }; } catch { return { summary: { totalProducts: 0, totalValue: 0, lowStockItems: 0, outOfStockItems: 0 }, data: [], }; } } async getStockMovementReport( tenantId: string, startDate: Date, endDate: Date, warehouseId?: string ): Promise { try { let query = ` SELECT sm.movement_type, COUNT(*) as movement_count, SUM(sm.quantity) as total_quantity, SUM(sm.total_cost) as total_value FROM inventory.stock_movements sm WHERE sm.tenant_id = $1 AND sm.status = 'confirmed' AND sm.created_at BETWEEN $2 AND $3 `; const params: any[] = [tenantId, startDate, endDate]; if (warehouseId) { query += ` AND (sm.source_warehouse_id = $4 OR sm.dest_warehouse_id = $4)`; params.push(warehouseId); } query += ` GROUP BY sm.movement_type ORDER BY total_quantity DESC`; return await this.dataSource.query(query, params); } catch { return []; } } // ==================== Financial Reports ==================== async getFinancialReport(params: FinancialReportParams): Promise<{ summary: FinancialSummary; data: any[]; period: ReportDateRange; }> { const { tenantId, startDate, endDate, reportType } = params; try { switch (reportType) { case 'income': return await this.getIncomeReport(tenantId, startDate, endDate); case 'expenses': return await this.getExpensesReport(tenantId, startDate, endDate); case 'profit_loss': return await this.getProfitLossReport(tenantId, startDate, endDate); case 'accounts_receivable': return await this.getAccountsReceivableReport(tenantId); case 'accounts_payable': return await this.getAccountsPayableReport(tenantId); default: return await this.getProfitLossReport(tenantId, startDate, endDate); } } catch { return { summary: { totalIncome: 0, totalExpenses: 0, netProfit: 0, margin: 0 }, data: [], period: { startDate, endDate }, }; } } private async getIncomeReport( tenantId: string, startDate: Date, endDate: Date ): Promise<{ summary: FinancialSummary; data: any[]; period: ReportDateRange }> { const query = ` SELECT DATE_TRUNC('month', i.invoice_date) as period, SUM(i.total) as total_income FROM billing.invoices i WHERE i.tenant_id = $1 AND i.invoice_type = 'sale' AND i.status NOT IN ('cancelled', 'voided', 'draft') AND i.invoice_date BETWEEN $2 AND $3 GROUP BY DATE_TRUNC('month', i.invoice_date) ORDER BY period `; const data = await this.dataSource.query(query, [tenantId, startDate, endDate]); const totalIncome = data.reduce((sum: number, row: any) => sum + parseFloat(row.total_income || 0), 0); return { summary: { totalIncome, totalExpenses: 0, netProfit: totalIncome, margin: 100 }, data, period: { startDate, endDate }, }; } private async getExpensesReport( tenantId: string, startDate: Date, endDate: Date ): Promise<{ summary: FinancialSummary; data: any[]; period: ReportDateRange }> { const query = ` SELECT DATE_TRUNC('month', i.invoice_date) as period, SUM(i.total) as total_expenses FROM billing.invoices i WHERE i.tenant_id = $1 AND i.invoice_type = 'purchase' AND i.status NOT IN ('cancelled', 'voided', 'draft') AND i.invoice_date BETWEEN $2 AND $3 GROUP BY DATE_TRUNC('month', i.invoice_date) ORDER BY period `; const data = await this.dataSource.query(query, [tenantId, startDate, endDate]); const totalExpenses = data.reduce((sum: number, row: any) => sum + parseFloat(row.total_expenses || 0), 0); return { summary: { totalIncome: 0, totalExpenses, netProfit: -totalExpenses, margin: 0 }, data, period: { startDate, endDate }, }; } private async getProfitLossReport( tenantId: string, startDate: Date, endDate: Date ): Promise<{ summary: FinancialSummary; data: any[]; period: ReportDateRange }> { const incomeQuery = ` SELECT COALESCE(SUM(total), 0) as total FROM billing.invoices WHERE tenant_id = $1 AND invoice_type = 'sale' AND status NOT IN ('cancelled', 'voided', 'draft') AND invoice_date BETWEEN $2 AND $3 `; const expensesQuery = ` SELECT COALESCE(SUM(total), 0) as total FROM billing.invoices WHERE tenant_id = $1 AND invoice_type = 'purchase' AND status NOT IN ('cancelled', 'voided', 'draft') AND invoice_date BETWEEN $2 AND $3 `; const [incomeResult, expensesResult] = await Promise.all([ this.dataSource.query(incomeQuery, [tenantId, startDate, endDate]), this.dataSource.query(expensesQuery, [tenantId, startDate, endDate]), ]); const totalIncome = parseFloat(incomeResult[0]?.total) || 0; const totalExpenses = parseFloat(expensesResult[0]?.total) || 0; const netProfit = totalIncome - totalExpenses; const margin = totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0; return { summary: { totalIncome, totalExpenses, netProfit, margin }, data: [{ totalIncome, totalExpenses, netProfit, margin }], period: { startDate, endDate }, }; } private async getAccountsReceivableReport( tenantId: string ): Promise<{ summary: FinancialSummary; data: any[]; period: ReportDateRange }> { const query = ` SELECT i.id, i.invoice_number, i.partner_name, i.total, i.amount_paid, (i.total - i.amount_paid) as amount_due, i.due_date, CASE WHEN i.due_date < CURRENT_DATE THEN 'overdue' WHEN i.due_date < CURRENT_DATE + INTERVAL '7 days' THEN 'due_soon' ELSE 'current' END as status FROM billing.invoices i WHERE i.tenant_id = $1 AND i.invoice_type = 'sale' AND i.status IN ('validated', 'sent', 'partial') AND (i.total - i.amount_paid) > 0 ORDER BY i.due_date ASC `; const data = await this.dataSource.query(query, [tenantId]); const totalIncome = data.reduce((sum: number, row: any) => sum + parseFloat(row.amount_due || 0), 0); const now = new Date(); return { summary: { totalIncome, totalExpenses: 0, netProfit: totalIncome, margin: 0 }, data, period: { startDate: now, endDate: now }, }; } private async getAccountsPayableReport( tenantId: string ): Promise<{ summary: FinancialSummary; data: any[]; period: ReportDateRange }> { const query = ` SELECT i.id, i.invoice_number, i.partner_name, i.total, i.amount_paid, (i.total - i.amount_paid) as amount_due, i.due_date, CASE WHEN i.due_date < CURRENT_DATE THEN 'overdue' WHEN i.due_date < CURRENT_DATE + INTERVAL '7 days' THEN 'due_soon' ELSE 'current' END as status FROM billing.invoices i WHERE i.tenant_id = $1 AND i.invoice_type = 'purchase' AND i.status IN ('validated', 'sent', 'partial') AND (i.total - i.amount_paid) > 0 ORDER BY i.due_date ASC `; const data = await this.dataSource.query(query, [tenantId]); const totalExpenses = data.reduce((sum: number, row: any) => sum + parseFloat(row.amount_due || 0), 0); const now = new Date(); return { summary: { totalIncome: 0, totalExpenses, netProfit: -totalExpenses, margin: 0 }, data, period: { startDate: now, endDate: now }, }; } }