/** * BidAnalyticsController - Controller de Analisis de Licitaciones * * Endpoints REST para dashboards y analisis de preconstruccion. * * @module Bidding */ import { Router, Request, Response, NextFunction } from 'express'; import { DataSource } from 'typeorm'; import { BidAnalyticsService } from '../services/bid-analytics.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; import { Tender } from '../entities/tender.entity'; import { Opportunity } from '../entities/opportunity.entity'; import { Proposal } from '../entities/proposal.entity'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { RefreshToken } from '../../auth/entities/refresh-token.entity'; import { ServiceContext } from '../../../shared/services/base.service'; export function createBidAnalyticsController(dataSource: DataSource): Router { const router = Router(); // Repositorios const tenderRepository = dataSource.getRepository(Tender); const opportunityRepository = dataSource.getRepository(Opportunity); const proposalRepository = dataSource.getRepository(Proposal); const userRepository = dataSource.getRepository(User); const tenantRepository = dataSource.getRepository(Tenant); const refreshTokenRepository = dataSource.getRepository(RefreshToken); // Servicios const analyticsService = new BidAnalyticsService(tenderRepository, opportunityRepository, proposalRepository); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); // Helper para crear contexto const getContext = (req: Request): ServiceContext => { if (!req.tenantId) { throw new Error('Tenant ID is required'); } return { tenantId: req.tenantId, userId: req.user?.sub, }; }; /** * GET /bid-analytics/dashboard */ router.get('/dashboard', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const dashboard = await analyticsService.getDashboard(getContext(req)); res.status(200).json({ success: true, data: dashboard }); } catch (error) { next(error); } }); /** * GET /bid-analytics/pipeline-by-source */ router.get('/pipeline-by-source', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const data = await analyticsService.getPipelineBySource(getContext(req)); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); /** * GET /bid-analytics/win-rate-by-type */ router.get('/win-rate-by-type', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const months = parseInt(req.query.months as string) || 12; const data = await analyticsService.getWinRateByType(getContext(req), months); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); /** * GET /bid-analytics/monthly-trend */ router.get('/monthly-trend', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const months = parseInt(req.query.months as string) || 12; const data = await analyticsService.getMonthlyTrend(getContext(req), months); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); /** * GET /bid-analytics/competitors */ router.get('/competitors', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const data = await analyticsService.getCompetitorAnalysis(getContext(req)); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); /** * GET /bid-analytics/funnel */ router.get('/funnel', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const months = parseInt(req.query.months as string) || 12; const data = await analyticsService.getFunnelAnalysis(getContext(req), months); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); /** * GET /bid-analytics/cycle-time */ router.get('/cycle-time', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const months = parseInt(req.query.months as string) || 12; const data = await analyticsService.getCycleTimeAnalysis(getContext(req), months); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); /** * GET /bid-analytics/win-rate */ router.get('/win-rate', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const dateFrom = req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined; const dateTo = req.query.dateTo ? new Date(req.query.dateTo as string) : undefined; const data = await analyticsService.getWinRate(getContext(req), { dateFrom, dateTo }); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); /** * GET /bid-analytics/pipeline */ router.get('/pipeline', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const data = await analyticsService.getPipelineValue(getContext(req)); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); /** * GET /bid-analytics/averages */ router.get('/averages', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const data = await analyticsService.getAverages(getContext(req)); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); /** * GET /bid-analytics/sources */ router.get('/sources', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.tenantId) { res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' }); return; } const dateFrom = req.query.dateFrom ? new Date(req.query.dateFrom as string) : undefined; const dateTo = req.query.dateTo ? new Date(req.query.dateTo as string) : undefined; const data = await analyticsService.getOpportunitiesBySource(getContext(req), dateFrom, dateTo); res.status(200).json({ success: true, data }); } catch (error) { next(error); } }); return router; } export default createBidAnalyticsController;