Re_Backend/src/services/dealerDashboard.service.ts

336 lines
10 KiB
TypeScript

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<string | null> {
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<string, CategoryData>();
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();