386 lines
15 KiB
TypeScript
386 lines
15 KiB
TypeScript
/**
|
|
* Claim Data Mapper Utilities
|
|
* Maps API response data to ClaimManagementRequest structure for frontend components
|
|
*/
|
|
|
|
import { isClaimManagementRequest } from './claimRequestUtils';
|
|
|
|
/**
|
|
* User roles in a claim management request
|
|
*/
|
|
export type RequestRole = 'INITIATOR' | 'DEALER' | 'DEPARTMENT_LEAD' | 'APPROVER' | 'SPECTATOR';
|
|
|
|
/**
|
|
* Claim Management Request structure for frontend
|
|
*/
|
|
export interface ClaimManagementRequest {
|
|
// Activity Information
|
|
activityInfo: {
|
|
activityName: string;
|
|
activityType: string;
|
|
requestedDate?: string;
|
|
location: string;
|
|
period?: {
|
|
startDate: string;
|
|
endDate: string;
|
|
};
|
|
estimatedBudget?: number;
|
|
closedExpenses?: number;
|
|
closedExpensesBreakdown?: Array<{ description: string; amount: number }>;
|
|
description?: string;
|
|
};
|
|
|
|
// Dealer Information
|
|
dealerInfo: {
|
|
dealerCode: string;
|
|
dealerName: string;
|
|
email?: string;
|
|
phone?: string;
|
|
address?: string;
|
|
};
|
|
|
|
// Proposal Details (Step 1)
|
|
proposalDetails?: {
|
|
proposalDocumentUrl?: string;
|
|
costBreakup: Array<{ description: string; amount: number }>;
|
|
totalEstimatedBudget: number;
|
|
timelineMode?: 'date' | 'days';
|
|
expectedCompletionDate?: string;
|
|
expectedCompletionDays?: number;
|
|
dealerComments?: string;
|
|
submittedAt?: string;
|
|
};
|
|
|
|
// IO Details (Step 3) - from internal_orders table
|
|
ioDetails?: {
|
|
ioNumber?: string;
|
|
ioRemark?: string;
|
|
availableBalance?: number;
|
|
blockedAmount?: number;
|
|
remainingBalance?: number;
|
|
organizedBy?: string;
|
|
organizedAt?: string;
|
|
};
|
|
|
|
// DMS Details (Step 7)
|
|
dmsDetails?: {
|
|
eInvoiceNumber?: string;
|
|
eInvoiceDate?: string;
|
|
dmsNumber?: string;
|
|
creditNoteNumber?: string;
|
|
creditNoteDate?: string;
|
|
creditNoteAmount?: number;
|
|
};
|
|
|
|
// Claim Amount
|
|
claimAmount: {
|
|
estimated: number;
|
|
closed: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Role-based visibility configuration
|
|
*/
|
|
export interface RoleVisibility {
|
|
showDealerInfo: boolean;
|
|
showProposalDetails: boolean;
|
|
showIODetails: boolean;
|
|
showDMSDetails: boolean;
|
|
showClaimAmount: boolean;
|
|
canEditClaimAmount: boolean;
|
|
}
|
|
|
|
/**
|
|
* Map API request data to ClaimManagementRequest structure
|
|
*/
|
|
export function mapToClaimManagementRequest(
|
|
apiRequest: any,
|
|
_currentUserId: string
|
|
): ClaimManagementRequest | null {
|
|
try {
|
|
if (!isClaimManagementRequest(apiRequest)) {
|
|
return null;
|
|
}
|
|
|
|
// Extract claim details from API response
|
|
const claimDetails = apiRequest.claimDetails || {};
|
|
const proposalDetails = apiRequest.proposalDetails || {};
|
|
const completionDetails = apiRequest.completionDetails || {};
|
|
const internalOrder = apiRequest.internalOrder || apiRequest.internal_order || {};
|
|
|
|
// Extract new normalized tables
|
|
const budgetTracking = apiRequest.budgetTracking || apiRequest.budget_tracking || {};
|
|
const invoice = apiRequest.invoice || {};
|
|
const creditNote = apiRequest.creditNote || apiRequest.credit_note || {};
|
|
const completionExpenses = apiRequest.completionExpenses || apiRequest.completion_expenses || [];
|
|
|
|
// Debug: Log raw claim details to help troubleshoot
|
|
console.debug('[claimDataMapper] Raw claimDetails:', claimDetails);
|
|
console.debug('[claimDataMapper] Raw apiRequest:', {
|
|
hasClaimDetails: !!apiRequest.claimDetails,
|
|
hasProposalDetails: !!apiRequest.proposalDetails,
|
|
hasCompletionDetails: !!apiRequest.completionDetails,
|
|
hasBudgetTracking: !!budgetTracking,
|
|
hasInvoice: !!invoice,
|
|
hasCreditNote: !!creditNote,
|
|
hasCompletionExpenses: Array.isArray(completionExpenses) && completionExpenses.length > 0,
|
|
workflowType: apiRequest.workflowType,
|
|
});
|
|
|
|
// Map activity information (matching ActivityInformationCard expectations)
|
|
// Handle both camelCase and snake_case field names from Sequelize
|
|
const periodStartDate = claimDetails.periodStartDate || claimDetails.period_start_date;
|
|
const periodEndDate = claimDetails.periodEndDate || claimDetails.period_end_date;
|
|
|
|
const activityName = claimDetails.activityName || claimDetails.activity_name || '';
|
|
const activityType = claimDetails.activityType || claimDetails.activity_type || '';
|
|
const location = claimDetails.location || '';
|
|
|
|
console.debug('[claimDataMapper] Mapped activity fields:', {
|
|
activityName,
|
|
activityType,
|
|
location,
|
|
hasActivityName: !!activityName,
|
|
hasActivityType: !!activityType,
|
|
hasLocation: !!location,
|
|
});
|
|
|
|
// Get budget values from budgetTracking table (new source of truth)
|
|
const estimatedBudget = budgetTracking.proposalEstimatedBudget ||
|
|
budgetTracking.proposal_estimated_budget ||
|
|
budgetTracking.initialEstimatedBudget ||
|
|
budgetTracking.initial_estimated_budget ||
|
|
claimDetails.estimatedBudget ||
|
|
claimDetails.estimated_budget;
|
|
|
|
// Get closed expenses - check multiple sources with proper number conversion
|
|
const closedExpensesRaw = budgetTracking?.closedExpenses ||
|
|
budgetTracking?.closed_expenses ||
|
|
completionDetails?.totalClosedExpenses ||
|
|
completionDetails?.total_closed_expenses ||
|
|
claimDetails?.closedExpenses ||
|
|
claimDetails?.closed_expenses;
|
|
// Convert to number and handle 0 as valid value
|
|
const closedExpenses = closedExpensesRaw !== null && closedExpensesRaw !== undefined
|
|
? Number(closedExpensesRaw)
|
|
: undefined;
|
|
|
|
// Get closed expenses breakdown from new completionExpenses table
|
|
const closedExpensesBreakdown = Array.isArray(completionExpenses) && completionExpenses.length > 0
|
|
? completionExpenses.map((exp: any) => ({
|
|
description: exp.description || exp.itemDescription || '',
|
|
amount: Number(exp.amount) || 0
|
|
}))
|
|
: (completionDetails?.closedExpenses ||
|
|
completionDetails?.closed_expenses ||
|
|
completionDetails?.closedExpensesBreakdown ||
|
|
[]);
|
|
|
|
const activityInfo = {
|
|
activityName,
|
|
activityType,
|
|
requestedDate: claimDetails.activityDate || claimDetails.activity_date || apiRequest.createdAt, // Use activityDate as requestedDate, fallback to createdAt
|
|
location,
|
|
period: (periodStartDate && periodEndDate) ? {
|
|
startDate: periodStartDate,
|
|
endDate: periodEndDate,
|
|
} : undefined,
|
|
estimatedBudget,
|
|
closedExpenses,
|
|
closedExpensesBreakdown,
|
|
description: apiRequest.description || '', // Get description from workflow request
|
|
};
|
|
|
|
// Map dealer information (matching DealerInformationCard expectations)
|
|
// Dealer info should always be available from claimDetails (created during claim request creation)
|
|
// Handle both camelCase and snake_case from Sequelize JSON serialization
|
|
const dealerInfo = {
|
|
dealerCode: claimDetails?.dealerCode || claimDetails?.dealer_code || claimDetails?.DealerCode || '',
|
|
dealerName: claimDetails?.dealerName || claimDetails?.dealer_name || claimDetails?.DealerName || '',
|
|
email: claimDetails?.dealerEmail || claimDetails?.dealer_email || claimDetails?.DealerEmail || '',
|
|
phone: claimDetails?.dealerPhone || claimDetails?.dealer_phone || claimDetails?.DealerPhone || '',
|
|
address: claimDetails?.dealerAddress || claimDetails?.dealer_address || claimDetails?.DealerAddress || '',
|
|
};
|
|
|
|
// Log warning if dealer info is missing (should always be present for claim management requests)
|
|
if (!dealerInfo.dealerCode || !dealerInfo.dealerName) {
|
|
console.warn('[claimDataMapper] Dealer information is missing from claimDetails:', {
|
|
hasClaimDetails: !!claimDetails,
|
|
dealerCode: dealerInfo.dealerCode,
|
|
dealerName: dealerInfo.dealerName,
|
|
rawClaimDetails: claimDetails,
|
|
availableKeys: claimDetails ? Object.keys(claimDetails) : [],
|
|
});
|
|
}
|
|
|
|
// Map proposal details
|
|
const proposal = proposalDetails ? {
|
|
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
|
|
costBreakup: proposalDetails.costBreakup || proposalDetails.cost_breakup || [],
|
|
totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0,
|
|
timelineMode: proposalDetails.timelineMode || proposalDetails.timeline_mode,
|
|
expectedCompletionDate: proposalDetails.expectedCompletionDate || proposalDetails.expected_completion_date,
|
|
expectedCompletionDays: proposalDetails.expectedCompletionDays || proposalDetails.expected_completion_days,
|
|
dealerComments: proposalDetails.dealerComments || proposalDetails.dealer_comments,
|
|
submittedAt: proposalDetails.submittedAt || proposalDetails.submitted_at,
|
|
} : undefined;
|
|
|
|
// Map IO details from dedicated internal_orders table
|
|
const ioDetails = {
|
|
ioNumber: internalOrder.ioNumber || internalOrder.io_number || claimDetails.ioNumber || claimDetails.io_number,
|
|
ioRemark: internalOrder.ioRemark || internalOrder.io_remark || '',
|
|
availableBalance: internalOrder.ioAvailableBalance || internalOrder.io_available_balance || claimDetails.ioAvailableBalance || claimDetails.io_available_balance,
|
|
blockedAmount: internalOrder.ioBlockedAmount || internalOrder.io_blocked_amount || claimDetails.ioBlockedAmount || claimDetails.io_blocked_amount,
|
|
remainingBalance: internalOrder.ioRemainingBalance || internalOrder.io_remaining_balance || claimDetails.ioRemainingBalance || claimDetails.io_remaining_balance,
|
|
organizedBy: internalOrder.organizer?.displayName || internalOrder.organizer?.name || internalOrder.organizedBy || '',
|
|
organizedAt: internalOrder.organizedAt || internalOrder.organized_at || '',
|
|
};
|
|
|
|
// Map DMS details from new invoice and credit note tables
|
|
const dmsDetails = {
|
|
eInvoiceNumber: invoice.invoiceNumber || invoice.invoice_number ||
|
|
claimDetails.eInvoiceNumber || claimDetails.e_invoice_number,
|
|
eInvoiceDate: invoice.invoiceDate || invoice.invoice_date ||
|
|
claimDetails.eInvoiceDate || claimDetails.e_invoice_date,
|
|
dmsNumber: invoice.dmsNumber || invoice.dms_number ||
|
|
claimDetails.dmsNumber || claimDetails.dms_number,
|
|
creditNoteNumber: creditNote.creditNoteNumber || creditNote.credit_note_number ||
|
|
claimDetails.creditNoteNumber || claimDetails.credit_note_number,
|
|
creditNoteDate: creditNote.creditNoteDate || creditNote.credit_note_date ||
|
|
claimDetails.creditNoteDate || claimDetails.credit_note_date,
|
|
creditNoteAmount: creditNote.creditNoteAmount ? Number(creditNote.creditNoteAmount) :
|
|
(creditNote.credit_note_amount ? Number(creditNote.credit_note_amount) :
|
|
(creditNote.creditNoteAmount ? Number(creditNote.creditNoteAmount) :
|
|
(claimDetails.creditNoteAmount ? Number(claimDetails.creditNoteAmount) :
|
|
(claimDetails.credit_note_amount ? Number(claimDetails.credit_note_amount) : undefined)))),
|
|
};
|
|
|
|
// Map claim amounts
|
|
const claimAmount = {
|
|
estimated: activityInfo.estimatedBudget || 0,
|
|
closed: activityInfo.closedExpenses || 0,
|
|
};
|
|
|
|
return {
|
|
activityInfo,
|
|
dealerInfo,
|
|
proposalDetails: proposal,
|
|
ioDetails: Object.keys(ioDetails).some(k => ioDetails[k as keyof typeof ioDetails]) ? ioDetails : undefined,
|
|
dmsDetails: Object.keys(dmsDetails).some(k => dmsDetails[k as keyof typeof dmsDetails]) ? dmsDetails : undefined,
|
|
claimAmount,
|
|
};
|
|
} catch (error) {
|
|
console.error('[claimDataMapper] Error mapping claim data:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine user's role in the request
|
|
*/
|
|
export function determineUserRole(apiRequest: any, currentUserId: string): RequestRole {
|
|
try {
|
|
// Check if user is the initiator
|
|
if (apiRequest.initiatorId === currentUserId ||
|
|
apiRequest.initiator?.userId === currentUserId ||
|
|
apiRequest.requestedBy?.userId === currentUserId) {
|
|
return 'INITIATOR';
|
|
}
|
|
|
|
// Check if user is a dealer (participant with DEALER type)
|
|
const participants = apiRequest.participants || [];
|
|
const dealerParticipant = participants.find((p: any) =>
|
|
(p.userId === currentUserId || p.user?.userId === currentUserId) &&
|
|
(p.participantType === 'DEALER' || p.type === 'DEALER')
|
|
);
|
|
if (dealerParticipant) {
|
|
return 'DEALER';
|
|
}
|
|
|
|
// Check if user is a department lead (approver at level 3)
|
|
const approvalLevels = apiRequest.approvalLevels || [];
|
|
const deptLeadLevel = approvalLevels.find((level: any) =>
|
|
level.levelNumber === 3 &&
|
|
(level.approverId === currentUserId || level.approver?.userId === currentUserId)
|
|
);
|
|
if (deptLeadLevel) {
|
|
return 'DEPARTMENT_LEAD';
|
|
}
|
|
|
|
// Check if user is an approver
|
|
const approverLevel = approvalLevels.find((level: any) =>
|
|
(level.approverId === currentUserId || level.approver?.userId === currentUserId) &&
|
|
level.status === 'PENDING'
|
|
);
|
|
if (approverLevel) {
|
|
return 'APPROVER';
|
|
}
|
|
|
|
// Default to spectator
|
|
return 'SPECTATOR';
|
|
} catch (error) {
|
|
console.error('[claimDataMapper] Error determining user role:', error);
|
|
return 'SPECTATOR';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get role-based visibility settings
|
|
*/
|
|
export function getRoleBasedVisibility(role: RequestRole): RoleVisibility {
|
|
switch (role) {
|
|
case 'INITIATOR':
|
|
return {
|
|
showDealerInfo: true,
|
|
showProposalDetails: true,
|
|
showIODetails: true,
|
|
showDMSDetails: true,
|
|
showClaimAmount: true,
|
|
canEditClaimAmount: false, // Can only edit in specific scenarios
|
|
};
|
|
|
|
case 'DEALER':
|
|
return {
|
|
showDealerInfo: true,
|
|
showProposalDetails: true,
|
|
showIODetails: false,
|
|
showDMSDetails: false,
|
|
showClaimAmount: true,
|
|
canEditClaimAmount: false,
|
|
};
|
|
|
|
case 'DEPARTMENT_LEAD':
|
|
return {
|
|
showDealerInfo: true,
|
|
showProposalDetails: true,
|
|
showIODetails: true,
|
|
showDMSDetails: true,
|
|
showClaimAmount: true,
|
|
canEditClaimAmount: false,
|
|
};
|
|
|
|
case 'APPROVER':
|
|
return {
|
|
showDealerInfo: true,
|
|
showProposalDetails: true,
|
|
showIODetails: true,
|
|
showDMSDetails: true,
|
|
showClaimAmount: true,
|
|
canEditClaimAmount: false,
|
|
};
|
|
|
|
case 'SPECTATOR':
|
|
default:
|
|
return {
|
|
showDealerInfo: false,
|
|
showProposalDetails: false,
|
|
showIODetails: false,
|
|
showDMSDetails: false,
|
|
showClaimAmount: false,
|
|
canEditClaimAmount: false,
|
|
};
|
|
}
|
|
}
|
|
|