Re_Figma_Code/src/utils/claimDataMapper.ts

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,
};
}
}