Re_Figma_Code/src/pages/Requests/components/RequestCard.tsx

227 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Individual Request Card Component
*/
import { motion } from 'framer-motion';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { User, ArrowRight, TrendingUp, Clock, Pause } from 'lucide-react';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
import type { ConvertedRequest } from '../types/requests.types';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
/**
* Strip HTML tags and convert to plain text for card preview
*/
const stripHtmlTags = (html: string): string => {
if (!html) return '';
// 1. Replace block-level tags with a space to avoid merging words (e.g. </div><div> -> " ")
// This preserves readability for the card preview
let text = html.replace(/<(address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hr|li|main|nav|noscript|ol|p|pre|section|table|tfoot|ul|video)[^>]*>/gi, ' ');
// 2. Replace <br> with space
text = text.replace(/<br\s*\/?>/gi, ' ');
// 3. Strip all other tags
text = text.replace(/<[^>]*>/g, '');
// 4. Clean up extra whitespace
text = text.replace(/\s+/g, ' ').trim();
// 5. Basic HTML entity decoding for common characters
text = text
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'");
return text;
};
interface RequestCardProps {
request: ConvertedRequest;
index: number;
onViewRequest: (requestId: string, requestTitle?: string, status?: string) => void;
}
function formatInr(val: number | null | undefined): string {
if (val == null) return '—';
return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 }).format(val);
}
export function RequestCard({ request, index, onViewRequest }: RequestCardProps) {
const isForm16 = (request?.templateType || (request as any)?.template_type || '').toString().toUpperCase() === 'FORM_16';
const form16 = (request as any)?.form16Submission;
const form16DisplayStatus = form16?.displayStatus;
const isForm16MismatchOrFailed = form16DisplayStatus && /balance mismatch|failed/i.test(String(form16DisplayStatus));
const statusConfig = isForm16 && form16DisplayStatus
? {
color: isForm16MismatchOrFailed ? 'bg-red-100 !text-red-800 border-red-200' : form16DisplayStatus === 'Completed' ? 'bg-green-100 !text-green-800 border-green-200' : 'bg-gray-100 !text-gray-700 border-gray-200',
icon: getStatusConfig(request.status).icon,
label: form16DisplayStatus,
}
: getStatusConfig(request.status);
const priorityConfig = getPriorityConfig(request.priority);
const StatusIcon = statusConfig.icon;
const PriorityIcon = priorityConfig.icon;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<Card
className="group hover:shadow-lg transition-all duration-300 cursor-pointer border border-gray-200 shadow-sm hover:shadow-md"
onClick={() => onViewRequest(request.id, request.title, request.status)}
data-testid={`request-card-${request.id}`}
>
<CardContent className="p-3 sm:p-6">
<div className="space-y-3 sm:space-y-4">
{/* Header with Title and Status Badges */}
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h4
className="text-base sm:text-lg font-semibold text-gray-900 mb-2 group-hover:text-blue-600 transition-colors line-clamp-2"
data-testid="request-title"
>
{request.title}
</h4>
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2 mb-2">
<Badge
variant="outline"
className={`${statusConfig.color} border font-medium text-xs shrink-0`}
data-testid="status-badge"
>
<StatusIcon className="w-3 h-3 mr-1" />
<span className="capitalize">{statusConfig.label}</span>
</Badge>
{((request as any).pauseInfo?.isPaused || (request as any).isPaused) && (
<Badge
variant="outline"
className="bg-orange-50 text-orange-700 border-orange-300 font-medium text-xs shrink-0"
data-testid="pause-badge"
>
<Pause className="w-3 h-3 mr-1" />
Paused
</Badge>
)}
<Badge
variant="outline"
className={`${priorityConfig.color} border font-medium text-xs capitalize shrink-0`}
data-testid="priority-badge"
>
<PriorityIcon className="w-3 h-3 mr-1" />
{request.priority}
</Badge>
{/* Template Type Badge Form 16 shows green "Form 16", others unchanged */}
{(() => {
const templateType = request?.templateType || (request as any)?.template_type || '';
const templateTypeUpper = templateType?.toUpperCase() || '';
// Direct mapping from templateType
let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
if (templateTypeUpper === 'DEALER CLAIM') {
templateLabel = 'Dealer Claim';
templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
} else if (templateTypeUpper === 'FORM_16') {
templateLabel = 'Form 16';
templateColor = 'bg-emerald-100 !text-emerald-700 border-emerald-200';
} else if (templateTypeUpper === 'TEMPLATE') {
templateLabel = 'Template';
}
return (
<Badge
variant="outline"
className={`${templateColor} font-medium text-xs shrink-0`}
data-testid="template-type-badge"
>
{templateLabel}
</Badge>
);
})()}
{request.department && (
<Badge
variant="secondary"
className="bg-purple-100 text-purple-700 text-xs shrink-0"
data-testid="department-badge"
>
{request.department}
</Badge>
)}
</div>
<p
className="text-xs sm:text-sm text-gray-600 mb-2 sm:mb-3 line-clamp-2 leading-relaxed"
data-testid="request-description"
>
{stripHtmlTags(request.description || '') || 'No description provided'}
</p>
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4 text-xs sm:text-sm text-gray-500">
<span className="truncate" data-testid="request-id-display">
<span className="font-medium">ID:</span> {request.displayId || request.id}
</span>
<span className="truncate" data-testid="submitted-date">
<span className="font-medium">Submitted:</span> {formatDateDDMMYYYY(request.submittedDate)}
</span>
{isForm16 && form16 && (
<>
<span className="truncate" data-testid="form16-total-amount">
<span className="font-medium">Total amount:</span> {form16.totalAmount != null ? formatInr(form16.totalAmount) : '—'}
</span>
<span className="truncate" data-testid="form16-credit-note">
<span className="font-medium">Credit note:</span> {form16.creditNoteNumber || '—'}
</span>
</>
)}
</div>
</div>
<ArrowRight className="w-4 h-4 sm:w-5 sm:h-5 text-gray-400 group-hover:text-blue-600 transition-colors flex-shrink-0 mt-1" />
</div>
{/* Current Approver and Level Info Form 16 shows "Form 16 OCR FLOW" instead */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4 pt-3 border-t border-gray-100">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
{(request?.templateType || (request as any)?.template_type || '').toString().toUpperCase() === 'FORM_16' ? (
<div className="flex items-center gap-2 min-w-0">
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-emerald-600 flex-shrink-0" />
<span className="text-xs sm:text-sm font-medium text-emerald-700" data-testid="form16-ocr-flow">
Form 16 OCR FLOW
</span>
</div>
) : (
<>
<div className="flex items-center gap-2 min-w-0">
<User className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-gray-400 flex-shrink-0" />
<span className="text-xs sm:text-sm truncate" data-testid="current-approver">
<span className="text-gray-500">Current Approver:</span> <span className="text-gray-900 font-medium">{request.currentApprover}</span>
</span>
</div>
<div className="flex items-center gap-2">
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-gray-400 flex-shrink-0" />
<span className="text-xs sm:text-sm" data-testid="approval-level">
<span className="text-gray-500">Approval Level:</span> <span className="text-gray-900 font-medium">{request.approverLevel}</span>
</span>
</div>
</>
)}
</div>
<div className="flex items-center gap-1.5 text-xs text-gray-500">
<Clock className="w-3.5 h-3.5 flex-shrink-0" />
<span data-testid="submitted-timestamp">
Submitted: {formatDateDDMMYYYY(request.submittedDate)}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
);
}