import { WorkflowRequest } from '@models/WorkflowRequest'; import { DealerClaimDetails } from '@models/DealerClaimDetails'; import { ClaimCreditNote } from '@models/ClaimCreditNote'; import { DealerProposalDetails } from '@models/DealerProposalDetails'; import { ClaimBudgetTracking } from '@models/ClaimBudgetTracking'; import { Op, QueryTypes } from 'sequelize'; import { sequelize } from '@config/database'; import dayjs from 'dayjs'; import logger from '@utils/logger'; import { User } from '@models/User'; interface DateRangeFilter { start: Date; end: Date; } interface DashboardKPIs { totalClaims: number; totalValue: number; approved: number; rejected: number; pending: number; credited: number; pendingCredit: number; approvedValue: number; rejectedValue: number; pendingValue: number; creditedValue: number; pendingCreditValue: number; } interface CategoryData { activityType: string; raised: number; raisedValue: number; approved: number; approvedValue: number; rejected: number; rejectedValue: number; pending: number; pendingValue: number; credited: number; creditedValue: number; pendingCredit: number; pendingCreditValue: number; approvalRate: number; creditRate: number; } export class DealerDashboardService { /** * Parse date range string to Date objects */ private parseDateRange(dateRange?: string, startDate?: string, endDate?: string): DateRangeFilter { if (dateRange === 'custom' && startDate && endDate) { const start = dayjs(startDate).startOf('day').toDate(); const end = dayjs(endDate).endOf('day').toDate(); const now = dayjs(); const actualEnd = end > now.toDate() ? now.endOf('day').toDate() : end; return { start, end: actualEnd }; } if (dateRange === 'custom' && (!startDate || !endDate)) { const now = dayjs(); return { start: now.subtract(30, 'day').startOf('day').toDate(), end: now.endOf('day').toDate() }; } const now = dayjs(); switch (dateRange) { case 'today': return { start: now.startOf('day').toDate(), end: now.endOf('day').toDate() }; case 'week': return { start: now.startOf('week').toDate(), end: now.endOf('week').toDate() }; case 'month': return { start: now.startOf('month').toDate(), end: now.endOf('month').toDate() }; case 'quarter': const currentMonth = now.month(); const quarterStartMonth = Math.floor(currentMonth / 3) * 3; return { start: now.month(quarterStartMonth).startOf('month').toDate(), end: now.month(quarterStartMonth + 2).endOf('month').toDate() }; case 'year': return { start: now.startOf('year').toDate(), end: now.endOf('year').toDate() }; default: return { start: now.subtract(30, 'day').startOf('day').toDate(), end: now.endOf('day').toDate() }; } } /** * Get dealer email from user email or user ID */ private async getDealerEmail(userEmail?: string, userId?: string): Promise { try { if (userEmail) { // Check if user email matches a dealer email in dealer_claim_details const dealerClaim = await DealerClaimDetails.findOne({ where: { dealerEmail: { [Op.iLike]: userEmail.toLowerCase() } }, limit: 1 }); if (dealerClaim) { return dealerClaim.dealerEmail?.toLowerCase() || null; } } if (userId) { // Get user email from userId const user = await User.findByPk(userId); if (user?.email) { const dealerClaim = await DealerClaimDetails.findOne({ where: { dealerEmail: { [Op.iLike]: user.email.toLowerCase() } }, limit: 1 }); if (dealerClaim) { return dealerClaim.dealerEmail?.toLowerCase() || null; } } } return null; } catch (error) { logger.error('[DealerDashboard] Error getting dealer email:', error); return null; } } /** * Get dashboard KPIs for dealer */ async getDashboardKPIs( userEmail?: string, userId?: string, dateRange?: string, startDate?: string, endDate?: string ): Promise<{ kpis: DashboardKPIs; categoryData: CategoryData[] }> { try { const dealerEmail = await this.getDealerEmail(userEmail, userId); if (!dealerEmail) { logger.warn('[DealerDashboard] No dealer email found for user'); return { kpis: { totalClaims: 0, totalValue: 0, approved: 0, rejected: 0, pending: 0, credited: 0, pendingCredit: 0, approvedValue: 0, rejectedValue: 0, pendingValue: 0, creditedValue: 0, pendingCreditValue: 0, }, categoryData: [] }; } const applyDateRange = dateRange !== undefined && dateRange !== null && dateRange !== 'all'; const range = applyDateRange ? this.parseDateRange(dateRange, startDate, endDate) : null; // Build date filter const dateFilter = applyDateRange && range ? `AND ( (wf.submission_date BETWEEN :start AND :end AND wf.submission_date IS NOT NULL) OR (wf.submission_date IS NULL AND wf.created_at BETWEEN :start AND :end) )` : `1=1`; const replacements: any = { dealerEmail: dealerEmail.toLowerCase() }; if (applyDateRange && range) { replacements.start = range.start; replacements.end = range.end; } // Get all dealer claims with their details // Filter by both workflow_type and template_type for compatibility const claimsQuery = ` SELECT wf.request_id, wf.status, dcd.activity_type, COALESCE(dpd.total_estimated_budget, cbt.proposal_estimated_budget, 0)::numeric AS estimated_budget, COALESCE(cbt.approved_budget, cbt.proposal_estimated_budget, dpd.total_estimated_budget, 0)::numeric AS approved_budget, cbt.final_claim_amount::numeric AS final_claim_amount, ccn.credit_note_number, ccn.credit_note_date, ccn.credit_amount::numeric AS credit_note_amount FROM workflow_requests wf INNER JOIN dealer_claim_details dcd ON wf.request_id = dcd.request_id LEFT JOIN dealer_proposal_details dpd ON wf.request_id = dpd.request_id LEFT JOIN claim_budget_tracking cbt ON wf.request_id = cbt.request_id LEFT JOIN claim_credit_notes ccn ON wf.request_id = ccn.request_id WHERE (wf.workflow_type = 'CLAIM_MANAGEMENT' OR wf.template_type = 'DEALER CLAIM') AND wf.is_draft = false AND (wf.is_deleted IS NULL OR wf.is_deleted = false) AND dcd.dealer_email ILIKE :dealerEmail AND ${dateFilter} `; const claims = await sequelize.query(claimsQuery, { replacements, type: QueryTypes.SELECT }) as any[]; // Calculate KPIs const kpis: DashboardKPIs = { totalClaims: claims.length, totalValue: 0, approved: 0, rejected: 0, pending: 0, credited: 0, pendingCredit: 0, approvedValue: 0, rejectedValue: 0, pendingValue: 0, creditedValue: 0, pendingCreditValue: 0, }; // Group by category const categoryMap = new Map(); for (const claim of claims) { const activityType = claim.activity_type || 'Unknown'; const status = (claim.status || '').toUpperCase(); const estimatedBudget = parseFloat(claim.estimated_budget || 0); const approvedBudget = parseFloat(claim.approved_budget || estimatedBudget); const finalClaimAmount = parseFloat(claim.final_claim_amount || approvedBudget); const hasCreditNote = !!(claim.credit_note_number && claim.credit_note_date); const creditNoteAmount = parseFloat(claim.credit_note_amount || finalClaimAmount); // Initialize category if not exists if (!categoryMap.has(activityType)) { categoryMap.set(activityType, { activityType, raised: 0, raisedValue: 0, approved: 0, approvedValue: 0, rejected: 0, rejectedValue: 0, pending: 0, pendingValue: 0, credited: 0, creditedValue: 0, pendingCredit: 0, pendingCreditValue: 0, approvalRate: 0, creditRate: 0, }); } const category = categoryMap.get(activityType)!; // Count and values by status category.raised++; category.raisedValue += estimatedBudget; kpis.totalValue += estimatedBudget; if (status === 'APPROVED' || status === 'CLOSED') { category.approved++; category.approvedValue += approvedBudget; kpis.approved++; kpis.approvedValue += approvedBudget; if (hasCreditNote) { category.credited++; category.creditedValue += creditNoteAmount; kpis.credited++; kpis.creditedValue += creditNoteAmount; } else { category.pendingCredit++; category.pendingCreditValue += finalClaimAmount; kpis.pendingCredit++; kpis.pendingCreditValue += finalClaimAmount; } } else if (status === 'REJECTED') { category.rejected++; category.rejectedValue += estimatedBudget; kpis.rejected++; kpis.rejectedValue += estimatedBudget; } else if (status === 'PENDING' || status === 'IN_PROGRESS') { category.pending++; category.pendingValue += estimatedBudget; kpis.pending++; kpis.pendingValue += estimatedBudget; } } // Calculate rates for each category const categoryData = Array.from(categoryMap.values()).map(cat => { cat.approvalRate = cat.raised > 0 ? (cat.approved / cat.raised) * 100 : 0; cat.creditRate = cat.approved > 0 ? (cat.credited / cat.approved) * 100 : 0; return cat; }); return { kpis, categoryData }; } catch (error) { logger.error('[DealerDashboard] Error fetching dashboard KPIs:', error); throw error; } } } export const dealerDashboardService = new DealerDashboardService();