Re_Figma_Code/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx

358 lines
17 KiB
TypeScript

import { motion } from 'framer-motion';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Users, Settings, Shield, User, CheckCircle, Minus, Plus } from 'lucide-react';
import { FormData } from '@/hooks/useCreateRequestForm';
import { useMultiUserSearch } from '@/hooks/useUserSearch';
import { ensureUserExists } from '@/services/userApi';
interface ApprovalWorkflowStepProps {
formData: FormData;
updateFormData: (field: keyof FormData, value: any) => void;
onValidationError: (error: { type: string; email: string; message: string }) => void;
}
/**
* Component: ApprovalWorkflowStep
*
* Purpose: Step 3 - Approval workflow configuration
*
* Features:
* - Configure number of approvers
* - Define approval hierarchy with TAT
* - User search with @ prefix
* - Test IDs for testing
*
* Note: This is a simplified version. Full implementation includes complex approver management.
*/
export function ApprovalWorkflowStep({
formData,
updateFormData,
onValidationError
}: ApprovalWorkflowStepProps) {
const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch();
const handleApproverEmailChange = (index: number, value: string) => {
const newApprovers = [...formData.approvers];
const previousEmail = newApprovers[index]?.email;
const emailChanged = previousEmail !== value;
newApprovers[index] = {
...newApprovers[index],
email: value,
level: index + 1,
userId: emailChanged ? undefined : newApprovers[index]?.userId,
name: emailChanged ? undefined : newApprovers[index]?.name,
department: emailChanged ? undefined : newApprovers[index]?.department,
avatar: emailChanged ? undefined : newApprovers[index]?.avatar
};
updateFormData('approvers', newApprovers);
if (!value || !value.startsWith('@') || value.length < 2) {
clearSearchForIndex(index);
return;
}
searchUsersForIndex(index, value, 10);
};
const handleUserSelect = async (index: number, selectedUser: any) => {
try {
const dbUser = await ensureUserExists({
userId: selectedUser.userId,
email: selectedUser.email,
displayName: selectedUser.displayName,
firstName: selectedUser.firstName,
lastName: selectedUser.lastName,
department: selectedUser.department,
phone: selectedUser.phone,
mobilePhone: selectedUser.mobilePhone,
designation: selectedUser.designation,
jobTitle: selectedUser.jobTitle,
manager: selectedUser.manager,
employeeId: selectedUser.employeeId,
employeeNumber: selectedUser.employeeNumber,
secondEmail: selectedUser.secondEmail,
location: selectedUser.location
});
const updated = [...formData.approvers];
updated[index] = {
...updated[index],
email: selectedUser.email,
name: selectedUser.displayName || [selectedUser.firstName, selectedUser.lastName].filter(Boolean).join(' '),
userId: dbUser.userId,
level: index + 1,
};
updateFormData('approvers', updated);
clearSearchForIndex(index);
} catch (err) {
console.error('Failed to ensure user exists:', err);
onValidationError({
type: 'error',
email: selectedUser.email,
message: 'Failed to validate user. Please try again.'
});
}
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="space-y-6"
data-testid="approval-workflow-step"
>
<div className="text-center mb-8" data-testid="approval-workflow-header">
<div className="w-16 h-16 bg-gradient-to-br from-orange-500 to-red-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Users className="w-8 h-8 text-white" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2" data-testid="approval-workflow-title">
Approval Workflow
</h2>
<p className="text-gray-600" data-testid="approval-workflow-description">
Define the approval hierarchy and assign approvers by email ID.
</p>
</div>
<div className="max-w-4xl mx-auto space-y-8" data-testid="approval-workflow-content">
{/* Number of Approvers */}
<Card data-testid="approval-workflow-config-card">
<CardHeader>
<CardTitle className="flex items-center gap-2" data-testid="approval-workflow-config-title">
<Settings className="w-5 h-5" />
Approval Configuration
</CardTitle>
<CardDescription>
Configure how many approvers you need and define the approval sequence.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div data-testid="approval-workflow-count-field">
<Label className="text-base font-semibold mb-4 block">Number of Approvers *</Label>
<div className="flex items-center gap-4">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const currentCount = formData.approverCount || 1;
const newCount = Math.max(1, currentCount - 1);
updateFormData('approverCount', newCount);
if (formData.approvers.length > newCount) {
updateFormData('approvers', formData.approvers.slice(0, newCount));
}
}}
disabled={(formData.approverCount || 1) <= 1}
data-testid="approval-workflow-decrease-count"
>
<Minus className="w-4 h-4" />
</Button>
<span className="text-2xl font-semibold w-12 text-center" data-testid="approval-workflow-count-display">
{formData.approverCount || 1}
</span>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const currentCount = formData.approverCount || 1;
const newCount = Math.min(10, currentCount + 1);
updateFormData('approverCount', newCount);
}}
disabled={(formData.approverCount || 1) >= 10}
data-testid="approval-workflow-increase-count"
>
<Plus className="w-4 h-4" />
</Button>
</div>
<p className="text-sm text-gray-600 mt-2">
Maximum 10 approvers allowed. Each approver will review sequentially.
</p>
</div>
</CardContent>
</Card>
{/* Approval Hierarchy */}
<Card data-testid="approval-workflow-hierarchy-card">
<CardHeader>
<CardTitle className="flex items-center gap-2" data-testid="approval-workflow-hierarchy-title">
<Shield className="w-5 h-5" />
Approval Hierarchy *
</CardTitle>
<CardDescription>
Define the approval sequence. Each approver will review the request in order from Level 1 to Level {formData.approverCount || 1}.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Initiator Card */}
<div className="p-4 rounded-lg border-2 border-blue-200 bg-blue-50" data-testid="approval-workflow-initiator-card">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-white" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-blue-900">Request Initiator</span>
<Badge variant="secondary" className="text-xs">YOU</Badge>
</div>
<p className="text-sm text-blue-700">Creates and submits the request</p>
</div>
</div>
</div>
{/* Dynamic Approver Cards */}
{Array.from({ length: formData.approverCount || 1 }, (_, index) => {
const level = index + 1;
const isLast = level === (formData.approverCount || 1);
if (!formData.approvers[index]) {
const newApprovers = [...formData.approvers];
newApprovers[index] = { email: '', name: '', level: level, tat: '' as any };
updateFormData('approvers', newApprovers);
}
return (
<div key={level} className="space-y-3" data-testid={`approval-workflow-approver-level-${level}`}>
<div className="flex justify-center">
<div className="w-px h-6 bg-gray-300"></div>
</div>
<div className={`p-4 rounded-lg border-2 transition-all ${
formData.approvers[index]?.email
? 'border-green-200 bg-green-50'
: 'border-gray-200 bg-gray-50'
}`}>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
formData.approvers[index]?.email
? 'bg-green-600'
: 'bg-gray-400'
}`}>
<span className="text-white font-semibold">{level}</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-3">
<span className="font-semibold text-gray-900">
Approver Level {level}
</span>
{isLast && (
<Badge variant="destructive" className="text-xs">FINAL APPROVER</Badge>
)}
</div>
<div className="space-y-4">
<div data-testid={`approval-workflow-approver-${level}-email-field`}>
<div className="flex items-center justify-between mb-1">
<Label htmlFor={`approver-${level}`} className="text-sm font-medium">
Email Address *
</Label>
{formData.approvers[index]?.email && formData.approvers[index]?.userId && (
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
<CheckCircle className="w-3 h-3 mr-1" />
Verified
</Badge>
)}
</div>
<div className="relative">
<Input
id={`approver-${level}`}
type="email"
placeholder="approver@royalenfield.com"
value={formData.approvers[index]?.email || ''}
onChange={(e) => handleApproverEmailChange(index, e.target.value)}
className="h-10 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full"
data-testid={`approval-workflow-approver-${level}-email-input`}
/>
{/* Search suggestions dropdown */}
{(userSearchLoading[index] || (userSearchResults[index]?.length || 0) > 0) && (
<div className="absolute left-0 right-0 top-full mt-1 z-50 border rounded-md bg-white shadow-lg">
{userSearchLoading[index] ? (
<div className="p-2 text-xs text-gray-500">Searching...</div>
) : (
<ul className="max-h-56 overflow-auto divide-y">
{userSearchResults[index]?.map((u) => (
<li
key={u.userId}
className="p-2 text-sm cursor-pointer hover:bg-gray-50"
onClick={() => handleUserSelect(index, u)}
data-testid={`approval-workflow-approver-${level}-search-result-${u.userId}`}
>
<div className="font-medium text-gray-900">{u.displayName || u.email}</div>
<div className="text-xs text-gray-600">{u.email}</div>
</li>
))}
</ul>
)}
</div>
)}
</div>
</div>
<div data-testid={`approval-workflow-approver-${level}-tat-field`}>
<Label htmlFor={`tat-${level}`} className="text-sm font-medium">
TAT (Turn Around Time) *
</Label>
<div className="flex items-center gap-2 mt-1">
<Input
id={`tat-${level}`}
type="number"
placeholder={formData.approvers[index]?.tatType === 'days' ? '7' : '24'}
min="1"
max={formData.approvers[index]?.tatType === 'days' ? '30' : '720'}
value={formData.approvers[index]?.tat || ''}
onChange={(e) => {
const newApprovers = [...formData.approvers];
newApprovers[index] = {
...newApprovers[index],
tat: parseInt(e.target.value) || '',
level: level,
tatType: formData.approvers[index]?.tatType || 'hours'
};
updateFormData('approvers', newApprovers);
}}
className="h-10 border-2 border-gray-300 focus:border-blue-500 flex-1"
data-testid={`approval-workflow-approver-${level}-tat-input`}
/>
<Select
value={formData.approvers[index]?.tatType || 'hours'}
onValueChange={(value) => {
const newApprovers = [...formData.approvers];
newApprovers[index] = {
...newApprovers[index],
tatType: value as 'hours' | 'days',
level: level,
tat: ''
};
updateFormData('approvers', newApprovers);
}}
data-testid={`approval-workflow-approver-${level}-tat-type-select`}
>
<SelectTrigger className="w-20 h-10 border-2 border-gray-300">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hours">Hours</SelectItem>
<SelectItem value="days">Days</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
})}
</CardContent>
</Card>
</div>
</motion.div>
);
}