597 lines
22 KiB
TypeScript
597 lines
22 KiB
TypeScript
import { useState } from 'react';
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog';
|
|
import { Button } from '../ui/button';
|
|
import { Input } from '../ui/input';
|
|
import { Label } from '../ui/label';
|
|
import { Textarea } from '../ui/textarea';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
|
import { Badge } from '../ui/badge';
|
|
import { Avatar, AvatarFallback } from '../ui/avatar';
|
|
import { Progress } from '../ui/progress';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
|
import { Switch } from '../ui/switch';
|
|
import { Calendar } from '../ui/calendar';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
|
import {
|
|
ArrowLeft,
|
|
ArrowRight,
|
|
Calendar as CalendarIcon,
|
|
Upload,
|
|
X,
|
|
FileText,
|
|
Check,
|
|
Users
|
|
} from 'lucide-react';
|
|
import { format } from 'date-fns';
|
|
|
|
interface NewRequestModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSubmit?: (requestData: any) => void;
|
|
}
|
|
|
|
export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProps) {
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
const [formData, setFormData] = useState({
|
|
title: '',
|
|
description: '',
|
|
priority: '',
|
|
slaEndDate: undefined as Date | undefined,
|
|
approvers: [] as any[],
|
|
workflowType: 'sequential',
|
|
spectators: [] as any[],
|
|
documents: [] as File[]
|
|
});
|
|
|
|
const totalSteps = 5;
|
|
|
|
// Mock users for selection
|
|
const availableUsers = [
|
|
{ id: '1', name: 'Mike Johnson', role: 'Team Lead', avatar: 'MJ' },
|
|
{ id: '2', name: 'Lisa Wong', role: 'Finance Manager', avatar: 'LW' },
|
|
{ id: '3', name: 'David Kumar', role: 'Department Head', avatar: 'DK' },
|
|
{ id: '4', name: 'Anna Smith', role: 'Marketing Coordinator', avatar: 'AS' },
|
|
{ id: '5', name: 'John Doe', role: 'Budget Analyst', avatar: 'JD' }
|
|
];
|
|
|
|
const updateFormData = (field: string, value: any) => {
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const addApprover = (user: any) => {
|
|
if (!formData.approvers.find(a => a.id === user.id)) {
|
|
updateFormData('approvers', [...formData.approvers, user]);
|
|
}
|
|
};
|
|
|
|
const removeApprover = (userId: string) => {
|
|
updateFormData('approvers', formData.approvers.filter(a => a.id !== userId));
|
|
};
|
|
|
|
const addSpectator = (user: any) => {
|
|
if (!formData.spectators.find(s => s.id === user.id)) {
|
|
updateFormData('spectators', [...formData.spectators, user]);
|
|
}
|
|
};
|
|
|
|
const removeSpectator = (userId: string) => {
|
|
updateFormData('spectators', formData.spectators.filter(s => s.id !== userId));
|
|
};
|
|
|
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = Array.from(event.target.files || []);
|
|
updateFormData('documents', [...formData.documents, ...files]);
|
|
};
|
|
|
|
const removeDocument = (index: number) => {
|
|
const newDocs = formData.documents.filter((_, i) => i !== index);
|
|
updateFormData('documents', newDocs);
|
|
};
|
|
|
|
const isStepValid = () => {
|
|
switch (currentStep) {
|
|
case 1:
|
|
return formData.title && formData.description && formData.priority && formData.slaEndDate;
|
|
case 2:
|
|
return formData.approvers.length > 0;
|
|
case 3:
|
|
return true; // Spectators are optional
|
|
case 4:
|
|
return true; // Documents are optional
|
|
case 5:
|
|
return true; // Review step
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const nextStep = () => {
|
|
if (currentStep < totalSteps && isStepValid()) {
|
|
setCurrentStep(currentStep + 1);
|
|
}
|
|
};
|
|
|
|
const prevStep = () => {
|
|
if (currentStep > 1) {
|
|
setCurrentStep(currentStep - 1);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
if (isStepValid()) {
|
|
onSubmit?.(formData);
|
|
onClose();
|
|
// Reset form
|
|
setCurrentStep(1);
|
|
setFormData({
|
|
title: '',
|
|
description: '',
|
|
priority: '',
|
|
slaEndDate: undefined,
|
|
approvers: [],
|
|
workflowType: 'sequential',
|
|
spectators: [],
|
|
documents: []
|
|
});
|
|
}
|
|
};
|
|
|
|
const renderStepContent = () => {
|
|
switch (currentStep) {
|
|
case 1:
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="title">Request Title *</Label>
|
|
<Input
|
|
id="title"
|
|
placeholder="Enter a descriptive title for your request"
|
|
value={formData.title}
|
|
onChange={(e) => updateFormData('title', e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="description">Description *</Label>
|
|
<Textarea
|
|
id="description"
|
|
placeholder="Provide detailed information about your request"
|
|
className="min-h-[120px]"
|
|
value={formData.description}
|
|
onChange={(e) => updateFormData('description', e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>Priority *</Label>
|
|
<Select value={formData.priority} onValueChange={(value) => updateFormData('priority', value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select priority" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="high">High Priority</SelectItem>
|
|
<SelectItem value="medium">Medium Priority</SelectItem>
|
|
<SelectItem value="low">Low Priority</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>SLA End Date *</Label>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" className="w-full justify-start text-left">
|
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
{formData.slaEndDate ? format(formData.slaEndDate, 'PPP') : 'Pick a date'}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-0">
|
|
<Calendar
|
|
mode="single"
|
|
selected={formData.slaEndDate}
|
|
onSelect={(date) => updateFormData('slaEndDate', date)}
|
|
initialFocus
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 2:
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<Label>Workflow Type</Label>
|
|
<div className="flex items-center space-x-2">
|
|
<Label htmlFor="workflow-sequential" className="text-sm">Sequential</Label>
|
|
<Switch
|
|
id="workflow-sequential"
|
|
checked={formData.workflowType === 'parallel'}
|
|
onCheckedChange={(checked) => updateFormData('workflowType', checked ? 'parallel' : 'sequential')}
|
|
/>
|
|
<Label htmlFor="workflow-sequential" className="text-sm">Parallel</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-3 bg-muted/50 rounded-lg text-sm text-muted-foreground">
|
|
{formData.workflowType === 'sequential'
|
|
? 'Approvers will review the request one after another in the order you specify.'
|
|
: 'All approvers will review the request simultaneously.'
|
|
}
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Add Approvers *</Label>
|
|
<Select onValueChange={(userId) => {
|
|
const user = availableUsers.find(u => u.id === userId);
|
|
if (user) addApprover(user);
|
|
}}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select users to add as approvers" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableUsers
|
|
.filter(user => !formData.approvers.find(a => a.id === user.id))
|
|
.map(user => (
|
|
<SelectItem key={user.id} value={user.id}>
|
|
<div className="flex items-center gap-2">
|
|
<Avatar className="h-6 w-6">
|
|
<AvatarFallback className="bg-re-green text-white text-xs">
|
|
{user.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<p className="font-medium">{user.name}</p>
|
|
<p className="text-sm text-muted-foreground">{user.role}</p>
|
|
</div>
|
|
</div>
|
|
</SelectItem>
|
|
))
|
|
}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{formData.approvers.length > 0 && (
|
|
<div className="space-y-2">
|
|
<Label>Selected Approvers ({formData.approvers.length})</Label>
|
|
<div className="space-y-2">
|
|
{formData.approvers.map((approver, index) => (
|
|
<div key={approver.id} className="flex items-center justify-between p-2 bg-muted/30 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2">
|
|
{formData.workflowType === 'sequential' && (
|
|
<Badge variant="outline" className="text-xs">
|
|
{index + 1}
|
|
</Badge>
|
|
)}
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarFallback className="bg-re-green text-white text-xs">
|
|
{approver.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-sm">{approver.name}</p>
|
|
<p className="text-xs text-muted-foreground">{approver.role}</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeApprover(approver.id)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
case 3:
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Add Spectators (Optional)</Label>
|
|
<p className="text-sm text-muted-foreground mb-2">
|
|
Spectators can view the request and participate in work notes but cannot approve or edit.
|
|
</p>
|
|
<Select onValueChange={(userId) => {
|
|
const user = availableUsers.find(u => u.id === userId);
|
|
if (user) addSpectator(user);
|
|
}}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select users to add as spectators" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableUsers
|
|
.filter(user =>
|
|
!formData.spectators.find(s => s.id === user.id) &&
|
|
!formData.approvers.find(a => a.id === user.id)
|
|
)
|
|
.map(user => (
|
|
<SelectItem key={user.id} value={user.id}>
|
|
<div className="flex items-center gap-2">
|
|
<Avatar className="h-6 w-6">
|
|
<AvatarFallback className="bg-re-light-green text-white text-xs">
|
|
{user.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<p className="font-medium">{user.name}</p>
|
|
<p className="text-sm text-muted-foreground">{user.role}</p>
|
|
</div>
|
|
</div>
|
|
</SelectItem>
|
|
))
|
|
}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{formData.spectators.length > 0 && (
|
|
<div className="space-y-2">
|
|
<Label>Selected Spectators ({formData.spectators.length})</Label>
|
|
<div className="space-y-2">
|
|
{formData.spectators.map((spectator) => (
|
|
<div key={spectator.id} className="flex items-center justify-between p-2 bg-muted/30 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarFallback className="bg-re-light-green text-white text-xs">
|
|
{spectator.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<p className="font-medium text-sm">{spectator.name}</p>
|
|
<p className="text-xs text-muted-foreground">{spectator.role}</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeSpectator(spectator.id)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
case 4:
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Upload Documents (Optional)</Label>
|
|
<p className="text-sm text-muted-foreground mb-2">
|
|
Attach supporting documents for your request. Maximum 10MB per file.
|
|
</p>
|
|
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center">
|
|
<Upload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
|
<p className="text-sm text-muted-foreground mb-2">
|
|
Drag and drop files here, or click to browse
|
|
</p>
|
|
<input
|
|
type="file"
|
|
multiple
|
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
|
|
onChange={handleFileUpload}
|
|
className="hidden"
|
|
id="file-upload"
|
|
/>
|
|
<Label htmlFor="file-upload" className="cursor-pointer">
|
|
<Button variant="outline" size="sm" type="button">
|
|
Browse Files
|
|
</Button>
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
|
|
{formData.documents.length > 0 && (
|
|
<div className="space-y-2">
|
|
<Label>Uploaded Documents ({formData.documents.length})</Label>
|
|
<div className="space-y-2">
|
|
{formData.documents.map((file, index) => (
|
|
<div key={index} className="flex items-center justify-between p-2 bg-muted/30 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="h-6 w-6 text-muted-foreground" />
|
|
<div>
|
|
<p className="font-medium text-sm">{file.name}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{(file.size / (1024 * 1024)).toFixed(2)} MB
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeDocument(index)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
case 5:
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h3 className="font-semibold mb-2">Review Your Request</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Please review all details before submitting your request.
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<FileText className="h-5 w-5" />
|
|
Basic Information
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div>
|
|
<Label>Title</Label>
|
|
<p className="text-sm">{formData.title}</p>
|
|
</div>
|
|
<div>
|
|
<Label>Description</Label>
|
|
<p className="text-sm text-muted-foreground">{formData.description}</p>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label>Priority</Label>
|
|
<Badge className="mt-1">
|
|
{formData.priority}
|
|
</Badge>
|
|
</div>
|
|
<div>
|
|
<Label>SLA End Date</Label>
|
|
<p className="text-sm">
|
|
{formData.slaEndDate ? format(formData.slaEndDate, 'PPP') : 'Not set'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Users className="h-5 w-5" />
|
|
Workflow & Participants
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div>
|
|
<Label>Workflow Type</Label>
|
|
<Badge variant="outline" className="mt-1">
|
|
{formData.workflowType}
|
|
</Badge>
|
|
</div>
|
|
<div>
|
|
<Label>Approvers ({formData.approvers.length})</Label>
|
|
<div className="flex flex-wrap gap-2 mt-1">
|
|
{formData.approvers.map((approver, index) => (
|
|
<Badge key={approver.id} variant="secondary">
|
|
{formData.workflowType === 'sequential' && `${index + 1}. `}
|
|
{approver.name}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{formData.spectators.length > 0 && (
|
|
<div>
|
|
<Label>Spectators ({formData.spectators.length})</Label>
|
|
<div className="flex flex-wrap gap-2 mt-1">
|
|
{formData.spectators.map((spectator) => (
|
|
<Badge key={spectator.id} variant="outline">
|
|
{spectator.name}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{formData.documents.length > 0 && (
|
|
<div>
|
|
<Label>Documents ({formData.documents.length})</Label>
|
|
<div className="flex flex-wrap gap-2 mt-1">
|
|
{formData.documents.map((doc, index) => (
|
|
<Badge key={index} variant="outline">
|
|
{doc.name}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Request</DialogTitle>
|
|
<DialogDescription>
|
|
Step {currentStep} of {totalSteps}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="space-y-2">
|
|
<Progress value={(currentStep / totalSteps) * 100} className="h-2" />
|
|
<div className="flex justify-between text-xs text-muted-foreground">
|
|
<span>Basics</span>
|
|
<span>Workflow</span>
|
|
<span>Participants</span>
|
|
<span>Documents</span>
|
|
<span>Review</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Step Content */}
|
|
<div className="py-4">
|
|
{renderStepContent()}
|
|
</div>
|
|
|
|
{/* Navigation Buttons */}
|
|
<div className="flex justify-between">
|
|
<Button
|
|
variant="outline"
|
|
onClick={prevStep}
|
|
disabled={currentStep === 1}
|
|
>
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
Back
|
|
</Button>
|
|
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
{currentStep === totalSteps ? (
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={!isStepValid()}
|
|
className="bg-re-green hover:bg-re-green/90"
|
|
>
|
|
<Check className="h-4 w-4 mr-2" />
|
|
Submit Request
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
onClick={nextStep}
|
|
disabled={!isStepValid()}
|
|
className="bg-re-green hover:bg-re-green/90"
|
|
>
|
|
Next
|
|
<ArrowRight className="h-4 w-4 ml-2" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |