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

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 &rarr;
</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>
);
}