Re_Figma_Code/src/dealer-claim/components/request-detail/OverviewTab.tsx

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