312 lines
13 KiB
TypeScript
312 lines
13 KiB
TypeScript
/**
|
|
* Dealer Claim Request Overview Tab
|
|
*
|
|
* This component is specific to Dealer Claim requests.
|
|
* Located in: src/dealer-claim/components/request-detail/
|
|
*/
|
|
|
|
import {
|
|
ActivityInformationCard,
|
|
DealerInformationCard,
|
|
ProposalDetailsCard,
|
|
RequestInitiatorCard,
|
|
} from './claim-cards';
|
|
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
|
import {
|
|
mapToClaimManagementRequest,
|
|
determineUserRole,
|
|
getRoleBasedVisibility,
|
|
type RequestRole,
|
|
} from '@/utils/claimDataMapper';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { RichTextEditor } from '@/components/ui/rich-text-editor';
|
|
import { FormattedDescription } from '@/components/common/FormattedDescription';
|
|
import { CheckCircle, RefreshCw, Loader2 } from 'lucide-react';
|
|
import { formatDateTime } from '@/utils/dateFormatter';
|
|
|
|
interface ClaimManagementOverviewTabProps {
|
|
request: any; // Original request object
|
|
apiRequest: any; // API request data
|
|
currentUserId: string;
|
|
isInitiator: boolean;
|
|
onEditClaimAmount?: () => void;
|
|
className?: string;
|
|
// Closure props
|
|
needsClosure?: boolean;
|
|
conclusionRemark?: string;
|
|
setConclusionRemark?: (value: string) => void;
|
|
conclusionLoading?: boolean;
|
|
conclusionSubmitting?: boolean;
|
|
aiGenerated?: boolean;
|
|
handleGenerateConclusion?: () => void;
|
|
handleFinalizeConclusion?: () => void;
|
|
}
|
|
|
|
export function ClaimManagementOverviewTab({
|
|
request: _request,
|
|
apiRequest,
|
|
currentUserId,
|
|
isInitiator: _isInitiator,
|
|
onEditClaimAmount: _onEditClaimAmount,
|
|
className = '',
|
|
needsClosure = false,
|
|
conclusionRemark = '',
|
|
setConclusionRemark,
|
|
conclusionLoading = false,
|
|
conclusionSubmitting = false,
|
|
aiGenerated = false,
|
|
handleGenerateConclusion,
|
|
handleFinalizeConclusion,
|
|
}: ClaimManagementOverviewTabProps) {
|
|
// Check if this is a claim management request
|
|
if (!isClaimManagementRequest(apiRequest)) {
|
|
return (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<p>This is not a claim management request.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Map API data to claim management structure
|
|
const claimRequest = mapToClaimManagementRequest(apiRequest, currentUserId);
|
|
|
|
if (!claimRequest) {
|
|
console.warn('[ClaimManagementOverviewTab] Failed to map claim data:', {
|
|
apiRequest,
|
|
hasClaimDetails: !!apiRequest?.claimDetails,
|
|
hasProposalDetails: !!apiRequest?.proposalDetails,
|
|
hasCompletionDetails: !!apiRequest?.completionDetails,
|
|
});
|
|
return (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<p>Unable to load claim management data.</p>
|
|
<p className="text-xs mt-2">Please ensure the request has been properly initialized.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Debug: Log mapped data for troubleshooting
|
|
console.debug('[ClaimManagementOverviewTab] Mapped claim data:', {
|
|
activityInfo: claimRequest.activityInfo,
|
|
dealerInfo: claimRequest.dealerInfo,
|
|
hasProposalDetails: !!claimRequest.proposalDetails,
|
|
closedExpenses: claimRequest.activityInfo?.closedExpenses,
|
|
closedExpensesBreakdown: claimRequest.activityInfo?.closedExpensesBreakdown,
|
|
hasDealerCode: !!claimRequest.dealerInfo?.dealerCode,
|
|
hasDealerName: !!claimRequest.dealerInfo?.dealerName,
|
|
});
|
|
|
|
// Determine user's role
|
|
const userRole: RequestRole = determineUserRole(apiRequest, currentUserId);
|
|
|
|
// Get visibility settings based on role
|
|
const visibility = getRoleBasedVisibility(userRole);
|
|
|
|
console.debug('[ClaimManagementOverviewTab] User role and visibility:', {
|
|
userRole,
|
|
visibility,
|
|
currentUserId,
|
|
showDealerInfo: visibility.showDealerInfo,
|
|
dealerInfoPresent: !!(claimRequest.dealerInfo?.dealerCode || claimRequest.dealerInfo?.dealerName),
|
|
});
|
|
|
|
// Extract initiator info from request
|
|
// The apiRequest has initiator object with displayName, email, department, phone, etc.
|
|
const initiatorInfo = {
|
|
name: apiRequest.initiator?.name || apiRequest.initiator?.displayName || apiRequest.initiator?.email || 'Unknown',
|
|
role: apiRequest.initiator?.role || apiRequest.initiator?.designation || 'Initiator',
|
|
department: apiRequest.initiator?.department || apiRequest.department || '',
|
|
email: apiRequest.initiator?.email || 'N/A',
|
|
phone: apiRequest.initiator?.phone || apiRequest.initiator?.mobile,
|
|
};
|
|
|
|
// Debug: Log closure props to help troubleshoot
|
|
console.debug('[ClaimManagementOverviewTab] Closure setup check:', {
|
|
needsClosure,
|
|
requestStatus: apiRequest?.status,
|
|
requestStatusLower: (apiRequest?.status || '').toLowerCase(),
|
|
hasConclusionRemark: !!conclusionRemark,
|
|
conclusionRemarkLength: conclusionRemark?.length || 0,
|
|
conclusionLoading,
|
|
conclusionSubmitting,
|
|
aiGenerated,
|
|
hasHandleGenerate: !!handleGenerateConclusion,
|
|
hasHandleFinalize: !!handleFinalizeConclusion,
|
|
hasSetConclusion: !!setConclusionRemark,
|
|
});
|
|
|
|
return (
|
|
<div className={`space-y-6 ${className}`}>
|
|
{/* Activity Information - Always visible */}
|
|
{/* Dealer-claim module: Business logic for preparing timestamp data */}
|
|
<ActivityInformationCard
|
|
activityInfo={claimRequest.activityInfo}
|
|
createdAt={apiRequest?.createdAt}
|
|
updatedAt={apiRequest?.updatedAt}
|
|
/>
|
|
|
|
{/* Dealer Information - Always visible */}
|
|
<DealerInformationCard dealerInfo={claimRequest.dealerInfo} />
|
|
|
|
{/* Proposal Details - Only shown after dealer submits proposal */}
|
|
{visibility.showProposalDetails && claimRequest.proposalDetails && (
|
|
<ProposalDetailsCard proposalDetails={claimRequest.proposalDetails} />
|
|
)}
|
|
|
|
{/* Request Initiator */}
|
|
<RequestInitiatorCard initiatorInfo={initiatorInfo} />
|
|
|
|
{/* Closed Request Conclusion Remark Display */}
|
|
{apiRequest?.status === 'closed' && apiRequest?.conclusionRemark && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<CheckCircle className="w-5 h-5 text-gray-600" />
|
|
Conclusion Remark
|
|
</CardTitle>
|
|
<CardDescription className="mt-1 text-xs sm:text-sm">Final summary of this closed request</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="pt-4">
|
|
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
|
<FormattedDescription
|
|
content={apiRequest.conclusionRemark || ''}
|
|
className="text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{apiRequest.closureDate && (
|
|
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 border-t border-gray-200 pt-3">
|
|
<span>Request closed on {formatDateTime(apiRequest.closureDate)}</span>
|
|
<span>By {initiatorInfo.name}</span>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Conclusion Remark Section - Closure Setup */}
|
|
{needsClosure && (
|
|
<Card data-testid="conclusion-remark-card">
|
|
<CardHeader className={`bg-gradient-to-r border-b ${
|
|
(apiRequest?.status || '').toLowerCase() === 'rejected'
|
|
? 'from-red-50 to-rose-50 border-red-200'
|
|
: 'from-green-50 to-emerald-50 border-green-200'
|
|
}`}>
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
<div>
|
|
<CardTitle className={`flex items-center gap-2 text-base sm:text-lg ${
|
|
(apiRequest?.status || '').toLowerCase() === 'rejected' ? 'text-red-700' : 'text-green-700'
|
|
}`}>
|
|
<CheckCircle className={`w-5 h-5 ${
|
|
(apiRequest?.status || '').toLowerCase() === 'rejected' ? 'text-red-600' : 'text-green-600'
|
|
}`} />
|
|
Conclusion Remark - Final Step
|
|
</CardTitle>
|
|
<CardDescription className="mt-1 text-xs sm:text-sm">
|
|
{(apiRequest?.status || '').toLowerCase() === 'rejected'
|
|
? 'This request was rejected. Please review the AI-generated closure remark and finalize it to close this request.'
|
|
: 'All approvals are complete. Please review and finalize the conclusion to close this request.'}
|
|
</CardDescription>
|
|
</div>
|
|
{handleGenerateConclusion && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleGenerateConclusion}
|
|
disabled={conclusionLoading}
|
|
className="gap-2 shrink-0"
|
|
data-testid="generate-ai-conclusion-button"
|
|
>
|
|
<RefreshCw className={`w-3.5 h-3.5 ${conclusionLoading ? 'animate-spin' : ''}`} />
|
|
{aiGenerated ? 'Regenerate' : 'Generate with AI'}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="pt-4">
|
|
{conclusionLoading ? (
|
|
<div className="flex items-center justify-center py-8" data-testid="conclusion-loading">
|
|
<div className="text-center">
|
|
<Loader2 className="w-8 h-8 text-blue-600 animate-spin mx-auto mb-2" />
|
|
<p className="text-sm text-gray-600">Preparing conclusion remark...</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<label className="text-sm font-medium text-gray-700">Conclusion Remark</label>
|
|
{aiGenerated && (
|
|
<span className="text-xs text-blue-600" data-testid="ai-generated-label">
|
|
✓ System-generated suggestion (editable)
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{setConclusionRemark && (
|
|
<RichTextEditor
|
|
value={conclusionRemark}
|
|
onChange={(html) => setConclusionRemark(html)}
|
|
placeholder="Enter a professional conclusion remark summarizing the request outcome, key decisions, and approvals..."
|
|
className="text-sm"
|
|
minHeight="160px"
|
|
data-testid="conclusion-remark-textarea"
|
|
/>
|
|
)}
|
|
<p className="text-xs text-blue-600 mt-1">
|
|
💡 Tip: You can paste formatted content (lists, tables) and the formatting will be preserved.
|
|
</p>
|
|
|
|
<div className="flex items-center justify-between mt-2">
|
|
<p className="text-xs text-gray-500">This will be the final summary for this request</p>
|
|
<p className="text-xs text-gray-500" data-testid="character-count">
|
|
{conclusionRemark ? conclusionRemark.replace(/<[^>]*>/g, '').length : 0} / 2000 characters
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
<p className="text-xs sm:text-sm font-semibold text-blue-900 mb-1.5">Finalizing this request will:</p>
|
|
<ul className="text-xs sm:text-sm text-blue-800 space-y-0.5 pl-4">
|
|
<li className="list-disc">Change request status to "CLOSED"</li>
|
|
<li className="list-disc">Notify all participants of closure</li>
|
|
<li className="list-disc">Move request to Closed Requests</li>
|
|
<li className="list-disc">Save conclusion remark permanently</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{handleFinalizeConclusion && (
|
|
<div className="flex gap-3 justify-end pt-3 border-t">
|
|
<Button
|
|
onClick={handleFinalizeConclusion}
|
|
disabled={conclusionSubmitting || !conclusionRemark.trim()}
|
|
className="bg-green-600 hover:bg-green-700 text-white"
|
|
data-testid="finalize-close-button"
|
|
>
|
|
{conclusionSubmitting ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
Finalizing...
|
|
</>
|
|
) : (
|
|
<>
|
|
<CheckCircle className="w-4 h-4 mr-2" />
|
|
Finalize & Close Request
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Export as DealerClaimOverviewTab for consistency
|
|
export { ClaimManagementOverviewTab as DealerClaimOverviewTab };
|