149 lines
6.5 KiB
TypeScript
149 lines
6.5 KiB
TypeScript
/**
|
|
* 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, FileText } 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 '';
|
|
|
|
// Check if we're in a browser environment
|
|
if (typeof document === 'undefined') {
|
|
// Fallback for SSR: use regex to strip HTML tags
|
|
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
// Create a temporary div to parse HTML
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = html;
|
|
|
|
// Get text content (automatically strips HTML tags)
|
|
let text = tempDiv.textContent || tempDiv.innerText || '';
|
|
|
|
// Clean up extra whitespace
|
|
text = text.replace(/\s+/g, ' ').trim();
|
|
|
|
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>
|
|
<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>
|
|
{request.templateType && (
|
|
<Badge
|
|
variant="secondary"
|
|
className="bg-purple-100 text-purple-700 text-xs shrink-0 hidden sm:inline-flex"
|
|
data-testid="template-badge"
|
|
>
|
|
<FileText className="w-3 h-3 mr-1" />
|
|
Template: {request.templateName}
|
|
</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 */}
|
|
<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">
|
|
<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>
|
|
);
|
|
}
|
|
|