419 lines
17 KiB
TypeScript
419 lines
17 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
|
import { Button } from './ui/button';
|
|
import { Input } from './ui/input';
|
|
import { Badge } from './ui/badge';
|
|
import { Avatar, AvatarFallback } from './ui/avatar';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
|
import {
|
|
FileText,
|
|
Search,
|
|
Filter,
|
|
Clock,
|
|
CheckCircle,
|
|
XCircle,
|
|
AlertCircle,
|
|
TrendingUp,
|
|
Calendar,
|
|
User,
|
|
ArrowRight,
|
|
MoreHorizontal,
|
|
Eye,
|
|
Edit,
|
|
Copy
|
|
} from 'lucide-react';
|
|
import { motion } from 'motion/react';
|
|
|
|
interface MyRequestsProps {
|
|
onViewRequest: (requestId: string, requestTitle?: string) => void;
|
|
dynamicRequests?: any[];
|
|
}
|
|
|
|
// Mock data for user's requests
|
|
const MY_REQUESTS_DATA = [
|
|
{
|
|
id: 'RE-REQ-2024-CM-001',
|
|
title: 'Dealer Marketing Activity Claim - Diwali Festival Campaign',
|
|
description: 'Claim request for dealer-led Diwali festival marketing campaign',
|
|
status: 'pending',
|
|
priority: 'standard',
|
|
department: 'Marketing - West Zone',
|
|
submittedDate: '2024-10-07',
|
|
currentApprover: 'Royal Motors Mumbai (Dealer)',
|
|
approverLevel: '1 of 8',
|
|
dueDate: '2024-10-16',
|
|
templateType: 'claim-management',
|
|
templateName: 'Claim Management',
|
|
estimatedCompletion: '2024-10-16'
|
|
},
|
|
{
|
|
id: 'RE-REQ-001',
|
|
title: 'Marketing Campaign Budget Approval',
|
|
description: 'Request for Q4 marketing campaign budget allocation for new motorcycle launch',
|
|
status: 'pending',
|
|
priority: 'express',
|
|
department: 'Marketing',
|
|
submittedDate: '2024-10-05',
|
|
currentApprover: 'Sarah Johnson',
|
|
approverLevel: '2 of 3',
|
|
dueDate: '2024-10-12'
|
|
},
|
|
{
|
|
id: 'RE-REQ-002',
|
|
title: 'IT Equipment Purchase Request',
|
|
description: 'New laptops and workstations for the development team',
|
|
status: 'approved',
|
|
priority: 'standard',
|
|
submittedDate: '2024-09-28',
|
|
currentApprover: 'Completed',
|
|
approverLevel: '3 of 3',
|
|
dueDate: '2024-10-01'
|
|
},
|
|
{
|
|
id: 'RE-REQ-003',
|
|
title: 'Training Program Authorization',
|
|
description: 'Employee skill development program for technical team',
|
|
status: 'in-review',
|
|
priority: 'standard',
|
|
submittedDate: '2024-10-03',
|
|
currentApprover: 'Michael Chen',
|
|
approverLevel: '1 of 2',
|
|
estimatedCompletion: '2024-10-10'
|
|
},
|
|
{
|
|
id: 'RE-REQ-004',
|
|
title: 'Vendor Contract Renewal',
|
|
description: 'Annual renewal for supply chain vendor contracts',
|
|
status: 'rejected',
|
|
priority: 'express',
|
|
submittedDate: '2024-09-25',
|
|
currentApprover: 'Rejected by Alex Kumar',
|
|
approverLevel: '1 of 3',
|
|
estimatedCompletion: 'N/A'
|
|
},
|
|
{
|
|
id: 'RE-REQ-005',
|
|
title: 'Office Space Renovation',
|
|
description: 'Workspace renovation for improved employee experience',
|
|
status: 'draft',
|
|
priority: 'standard',
|
|
submittedDate: '2024-10-07',
|
|
currentApprover: 'Not submitted',
|
|
approverLevel: '0 of 2',
|
|
estimatedCompletion: 'Pending submission'
|
|
}
|
|
];
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'approved':
|
|
return <CheckCircle className="w-4 h-4 text-green-600" />;
|
|
case 'rejected':
|
|
return <XCircle className="w-4 h-4 text-red-600" />;
|
|
case 'pending':
|
|
return <Clock className="w-4 h-4 text-yellow-600" />;
|
|
case 'in-review':
|
|
return <AlertCircle className="w-4 h-4 text-blue-600" />;
|
|
case 'draft':
|
|
return <Edit className="w-4 h-4 text-gray-600" />;
|
|
default:
|
|
return <FileText className="w-4 h-4 text-gray-600" />;
|
|
}
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
const baseClasses = "text-xs font-medium px-2 py-1 rounded-full";
|
|
switch (status) {
|
|
case 'approved':
|
|
return <Badge className={`${baseClasses} bg-green-100 text-green-800 border-green-200`}>Approved</Badge>;
|
|
case 'rejected':
|
|
return <Badge className={`${baseClasses} bg-red-100 text-red-800 border-red-200`}>Rejected</Badge>;
|
|
case 'pending':
|
|
return <Badge className={`${baseClasses} bg-yellow-100 text-yellow-800 border-yellow-200`}>Pending</Badge>;
|
|
case 'in-review':
|
|
return <Badge className={`${baseClasses} bg-blue-100 text-blue-800 border-blue-200`}>In Review</Badge>;
|
|
case 'draft':
|
|
return <Badge className={`${baseClasses} bg-gray-100 text-gray-800 border-gray-200`}>Draft</Badge>;
|
|
default:
|
|
return <Badge className={`${baseClasses} bg-gray-100 text-gray-800 border-gray-200`}>Unknown</Badge>;
|
|
}
|
|
};
|
|
|
|
const getPriorityBadge = (priority: string) => {
|
|
switch (priority) {
|
|
case 'express':
|
|
return <Badge variant="destructive" className="text-xs">Express</Badge>;
|
|
case 'standard':
|
|
return <Badge className="bg-blue-100 text-blue-800 border-blue-200 text-xs">Standard</Badge>;
|
|
default:
|
|
return <Badge variant="secondary" className="text-xs">Normal</Badge>;
|
|
}
|
|
};
|
|
|
|
export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsProps) {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
|
|
// Convert dynamic requests to the format expected by this component
|
|
const convertedDynamicRequests = dynamicRequests.map(req => ({
|
|
id: req.id,
|
|
title: req.title,
|
|
description: req.description,
|
|
status: req.status,
|
|
priority: req.priority,
|
|
department: req.department,
|
|
submittedDate: new Date(req.createdAt).toISOString().split('T')[0],
|
|
currentApprover: req.approvalFlow?.[0]?.approver || 'Current User (Initiator)',
|
|
approverLevel: `${req.currentStep} of ${req.totalSteps}`,
|
|
dueDate: new Date(req.dueDate).toISOString().split('T')[0],
|
|
templateType: req.templateType,
|
|
templateName: req.templateName
|
|
}));
|
|
|
|
// Merge static mock data with dynamic requests (dynamic requests first)
|
|
const allRequests = [...convertedDynamicRequests, ...MY_REQUESTS_DATA];
|
|
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>
|
|
<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"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-32">
|
|
<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">
|
|
<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">
|
|
{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="hover:shadow-lg transition-all duration-200 cursor-pointer group"
|
|
onClick={() => onViewRequest(request.id, request.title)}>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-start gap-4 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
{getStatusIcon(request.status)}
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
|
<h3 className="font-semibold text-gray-900 group-hover:text-[--re-green] transition-colors">
|
|
{request.title}
|
|
</h3>
|
|
{getStatusBadge(request.status)}
|
|
{getPriorityBadge(request.priority)}
|
|
{(request as any).templateType && (
|
|
<Badge className="text-xs bg-purple-100 text-purple-800 border-purple-200">
|
|
<FileText className="w-3 h-3 mr-1" />
|
|
Template: {(request as any).templateName}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-600 mb-3 line-clamp-2">
|
|
{request.description}
|
|
</p>
|
|
|
|
<div className="flex items-center gap-6 text-xs text-gray-500">
|
|
<div className="flex items-center gap-1">
|
|
<span className="font-medium text-gray-700">ID:</span>
|
|
<span className="font-mono">{request.id}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Calendar className="w-3 h-3" />
|
|
<span>Submitted: {new Date(request.submittedDate).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 ml-4">
|
|
<ArrowRight className="w-4 h-4 text-gray-400 group-hover:text-[--re-green] transition-colors" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
|
|
<div className="flex items-center gap-4 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<User className="w-4 h-4 text-gray-400" />
|
|
<span className="text-gray-600">
|
|
Current: <span className="font-medium text-gray-900">{request.currentApprover}</span>
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<TrendingUp className="w-4 h-4 text-gray-400" />
|
|
<span className="text-gray-600">
|
|
Level: <span className="font-medium text-gray-900">{request.approverLevel}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-sm text-gray-600">
|
|
<span className="font-medium">Estimated completion:</span> {request.estimatedCompletion}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |