Re_Figma_Code/src/pages/MyRequests/MyRequests.tsx

394 lines
16 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
FileText,
Search,
Clock,
CheckCircle,
XCircle,
AlertCircle,
User,
ArrowRight,
TrendingUp,
Eye,
Edit,
Flame,
Target
} from 'lucide-react';
import { motion } from 'framer-motion';
import workflowApi from '@/services/workflowApi';
interface MyRequestsProps {
onViewRequest: (requestId: string, requestTitle?: string) => void;
dynamicRequests?: any[];
}
// Removed mock data; list renders API data only
const getPriorityConfig = (priority: string) => {
switch (priority) {
case 'express':
return {
color: 'bg-red-100 text-red-800 border-red-200',
icon: Flame,
iconColor: 'text-red-600'
};
case 'standard':
return {
color: 'bg-blue-100 text-blue-800 border-blue-200',
icon: Target,
iconColor: 'text-blue-600'
};
default:
return {
color: 'bg-gray-100 text-gray-800 border-gray-200',
icon: Target,
iconColor: 'text-gray-600'
};
}
};
const getStatusConfig = (status: string) => {
switch (status) {
case 'approved':
return {
color: 'bg-green-100 text-green-800 border-green-200',
icon: CheckCircle,
iconColor: 'text-green-600'
};
case 'rejected':
return {
color: 'bg-red-100 text-red-800 border-red-200',
icon: XCircle,
iconColor: 'text-red-600'
};
case 'pending':
return {
color: 'bg-yellow-100 text-yellow-800 border-yellow-200',
icon: Clock,
iconColor: 'text-yellow-600'
};
case 'in-review':
return {
color: 'bg-blue-100 text-blue-800 border-blue-200',
icon: Eye,
iconColor: 'text-blue-600'
};
case 'draft':
return {
color: 'bg-gray-100 text-gray-800 border-gray-200',
icon: Edit,
iconColor: 'text-gray-600'
};
default:
return {
color: 'bg-gray-100 text-gray-800 border-gray-200',
icon: AlertCircle,
iconColor: 'text-gray-600'
};
}
};
export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsProps) {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [apiRequests, setApiRequests] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
let mounted = true;
(async () => {
try {
setLoading(true);
const result = await workflowApi.listMyWorkflows({ page: 1, limit: 20 });
const items = Array.isArray(result?.data) ? result.data : Array.isArray(result) ? result : [];
if (!mounted) return;
setApiRequests(items);
} catch (_) {
if (!mounted) return;
setApiRequests([]);
} finally {
if (mounted) setLoading(false);
}
})();
return () => { mounted = false; };
}, []);
// Convert API/dynamic requests to the format expected by this component
const sourceRequests = (apiRequests.length ? apiRequests : dynamicRequests);
const convertedDynamicRequests = sourceRequests.map((req: any) => ({
id: req.requestNumber || req.request_number || req.requestId || req.id || req.request_id, // Use requestNumber as primary identifier
requestId: req.requestId || req.id || req.request_id, // Keep requestId for API calls if needed
displayId: req.requestNumber || req.request_number || req.id,
title: req.title,
description: req.description,
status: (req.status || '').toString().toLowerCase().replace('in_progress','in-review'),
priority: (req.priority || '').toString().toLowerCase(),
department: req.department,
submittedDate: req.submittedAt || (req.createdAt ? new Date(req.createdAt).toISOString().split('T')[0] : undefined),
currentApprover: req.currentApprover?.name || req.currentApprover?.email || '—',
approverLevel: req.currentLevel && req.totalLevels ? `${req.currentLevel} of ${req.totalLevels}` : (req.currentStep && req.totalSteps ? `${req.currentStep} of ${req.totalSteps}` : '—'),
dueDate: req.dueDate ? new Date(req.dueDate).toISOString().split('T')[0] : undefined,
templateType: req.templateType,
templateName: req.templateName
}));
// Use only API/dynamic requests
const allRequests = convertedDynamicRequests;
const [priorityFilter, setPriorityFilter] = useState('all');
// Filter requests based on search and filters
const filteredRequests = allRequests.filter(request => {
const matchesSearch =
request.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
request.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
request.id.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || request.status === statusFilter;
const matchesPriority = priorityFilter === 'all' || request.priority === priorityFilter;
return matchesSearch && matchesStatus && matchesPriority;
});
// Stats calculation
const stats = {
total: allRequests.length,
pending: allRequests.filter(r => r.status === 'pending').length,
approved: allRequests.filter(r => r.status === 'approved').length,
inReview: allRequests.filter(r => r.status === 'in-review').length,
rejected: allRequests.filter(r => r.status === 'rejected').length,
draft: allRequests.filter(r => r.status === 'draft').length
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-gray-900 flex items-center gap-2">
<User className="w-7 h-7 text-[--re-green]" />
My Requests
</h1>
<p className="text-gray-600 mt-1">
Track and manage all your submitted requests
</p>
</div>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<Card className="bg-gradient-to-br from-blue-50 to-blue-100 border-blue-200">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-blue-700 font-medium">Total</p>
<p className="text-2xl font-bold text-blue-900">{stats.total}</p>
</div>
<FileText className="w-8 h-8 text-blue-600" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-orange-50 to-orange-100 border-orange-200">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-orange-700 font-medium">In Progress</p>
<p className="text-2xl font-bold text-orange-900">{stats.pending + stats.inReview}</p>
</div>
<Clock className="w-8 h-8 text-orange-600" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-green-50 to-green-100 border-green-200">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-green-700 font-medium">Approved</p>
<p className="text-2xl font-bold text-green-900">{stats.approved}</p>
</div>
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-red-50 to-red-100 border-red-200">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-red-700 font-medium">Rejected</p>
<p className="text-2xl font-bold text-red-900">{stats.rejected}</p>
</div>
<XCircle className="w-8 h-8 text-red-600" />
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-gray-50 to-gray-100 border-gray-200">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-700 font-medium">Draft</p>
<p className="text-2xl font-bold text-gray-900">{stats.draft}</p>
</div>
<Edit className="w-8 h-8 text-gray-600" />
</div>
</CardContent>
</Card>
</div>
{/* Filters and Search */}
<Card className="border-gray-200">
<CardContent className="p-6">
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Search requests by title, description, or ID..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 bg-white border-gray-300 hover:border-gray-400 focus:border-re-green focus:ring-1 focus:ring-re-green"
/>
</div>
<div className="flex gap-3">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-32 bg-white border-gray-300 hover:border-gray-400 focus:border-re-green focus:ring-1 focus:ring-re-green">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="in-review">In Review</SelectItem>
<SelectItem value="approved">Approved</SelectItem>
<SelectItem value="rejected">Rejected</SelectItem>
</SelectContent>
</Select>
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger className="w-32 bg-white border-gray-300 hover:border-gray-400 focus:border-re-green focus:ring-1 focus:ring-re-green">
<SelectValue placeholder="Priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Priority</SelectItem>
<SelectItem value="express">Express</SelectItem>
<SelectItem value="standard">Standard</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* Requests List */}
<div className="space-y-4">
{loading ? (
<Card>
<CardContent className="p-6 text-sm text-gray-600">Loading your requests</CardContent>
</Card>
) : filteredRequests.length === 0 ? (
<Card>
<CardContent className="p-12 text-center">
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No requests found</h3>
<p className="text-gray-600">
{searchTerm || statusFilter !== 'all' || priorityFilter !== 'all'
? 'Try adjusting your search or filters'
: 'You haven\'t created any requests yet'}
</p>
</CardContent>
</Card>
) : (
filteredRequests.map((request, index) => (
<motion.div
key={request.id}
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)}
>
<CardContent className="p-6">
<div className="space-y-4">
{/* Header with Title and Status Badges */}
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<h4 className="text-lg font-semibold text-gray-900 mb-2 group-hover:text-blue-600 transition-colors">
{request.title}
</h4>
<div className="flex items-center gap-2 mb-2">
<Badge
variant="outline"
className={`${getStatusConfig(request.status).color} border font-medium text-xs`}
>
{(() => {
const IconComponent = getStatusConfig(request.status).icon;
return <IconComponent className="w-3 h-3 mr-1" />;
})()}
{request.status}
</Badge>
<Badge
variant="outline"
className={`${getPriorityConfig(request.priority).color} border font-medium text-xs capitalize`}
>
{request.priority}
</Badge>
{(request as any).templateType && (
<Badge variant="secondary" className="bg-purple-100 text-purple-700 text-xs">
<FileText className="w-3 h-3 mr-1" />
Template: {(request as any).templateName}
</Badge>
)}
</div>
<p className="text-sm text-gray-600 mb-3">
{request.description}
</p>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span><span className="font-medium">ID:</span> {(request as any).displayId || request.id}</span>
<span><span className="font-medium">Submitted:</span> {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'}</span>
</div>
</div>
<ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-blue-600 transition-colors flex-shrink-0" />
</div>
{/* Current Approver and Level Info */}
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<span className="text-sm">
<span className="text-gray-500">Current:</span> <span className="text-gray-900 font-medium">{request.currentApprover}</span>
</span>
</div>
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-gray-400" />
<span className="text-sm">
<span className="text-gray-500">Level:</span> <span className="text-gray-900 font-medium">{request.approverLevel}</span>
</span>
</div>
</div>
<div className="text-right">
<span className="text-sm text-gray-500">
<span className="font-medium">Estimated completion:</span>
</span>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))
)}
</div>
</div>
);
}