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

194 lines
8.8 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.

/**
* Request Card Component
* Displays a single request card in the My Requests list
*/
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ArrowRight, User, TrendingUp, Clock, Pause } from 'lucide-react';
import { motion } from 'framer-motion';
import { MyRequest } from '../types/myRequests.types';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
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: MyRequest;
index: number;
onViewRequest: (requestId: string, requestTitle?: string, status?: string) => void;
}
export function RequestCard({ request, index, onViewRequest }: RequestCardProps) {
const statusConfig = 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">{request.status}</span>
</Badge>
{(request.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" */}
{(() => {
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>
);
})()}
</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>
</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" */}
<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>
);
}