295 lines
13 KiB
TypeScript
295 lines
13 KiB
TypeScript
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Check, Clock, Users, Info, Flame, Target, TrendingUp, FolderOpen, ArrowLeft } from 'lucide-react';
|
|
import { RequestTemplate } from '@/hooks/useCreateRequestForm';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
interface TemplateSelectionStepProps {
|
|
templates: RequestTemplate[];
|
|
selectedTemplate: RequestTemplate | null;
|
|
onSelectTemplate: (template: RequestTemplate) => void;
|
|
adminTemplates?: RequestTemplate[];
|
|
}
|
|
|
|
const getPriorityIcon = (priority: string) => {
|
|
switch (priority) {
|
|
case 'high': return <Flame className="w-4 h-4 text-red-600" />;
|
|
case 'medium': return <Target className="w-4 h-4 text-orange-600" />;
|
|
case 'low': return <TrendingUp className="w-4 h-4 text-green-600" />;
|
|
default: return <Target className="w-4 h-4 text-gray-600" />;
|
|
}
|
|
};
|
|
|
|
export function TemplateSelectionStep({
|
|
templates,
|
|
selectedTemplate,
|
|
onSelectTemplate,
|
|
adminTemplates = []
|
|
}: TemplateSelectionStepProps) {
|
|
const [viewMode, setViewMode] = useState<'main' | 'admin'>('main');
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const handleTemplateClick = (template: RequestTemplate) => {
|
|
if (template.id === 'admin-templates-category') {
|
|
setViewMode('admin');
|
|
} else {
|
|
if (viewMode === 'admin') {
|
|
// If selecting an actual admin template, redirect to dedicated flow
|
|
navigate(`/create-admin-request/${template.id}`);
|
|
} else {
|
|
// Default behavior for standard templates
|
|
onSelectTemplate(template);
|
|
}
|
|
}
|
|
};
|
|
|
|
const displayTemplates = viewMode === 'main'
|
|
? [
|
|
...templates,
|
|
{
|
|
id: 'admin-templates-category',
|
|
name: 'Admin Templates',
|
|
description: 'Browse standardized request workflows created by your organization administrators',
|
|
category: 'Organization',
|
|
icon: FolderOpen,
|
|
estimatedTime: 'Variable',
|
|
commonApprovers: [],
|
|
suggestedSLA: 0,
|
|
priority: 'medium',
|
|
fields: {}
|
|
} as any
|
|
]
|
|
: adminTemplates;
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
className="min-h-full flex flex-col items-center justify-center py-8"
|
|
data-testid="template-selection-step"
|
|
>
|
|
{/* Header Section */}
|
|
<div className="text-center mb-12 max-w-3xl" data-testid="template-selection-header">
|
|
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-4" data-testid="template-selection-title">
|
|
{viewMode === 'main' ? 'Choose Your Request Type' : 'Organization Templates'}
|
|
</h1>
|
|
<p className="text-lg text-gray-600" data-testid="template-selection-description">
|
|
{viewMode === 'main'
|
|
? 'Start with a pre-built template for faster approvals, or create a custom request tailored to your needs.'
|
|
: 'Select a pre-configured workflow template defined by your organization.'}
|
|
</p>
|
|
</div>
|
|
|
|
{viewMode === 'admin' && (
|
|
<div className="w-full max-w-6xl mb-6 flex justify-start">
|
|
<Button variant="ghost" className="gap-2" onClick={() => setViewMode('main')}>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to All Types
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Template Cards Grid */}
|
|
<div
|
|
className="w-full max-w-6xl grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"
|
|
data-testid="template-selection-grid"
|
|
>
|
|
{displayTemplates.length === 0 && viewMode === 'admin' ? (
|
|
<div className="col-span-full text-center py-12 text-gray-500 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200">
|
|
<FolderOpen className="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
|
<p>No admin templates available yet.</p>
|
|
</div>
|
|
) : (
|
|
displayTemplates.map((template) => {
|
|
const isComingSoon = template.id === 'existing-template' && viewMode === 'main'; // Only show coming soon for placeholder
|
|
const isDisabled = isComingSoon;
|
|
const isCategoryCard = template.id === 'admin-templates-category';
|
|
const isCustomCard = template.id === 'custom';
|
|
const isSelected = selectedTemplate?.id === template.id;
|
|
|
|
return (
|
|
<motion.div
|
|
key={template.id}
|
|
whileHover={!isDisabled ? { scale: 1.03 } : {}}
|
|
whileTap={!isDisabled ? { scale: 0.98 } : {}}
|
|
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
|
data-testid={`template-card-${template.id}`}
|
|
>
|
|
<Card
|
|
className={`h-full transition-all duration-300 border-2 ${isDisabled
|
|
? 'border-gray-200 bg-gray-50/50 opacity-85 cursor-not-allowed'
|
|
: isSelected
|
|
? 'border-blue-500 shadow-xl bg-blue-50/50 ring-2 ring-blue-200 cursor-pointer'
|
|
: isCategoryCard
|
|
? 'border-blue-200 bg-blue-50/30 hover:border-blue-400 hover:shadow-lg cursor-pointer'
|
|
: 'border-gray-200 hover:border-blue-300 hover:shadow-lg cursor-pointer'
|
|
}`}
|
|
onClick={!isDisabled ? () => handleTemplateClick(template) : undefined}
|
|
data-testid={`template-card-${template.id}-clickable`}
|
|
>
|
|
<CardHeader className="space-y-4 pb-4">
|
|
<div className="flex items-start justify-between">
|
|
<div
|
|
className={`w-14 h-14 rounded-xl flex items-center justify-center ${isSelected
|
|
? 'bg-blue-100'
|
|
: isCategoryCard
|
|
? 'bg-blue-100'
|
|
: 'bg-gray-100'
|
|
}`}
|
|
data-testid={`template-card-${template.id}-icon`}
|
|
>
|
|
<template.icon
|
|
className={`w-7 h-7 ${isSelected
|
|
? 'text-blue-600'
|
|
: isCategoryCard
|
|
? 'text-blue-600'
|
|
: 'text-gray-600'
|
|
}`}
|
|
/>
|
|
</div>
|
|
{isSelected && (
|
|
<motion.div
|
|
initial={{ scale: 0 }}
|
|
animate={{ scale: 1 }}
|
|
transition={{ type: "spring", stiffness: 500, damping: 15 }}
|
|
data-testid={`template-card-${template.id}-selected-indicator`}
|
|
>
|
|
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center">
|
|
<Check className="w-5 h-5 text-white" />
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
<div className="text-left">
|
|
<div className="flex items-start justify-between gap-2 mb-2">
|
|
<CardTitle className="text-xl" data-testid={`template-card-${template.id}-name`}>
|
|
{template.name}
|
|
</CardTitle>
|
|
{isComingSoon && (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs bg-yellow-100 text-yellow-700 border-yellow-300 font-semibold"
|
|
data-testid={`template-card-${template.id}-coming-soon-badge`}
|
|
>
|
|
Coming Soon
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary" className="text-xs" data-testid={`template-card-${template.id}-category`}>
|
|
{template.category}
|
|
</Badge>
|
|
{getPriorityIcon(template.priority)}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="pt-0 space-y-4">
|
|
<p
|
|
className="text-sm text-gray-600 leading-relaxed line-clamp-2"
|
|
data-testid={`template-card-${template.id}-description`}
|
|
>
|
|
{template.description}
|
|
</p>
|
|
|
|
{!isCategoryCard && (
|
|
<>
|
|
<Separator />
|
|
<div className="grid grid-cols-2 gap-3 text-xs text-gray-500">
|
|
<div className="flex items-center gap-1.5" data-testid={`template-card-${template.id}-estimated-time`}>
|
|
<Clock className="w-3.5 h-3.5" />
|
|
<span>{template.estimatedTime}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5" data-testid={`template-card-${template.id}-approvers-count`}>
|
|
<Users className="w-3.5 h-3.5" />
|
|
<span>{template.commonApprovers?.length || 0} approvers</span>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
{isCategoryCard && (
|
|
<div className="pt-2">
|
|
<p className="text-xs text-blue-600 font-medium flex items-center gap-1">
|
|
Click to browse templates →
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* Template Details Card */}
|
|
<AnimatePresence>
|
|
{selectedTemplate && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20, height: 0 }}
|
|
animate={{ opacity: 1, y: 0, height: 'auto' }}
|
|
exit={{ opacity: 0, y: -20, height: 0 }}
|
|
transition={{ duration: 0.3 }}
|
|
className="w-full max-w-6xl"
|
|
data-testid="template-details-card"
|
|
>
|
|
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 border-2 border-blue-200">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-blue-900" data-testid="template-details-title">
|
|
<Info className="w-5 h-5" />
|
|
{selectedTemplate.name} - Template Details
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-sla">
|
|
<Label className="text-blue-900 font-semibold">Suggested SLA</Label>
|
|
<p className="text-blue-700 mt-1">{selectedTemplate.suggestedSLA} hours</p>
|
|
</div>
|
|
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-priority">
|
|
<Label className="text-blue-900 font-semibold">Priority Level</Label>
|
|
<div className="flex items-center gap-1 mt-1">
|
|
{getPriorityIcon(selectedTemplate.priority)}
|
|
<span className="text-blue-700 capitalize">{selectedTemplate.priority}</span>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-duration">
|
|
<Label className="text-blue-900 font-semibold">Estimated Duration</Label>
|
|
<p className="text-blue-700 mt-1">{selectedTemplate.estimatedTime}</p>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-approvers">
|
|
<Label className="text-blue-900 font-semibold">Approvers</Label>
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
{selectedTemplate.commonApprovers?.length > 0 ? (
|
|
selectedTemplate.commonApprovers.map((approver, index) => (
|
|
<Badge
|
|
key={`${selectedTemplate.id}-approver-${index}-${approver}`}
|
|
variant="outline"
|
|
className="border-blue-300 text-blue-700 bg-white"
|
|
data-testid={`template-details-approver-${index}`}
|
|
>
|
|
{approver}
|
|
</Badge>
|
|
))
|
|
) : (
|
|
<span className="text-sm text-gray-500 italic">No specific approvers defined</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|