Re_Figma_Code/src/pages/ClosedRequests/ClosedRequests.tsx

467 lines
20 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Calendar, Filter, Search, FileText, AlertCircle, CheckCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, Settings2, X, XCircle } from 'lucide-react';
import workflowApi from '@/services/workflowApi';
interface Request {
id: string;
title: string;
description: string;
status: 'approved' | 'rejected';
priority: 'express' | 'standard';
initiator: { name: string; avatar: string };
createdAt: string;
dueDate?: string;
reason?: string;
department?: string;
}
interface ClosedRequestsProps {
onViewRequest?: (requestId: string, requestTitle?: string) => void;
}
// Removed static data; will load from API
// Utility functions
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: AlertCircle,
iconColor: 'text-red-600'
};
default:
return {
color: 'bg-gray-100 text-gray-800 border-gray-200',
icon: AlertCircle,
iconColor: 'text-gray-600'
};
}
};
export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
const [searchTerm, setSearchTerm] = useState('');
const [priorityFilter, setPriorityFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority'>('due');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const [items, setItems] = useState<Request[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
let mounted = true;
(async () => {
try {
setLoading(true);
const result = await workflowApi.listClosedByMe({ page: 1, limit: 50 });
const data = Array.isArray((result as any)?.data)
? (result as any).data
: Array.isArray((result as any)?.data?.data)
? (result as any).data.data
: Array.isArray(result as any)
? (result as any)
: [];
if (!mounted) return;
const mapped: Request[] = data
.filter((r: any) => ['APPROVED', 'REJECTED'].includes((r.status || '').toString()))
.map((r: any) => ({
id: r.requestNumber || r.request_number || r.requestId, // Use requestNumber as primary identifier
requestId: r.requestId, // Keep requestId for reference
displayId: r.requestNumber || r.request_number || r.requestId,
title: r.title,
description: r.description,
status: (r.status || '').toString().toLowerCase(),
priority: (r.priority || '').toString().toLowerCase(),
initiator: { name: r.initiator?.displayName || r.initiator?.email || '—', avatar: (r.initiator?.displayName || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase() },
createdAt: r.submittedAt || '—',
dueDate: undefined,
reason: r.conclusionRemark,
department: r.department
}));
setItems(mapped);
} finally {
if (mounted) setLoading(false);
}
})();
return () => { mounted = false; };
}, []);
const filteredAndSortedRequests = useMemo(() => {
let filtered = items.filter(request => {
const matchesSearch =
request.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
request.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
request.initiator.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesPriority = priorityFilter === 'all' || request.priority === priorityFilter;
const matchesStatus = statusFilter === 'all' || request.status === statusFilter;
return matchesSearch && matchesPriority && matchesStatus;
});
// Sort requests
filtered.sort((a, b) => {
let aValue: any, bValue: any;
switch (sortBy) {
case 'created':
aValue = new Date(a.createdAt);
bValue = new Date(b.createdAt);
break;
case 'due':
aValue = new Date(a.dueDate);
bValue = new Date(b.dueDate);
break;
case 'priority':
const priorityOrder = { express: 2, standard: 1 };
aValue = priorityOrder[a.priority as keyof typeof priorityOrder];
bValue = priorityOrder[b.priority as keyof typeof priorityOrder];
break;
default:
return 0;
}
if (sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
return filtered;
}, [items, searchTerm, priorityFilter, statusFilter, sortBy, sortOrder]);
const clearFilters = () => {
setSearchTerm('');
setPriorityFilter('all');
setStatusFilter('all');
};
const activeFiltersCount = [
searchTerm,
priorityFilter !== 'all' ? priorityFilter : null,
statusFilter !== 'all' ? statusFilter : null
].filter(Boolean).length;
return (
<div className="space-y-4 sm:space-y-6 max-w-7xl mx-auto">
{/* Enhanced Header */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4 md:gap-6">
<div className="space-y-1 sm:space-y-2">
<div className="flex items-center gap-2 sm:gap-3">
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl flex items-center justify-center shadow-lg">
<FileText className="w-5 h-5 sm:w-6 sm:h-6 text-white" />
</div>
<div>
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900">Closed Requests</h1>
<p className="text-sm sm:text-base text-gray-600">Review completed and archived requests</p>
</div>
</div>
</div>
<div className="flex items-center gap-2 sm:gap-3">
<Badge variant="secondary" className="text-xs sm:text-sm md:text-base px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 bg-slate-100 text-slate-800 font-semibold">
{loading ? 'Loading…' : `${filteredAndSortedRequests.length} closed`}
<span className="hidden sm:inline ml-1">requests</span>
</Badge>
<Button variant="outline" size="sm" className="gap-1 sm:gap-2 h-8 sm:h-9">
<RefreshCw className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span className="hidden sm:inline">Refresh</span>
</Button>
</div>
</div>
{/* Enhanced Filters Section */}
<Card className="shadow-lg border-0">
<CardHeader className="pb-3 sm:pb-4 px-3 sm:px-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 sm:gap-3">
<div className="p-1.5 sm:p-2 bg-blue-100 rounded-lg">
<Filter className="h-4 w-4 sm:h-5 sm:w-5 text-blue-600" />
</div>
<div>
<CardTitle className="text-base sm:text-lg">Filters & Search</CardTitle>
<CardDescription className="text-xs sm:text-sm">
{activeFiltersCount > 0 && (
<span className="text-blue-600 font-medium">
{activeFiltersCount} filter{activeFiltersCount > 1 ? 's' : ''} active
</span>
)}
</CardDescription>
</div>
</div>
<div className="flex items-center gap-1 sm:gap-2">
{activeFiltersCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-red-600 hover:bg-red-50 gap-1 h-8 sm:h-9 px-2 sm:px-3"
>
<X className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
<span className="text-xs sm:text-sm">Clear</span>
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
className="gap-1 sm:gap-2 h-8 sm:h-9 px-2 sm:px-3"
>
<Settings2 className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span className="text-xs sm:text-sm hidden md:inline">{showAdvancedFilters ? 'Basic' : 'Advanced'}</span>
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3 sm:space-y-4 px-3 sm:px-6">
{/* Primary filters */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-3.5 h-3.5 sm:w-4 sm:h-4" />
<Input
placeholder="Search requests, IDs..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 sm:pl-10 h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white transition-colors"
/>
</div>
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white">
<SelectValue placeholder="All Priorities" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Priorities</SelectItem>
<SelectItem value="express">
<div className="flex items-center gap-2">
<Flame className="w-4 h-4 text-orange-600" />
<span>Express</span>
</div>
</SelectItem>
<SelectItem value="standard">
<div className="flex items-center gap-2">
<Target className="w-4 h-4 text-blue-600" />
<span>Standard</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white">
<SelectValue placeholder="All Statuses" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="approved">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>Approved</span>
</div>
</SelectItem>
<SelectItem value="rejected">
<div className="flex items-center gap-2">
<XCircle className="w-4 h-4 text-red-600" />
<span>Rejected</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<div className="flex gap-2">
<Select value={sortBy} onValueChange={(value: any) => setSortBy(value)}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="due">Due Date</SelectItem>
<SelectItem value="created">Date Created</SelectItem>
<SelectItem value="priority">Priority</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
className="px-2 sm:px-3 h-9 sm:h-10 md:h-11"
>
{sortOrder === 'asc' ? <SortAsc className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> : <SortDesc className="w-3.5 h-3.5 sm:w-4 sm:h-4" />}
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Requests List */}
<div className="space-y-4">
{filteredAndSortedRequests.map((request) => {
const priorityConfig = getPriorityConfig(request.priority);
const statusConfig = getStatusConfig(request.status);
return (
<Card
key={request.id}
className="group hover:shadow-xl transition-all duration-300 cursor-pointer border-0 shadow-md hover:scale-[1.01]"
onClick={() => onViewRequest?.(request.id, request.title)}
>
<CardContent className="p-3 sm:p-6">
<div className="flex flex-col sm:flex-row items-start gap-3 sm:gap-6">
{/* Priority Indicator */}
<div className="flex sm:flex-col items-center gap-2 pt-1 w-full sm:w-auto">
<div className={`p-2 sm:p-3 rounded-xl ${priorityConfig.color} border flex-shrink-0`}>
<priorityConfig.icon className={`w-4 h-4 sm:w-5 sm:h-5 ${priorityConfig.iconColor}`} />
</div>
<Badge
variant="outline"
className={`text-xs font-medium ${priorityConfig.color} capitalize flex-shrink-0`}
>
{request.priority}
</Badge>
</div>
{/* Main Content */}
<div className="flex-1 min-w-0 space-y-3 sm:space-y-4 w-full">
{/* Header */}
<div className="flex items-start justify-between gap-2 sm:gap-4">
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-1.5 sm:gap-3 mb-2">
<h3 className="text-sm sm:text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
{(request as any).displayId || request.id}
</h3>
<Badge
variant="outline"
className={`${statusConfig.color} border font-medium text-xs shrink-0`}
>
<statusConfig.icon className="w-3 h-3 mr-1" />
<span className="capitalize">{request.status}</span>
</Badge>
{request.department && (
<Badge variant="secondary" className="bg-gray-100 text-gray-700 text-xs hidden sm:inline-flex shrink-0">
{request.department}
</Badge>
)}
</div>
<h4 className="text-base sm:text-xl font-bold text-gray-900 mb-2 line-clamp-2">
{request.title}
</h4>
<p className="text-xs sm:text-sm text-gray-600 line-clamp-2 leading-relaxed">
{request.description}
</p>
</div>
<div className="flex flex-col items-end gap-2 flex-shrink-0">
<ArrowRight className="w-4 h-4 sm:w-5 sm:h-5 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
{/* Status Info */}
<div className="flex items-center gap-2 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg">
<div className="flex items-center gap-2 min-w-0">
<CheckCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-green-500 flex-shrink-0" />
<span className="text-xs sm:text-sm text-gray-700 font-medium truncate">
{request.reason}
</span>
</div>
</div>
{/* Participants & Metadata */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-6">
<div className="flex items-center gap-2 min-w-0">
<Avatar className="h-7 w-7 sm:h-8 sm:w-8 ring-2 ring-white shadow-sm flex-shrink-0">
<AvatarFallback className="bg-slate-700 text-white text-xs sm:text-sm font-semibold">
{request.initiator.avatar}
</AvatarFallback>
</Avatar>
<div className="min-w-0">
<p className="text-xs sm:text-sm font-medium text-gray-900 truncate">{request.initiator.name}</p>
<p className="text-xs text-gray-500">Initiator</p>
</div>
</div>
<div className="text-left sm:text-right">
<div className="flex flex-col gap-1 text-xs text-gray-500">
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3 flex-shrink-0" />
<span className="truncate">Created {request.createdAt}</span>
</span>
<span className="truncate">Closed {request.dueDate}</span>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
{/* Empty State */}
{filteredAndSortedRequests.length === 0 && (
<Card className="shadow-lg border-0">
<CardContent className="flex flex-col items-center justify-center py-16">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<FileText className="h-8 w-8 text-gray-400" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">No requests found</h3>
<p className="text-gray-600 text-center max-w-md">
{searchTerm || activeFiltersCount > 0
? 'Try adjusting your filters or search terms to see more results.'
: 'No closed requests available at the moment.'
}
</p>
{activeFiltersCount > 0 && (
<Button
variant="outline"
className="mt-4"
onClick={clearFilters}
>
Clear all filters
</Button>
)}
</CardContent>
</Card>
)}
</div>
);
}