satrted implementing admin templates implemented api to create template enhancing the request preview & detail

This commit is contained in:
laxmanhalaki 2026-01-22 19:24:01 +05:30
parent 1391e2d2f5
commit ec8987032f
12 changed files with 1170 additions and 197 deletions

View File

@ -20,6 +20,8 @@ import { Settings } from '@/pages/Settings';
import { Notifications } from '@/pages/Notifications'; import { Notifications } from '@/pages/Notifications';
import { DetailedReports } from '@/pages/DetailedReports'; import { DetailedReports } from '@/pages/DetailedReports';
import { Admin } from '@/pages/Admin'; import { Admin } from '@/pages/Admin';
import { CreateTemplate } from '@/pages/Admin/Templates/CreateTemplate';
import { CreateAdminRequest } from '@/pages/CreateAdminRequest/CreateAdminRequest';
import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal'; import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
import { Toaster } from '@/components/ui/sonner'; import { Toaster } from '@/components/ui/sonner';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -483,6 +485,33 @@ function AppRoutes({ onLogout }: AppProps) {
} }
/> />
{/* Admin Routes - Placed higher to prevent matching issues */}
<Route
path="/admin/create-template"
element={
<PageLayout currentPage="admin" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
<CreateTemplate />
</PageLayout>
}
/>
{/* Create Request from Admin Template (Dedicated Flow) */}
<Route
path="/create-admin-request/:templateId"
element={
<CreateAdminRequest />
}
/>
<Route
path="/admin"
element={
<PageLayout currentPage="admin" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
<Admin />
</PageLayout>
}
/>
{/* Open Requests */} {/* Open Requests */}
<Route <Route
path="/open-requests" path="/open-requests"
@ -648,15 +677,9 @@ function AppRoutes({ onLogout }: AppProps) {
} }
/> />
{/* Admin Control Panel */}
<Route
path="/admin"
element={
<PageLayout currentPage="admin" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
<Admin />
</PageLayout>
}
/>
</Routes> </Routes>
<Toaster <Toaster

View File

@ -61,6 +61,7 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
{ id: 'dashboard', label: 'Dashboard', icon: Home }, { id: 'dashboard', label: 'Dashboard', icon: Home },
// Add "All Requests" for all users (admin sees org-level, regular users see their participant requests) // Add "All Requests" for all users (admin sees org-level, regular users see their participant requests)
{ id: 'requests', label: 'All Requests', icon: List }, { id: 'requests', label: 'All Requests', icon: List },
{ id: 'admin/create-template', label: 'Create Template', icon: Plus, adminOnly: true }, // Added Create Template
]; ];
// Add remaining menu items // Add remaining menu items
@ -233,18 +234,21 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
</div> </div>
<div className="p-3 flex-1 overflow-y-auto"> <div className="p-3 flex-1 overflow-y-auto">
<div className="space-y-2"> <div className="space-y-2">
{menuItems.map((item) => ( {menuItems.filter(item => !item.adminOnly || (user as any)?.role === 'ADMIN').map((item) => (
<button <button
key={item.id} key={item.id}
onClick={() => { onClick={() => {
if (item.id === 'admin/create-template') {
onNavigate?.('admin/create-template');
} else {
onNavigate?.(item.id); onNavigate?.(item.id);
}
// Close sidebar on mobile after navigation // Close sidebar on mobile after navigation
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
setSidebarOpen(false); setSidebarOpen(false);
} }
}} }}
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors ${ className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors ${currentPage === item.id
currentPage === item.id
? 'bg-re-green text-white font-medium' ? 'bg-re-green text-white font-medium'
: 'text-gray-300 hover:bg-gray-900 hover:text-white' : 'text-gray-300 hover:bg-gray-900 hover:text-white'
}`} }`}
@ -342,8 +346,7 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
{notifications.map((notif) => ( {notifications.map((notif) => (
<div <div
key={notif.notificationId} key={notif.notificationId}
className={`p-3 hover:bg-gray-50 cursor-pointer transition-colors ${ className={`p-3 hover:bg-gray-50 cursor-pointer transition-colors ${!notif.isRead ? 'bg-blue-50' : ''
!notif.isRead ? 'bg-blue-50' : ''
}`} }`}
onClick={() => handleNotificationClick(notif)} onClick={() => handleNotificationClick(notif)}
> >

View File

@ -1,15 +1,19 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Check, Clock, Users, Info, Flame, Target, TrendingUp } from 'lucide-react'; import { Check, Clock, Users, Info, Flame, Target, TrendingUp, FolderOpen, ArrowLeft } from 'lucide-react';
import { RequestTemplate } from '@/hooks/useCreateRequestForm'; import { RequestTemplate } from '@/hooks/useCreateRequestForm';
import { Button } from '@/components/ui/button';
interface TemplateSelectionStepProps { interface TemplateSelectionStepProps {
templates: RequestTemplate[]; templates: RequestTemplate[];
selectedTemplate: RequestTemplate | null; selectedTemplate: RequestTemplate | null;
onSelectTemplate: (template: RequestTemplate) => void; onSelectTemplate: (template: RequestTemplate) => void;
adminTemplates?: RequestTemplate[];
} }
const getPriorityIcon = (priority: string) => { const getPriorityIcon = (priority: string) => {
@ -21,21 +25,48 @@ const getPriorityIcon = (priority: string) => {
} }
}; };
/**
* Component: TemplateSelectionStep
*
* Purpose: Step 1 - Template selection for request creation
*
* Features:
* - Displays available templates
* - Shows template details when selected
* - Test IDs for testing
*/
export function TemplateSelectionStep({ export function TemplateSelectionStep({
templates, templates,
selectedTemplate, selectedTemplate,
onSelectTemplate onSelectTemplate,
adminTemplates = []
}: TemplateSelectionStepProps) { }: 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 ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@ -47,21 +78,41 @@ export function TemplateSelectionStep({
{/* Header Section */} {/* Header Section */}
<div className="text-center mb-12 max-w-3xl" data-testid="template-selection-header"> <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"> <h1 className="text-4xl lg:text-5xl font-bold text-gray-900 mb-4" data-testid="template-selection-title">
Choose Your Request Type {viewMode === 'main' ? 'Choose Your Request Type' : 'Organization Templates'}
</h1> </h1>
<p className="text-lg text-gray-600" data-testid="template-selection-description"> <p className="text-lg text-gray-600" data-testid="template-selection-description">
Start with a pre-built template for faster approvals, or create a custom request tailored to your needs. {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> </p>
</div> </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 */} {/* Template Cards Grid */}
<div <div
className="w-full max-w-6xl grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8" 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" data-testid="template-selection-grid"
> >
{templates.map((template) => { {displayTemplates.length === 0 && viewMode === 'admin' ? (
const isComingSoon = template.id === 'existing-template'; <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 isDisabled = isComingSoon;
const isCategoryCard = template.id === 'admin-templates-category';
const isCustomCard = template.id === 'custom';
const isSelected = selectedTemplate?.id === template.id;
return ( return (
<motion.div <motion.div
@ -72,35 +123,38 @@ export function TemplateSelectionStep({
data-testid={`template-card-${template.id}`} data-testid={`template-card-${template.id}`}
> >
<Card <Card
className={`h-full transition-all duration-300 border-2 ${ className={`h-full transition-all duration-300 border-2 ${isDisabled
isDisabled
? 'border-gray-200 bg-gray-50/50 opacity-85 cursor-not-allowed' ? 'border-gray-200 bg-gray-50/50 opacity-85 cursor-not-allowed'
: selectedTemplate?.id === template.id : isSelected
? 'border-blue-500 shadow-xl bg-blue-50/50 ring-2 ring-blue-200 cursor-pointer' ? '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' : 'border-gray-200 hover:border-blue-300 hover:shadow-lg cursor-pointer'
}`} }`}
onClick={!isDisabled ? () => onSelectTemplate(template) : undefined} onClick={!isDisabled ? () => handleTemplateClick(template) : undefined}
data-testid={`template-card-${template.id}-clickable`} data-testid={`template-card-${template.id}-clickable`}
> >
<CardHeader className="space-y-4 pb-4"> <CardHeader className="space-y-4 pb-4">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div <div
className={`w-14 h-14 rounded-xl flex items-center justify-center ${ className={`w-14 h-14 rounded-xl flex items-center justify-center ${isSelected
selectedTemplate?.id === template.id ? 'bg-blue-100'
: isCategoryCard
? 'bg-blue-100' ? 'bg-blue-100'
: 'bg-gray-100' : 'bg-gray-100'
}`} }`}
data-testid={`template-card-${template.id}-icon`} data-testid={`template-card-${template.id}-icon`}
> >
<template.icon <template.icon
className={`w-7 h-7 ${ className={`w-7 h-7 ${isSelected
selectedTemplate?.id === template.id ? 'text-blue-600'
: isCategoryCard
? 'text-blue-600' ? 'text-blue-600'
: 'text-gray-600' : 'text-gray-600'
}`} }`}
/> />
</div> </div>
{selectedTemplate?.id === template.id && ( {isSelected && (
<motion.div <motion.div
initial={{ scale: 0 }} initial={{ scale: 0 }}
animate={{ scale: 1 }} animate={{ scale: 1 }}
@ -143,6 +197,9 @@ export function TemplateSelectionStep({
> >
{template.description} {template.description}
</p> </p>
{!isCategoryCard && (
<>
<Separator /> <Separator />
<div className="grid grid-cols-2 gap-3 text-xs text-gray-500"> <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`}> <div className="flex items-center gap-1.5" data-testid={`template-card-${template.id}-estimated-time`}>
@ -151,14 +208,24 @@ export function TemplateSelectionStep({
</div> </div>
<div className="flex items-center gap-1.5" data-testid={`template-card-${template.id}-approvers-count`}> <div className="flex items-center gap-1.5" data-testid={`template-card-${template.id}-approvers-count`}>
<Users className="w-3.5 h-3.5" /> <Users className="w-3.5 h-3.5" />
<span>{template.commonApprovers.length} approvers</span> <span>{template.commonApprovers?.length || 0} approvers</span>
</div> </div>
</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> </CardContent>
</Card> </Card>
</motion.div> </motion.div>
); );
})} })
)}
</div> </div>
{/* Template Details Card */} {/* Template Details Card */}
@ -183,7 +250,7 @@ export function TemplateSelectionStep({
<div className="grid grid-cols-1 md:grid-cols-3 gap-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"> <div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-sla">
<Label className="text-blue-900 font-semibold">Suggested SLA</Label> <Label className="text-blue-900 font-semibold">Suggested SLA</Label>
<p className="text-blue-700 mt-1">{selectedTemplate.suggestedSLA} days</p> <p className="text-blue-700 mt-1">{selectedTemplate.suggestedSLA} hours</p>
</div> </div>
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-priority"> <div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-priority">
<Label className="text-blue-900 font-semibold">Priority Level</Label> <Label className="text-blue-900 font-semibold">Priority Level</Label>
@ -198,9 +265,10 @@ export function TemplateSelectionStep({
</div> </div>
</div> </div>
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-approvers"> <div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-approvers">
<Label className="text-blue-900 font-semibold">Common Approvers</Label> <Label className="text-blue-900 font-semibold">Approvers</Label>
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
{selectedTemplate.commonApprovers.map((approver, index) => ( {selectedTemplate.commonApprovers?.length > 0 ? (
selectedTemplate.commonApprovers.map((approver, index) => (
<Badge <Badge
key={`${selectedTemplate.id}-approver-${index}-${approver}`} key={`${selectedTemplate.id}-approver-${index}-${approver}`}
variant="outline" variant="outline"
@ -209,7 +277,10 @@ export function TemplateSelectionStep({
> >
{approver} {approver}
</Badge> </Badge>
))} ))
) : (
<span className="text-sm text-gray-500 italic">No specific approvers defined</span>
)}
</div> </div>
</div> </div>
</CardContent> </CardContent>

View File

@ -19,12 +19,15 @@ interface WizardStepperProps {
export function WizardStepper({ currentStep, totalSteps, stepNames }: WizardStepperProps) { export function WizardStepper({ currentStep, totalSteps, stepNames }: WizardStepperProps) {
const progressPercentage = Math.round((currentStep / totalSteps) * 100); const progressPercentage = Math.round((currentStep / totalSteps) * 100);
// Use a narrower container for fewer steps to avoid excessive spacing
const containerMaxWidth = stepNames.length <= 3 ? 'max-w-xl' : 'max-w-6xl';
return ( return (
<div <div
className="bg-white border-b border-gray-200 px-3 sm:px-6 py-2 sm:py-3 flex-shrink-0" className="bg-white border-b border-gray-200 px-3 sm:px-6 py-2 sm:py-3 flex-shrink-0"
data-testid="wizard-stepper" data-testid="wizard-stepper"
> >
<div className="max-w-6xl mx-auto"> <div className={`${containerMaxWidth} mx-auto`}>
{/* Mobile: Current step indicator only */} {/* Mobile: Current step indicator only */}
<div className="block sm:hidden" data-testid="wizard-stepper-mobile"> <div className="block sm:hidden" data-testid="wizard-stepper-mobile">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
@ -65,15 +68,14 @@ export function WizardStepper({ currentStep, totalSteps, stepNames }: WizardStep
{/* Desktop: Full step indicator */} {/* Desktop: Full step indicator */}
<div className="hidden sm:block" data-testid="wizard-stepper-desktop"> <div className="hidden sm:block" data-testid="wizard-stepper-desktop">
<div className="flex items-center justify-between mb-2" data-testid="wizard-stepper-desktop-steps"> <div className="flex items-center justify-center gap-4 mb-2" data-testid="wizard-stepper-desktop-steps">
{stepNames.map((_, index) => ( {stepNames.map((_, index) => (
<div key={index} className="flex items-center" data-testid={`wizard-stepper-desktop-step-${index + 1}`}> <div key={index} className="flex items-center flex-1 last:flex-none" data-testid={`wizard-stepper-desktop-step-${index + 1}`}>
<div <div
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold ${ className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold flex-shrink-0 ${index + 1 < currentStep
index + 1 < currentStep ? 'bg-green-500 text-white'
? 'bg-green-600 text-white'
: index + 1 === currentStep : index + 1 === currentStep
? 'bg-blue-600 text-white' ? 'bg-green-500 text-white ring-2 ring-green-500/30 ring-offset-1'
: 'bg-gray-200 text-gray-600' : 'bg-gray-200 text-gray-600'
}`} }`}
data-testid={`wizard-stepper-desktop-step-${index + 1}-indicator`} data-testid={`wizard-stepper-desktop-step-${index + 1}-indicator`}
@ -86,8 +88,7 @@ export function WizardStepper({ currentStep, totalSteps, stepNames }: WizardStep
</div> </div>
{index < stepNames.length - 1 && ( {index < stepNames.length - 1 && (
<div <div
className={`w-8 md:w-12 lg:w-16 h-1 mx-1 md:mx-2 ${ className={`flex-1 h-0.5 mx-2 ${index + 1 < currentStep ? 'bg-green-500' : 'bg-gray-200'
index + 1 < currentStep ? 'bg-green-600' : 'bg-gray-200'
}`} }`}
data-testid={`wizard-stepper-desktop-step-${index + 1}-connector`} data-testid={`wizard-stepper-desktop-step-${index + 1}-connector`}
/> />
@ -96,14 +97,13 @@ export function WizardStepper({ currentStep, totalSteps, stepNames }: WizardStep
))} ))}
</div> </div>
<div <div
className="hidden lg:flex justify-between text-xs text-gray-600 mt-2" className="hidden lg:flex justify-between text-xs text-gray-600 mt-2 px-1"
data-testid="wizard-stepper-desktop-labels" data-testid="wizard-stepper-desktop-labels"
> >
{stepNames.map((step, index) => ( {stepNames.map((step, index) => (
<span <span
key={index} key={index}
className={`${ className={`${index + 1 === currentStep ? 'font-semibold text-green-600' : ''
index + 1 === currentStep ? 'font-semibold text-blue-600' : ''
}`} }`}
data-testid={`wizard-stepper-desktop-label-${index + 1}`} data-testid={`wizard-stepper-desktop-label-${index + 1}`}
> >

View File

@ -10,6 +10,7 @@ export interface RequestTemplate {
icon: React.ComponentType<any>; icon: React.ComponentType<any>;
estimatedTime: string; estimatedTime: string;
commonApprovers: string[]; commonApprovers: string[];
workflowApprovers?: any[]; // Full approver objects for Admin Templates
suggestedSLA: number; suggestedSLA: number;
priority: 'high' | 'medium' | 'low'; priority: 'high' | 'medium' | 'low';
fields: { fields: {

View File

@ -0,0 +1,287 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { X, Save, ArrowLeft } from 'lucide-react';
import { useUserSearch } from '@/hooks/useUserSearch';
import { createTemplate } from '@/services/workflowTemplateApi';
import { toast } from 'sonner';
export function CreateTemplate() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const { searchResults, searchLoading, searchUsersDebounced, clearSearch } = useUserSearch();
const [approverSearchInput, setApproverSearchInput] = useState('');
const [formData, setFormData] = useState({
name: '',
description: '',
category: 'General',
priority: 'medium' as 'low' | 'medium' | 'high',
estimatedTime: '2 days',
suggestedSLA: 24,
approvers: [] as any[]
});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSelectChange = (name: string, value: string) => {
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleApproverSearch = (val: string) => {
setApproverSearchInput(val);
if (val.length >= 2) {
searchUsersDebounced(val, 5);
} else {
clearSearch();
}
};
const addApprover = (user: any) => {
if (formData.approvers.some(a => a.userId === user.userId)) {
toast.error('Approver already added');
return;
}
setFormData(prev => ({
...prev,
approvers: [...prev.approvers, {
userId: user.userId,
name: user.displayName || user.email,
email: user.email,
level: prev.approvers.length + 1,
tat: 24 // Default TAT in hours
}]
}));
setApproverSearchInput('');
clearSearch();
};
const removeApprover = (index: number) => {
const newApprovers = [...formData.approvers];
newApprovers.splice(index, 1);
// Re-index levels
newApprovers.forEach((a, i) => a.level = i + 1);
setFormData(prev => ({ ...prev, approvers: newApprovers }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.description) {
toast.error('Please fill in required fields');
return;
}
try {
setLoading(true);
await createTemplate(formData);
toast.success('Template created successfully');
navigate('/dashboard'); // Or back to list
} catch (error) {
toast.error('Failed to create template');
console.error(error);
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto p-6 space-y-6">
<div className="flex items-center gap-4 mb-6">
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ArrowLeft className="w-5 h-5" />
</Button>
<div>
<h1 className="text-3xl font-bold text-gray-900">Create Workflow Template</h1>
<p className="text-gray-500">Define a new standardized request workflow</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Basic Information</CardTitle>
<CardDescription>General details about the template</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Template Name *</Label>
<Input
id="name"
name="name"
placeholder="e.g., Office Stationery Request"
value={formData.name}
onChange={handleInputChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<div className="relative">
{/* Simple text input for now, could be select */}
<Input
id="category"
name="category"
placeholder="e.g., Admin, HR, Finance"
value={formData.category}
onChange={handleInputChange}
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description *</Label>
<Textarea
id="description"
name="description"
placeholder="Describe what this request is for..."
value={formData.description}
onChange={handleInputChange}
required
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="priority">Default Priority</Label>
<Select
name="priority"
value={formData.priority}
onValueChange={(val) => handleSelectChange('priority', val)}
>
<SelectTrigger>
<SelectValue placeholder="Select priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="estimatedTime">Estimated Time</Label>
<Input
id="estimatedTime"
name="estimatedTime"
placeholder="e.g., 2 days"
value={formData.estimatedTime}
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="suggestedSLA">SLA (Hours)</Label>
<Input
id="suggestedSLA"
name="suggestedSLA"
type="number"
placeholder="24"
value={formData.suggestedSLA}
onChange={handleInputChange}
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Approver Workflow</CardTitle>
<CardDescription>Define static approvers for this template</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
{formData.approvers.map((approver, index) => (
<div key={index} className="flex items-center gap-4 p-3 bg-gray-50 rounded-lg border border-gray-100">
<Badge variant="outline" className="bg-white">Level {approver.level}</Badge>
<div className="flex-1">
<div className="font-medium text-gray-900">{approver.name}</div>
<div className="text-sm text-gray-500">{approver.email}</div>
</div>
<div className="w-32 flex items-center gap-2">
<Label htmlFor={`tat-${index}`} className="text-xs whitespace-nowrap">TAT (Hrs)</Label>
<Input
id={`tat-${index}`}
type="number"
className="h-8 w-16"
value={approver.tat || 24}
onChange={(e) => {
const newApprovers = [...formData.approvers];
newApprovers[index].tat = parseInt(e.target.value) || 0;
setFormData(prev => ({ ...prev, approvers: newApprovers }));
}}
/>
</div>
<Button type="button" variant="ghost" size="sm" onClick={() => removeApprover(index)}>
<X className="w-4 h-4 text-gray-500 hover:text-red-600" />
</Button>
</div>
))}
{formData.approvers.length === 0 && (
<div className="text-center p-8 border-2 border-dashed rounded-lg text-gray-500 text-sm">
No approvers defined. Requests will be auto-approved or require manual assignment.
</div>
)}
</div>
<div className="space-y-2 relative">
<Label>Add Approver</Label>
<div className="flex gap-2">
<Input
placeholder="Search user by name or email..."
value={approverSearchInput}
onChange={(e) => handleApproverSearch(e.target.value)}
/>
</div>
{(searchLoading || searchResults.length > 0) && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white border rounded-lg shadow-lg z-10 max-h-60 overflow-y-auto">
{searchLoading && <div className="p-2 text-sm text-gray-500">Searching...</div>}
{searchResults.map(user => (
<div
key={user.userId}
className="p-2 hover:bg-gray-50 cursor-pointer flex items-center gap-3"
onClick={() => addApprover(user)}
>
<Avatar className="h-8 w-8">
<AvatarFallback>{(user.displayName || 'U').substring(0, 2)}</AvatarFallback>
</Avatar>
<div>
<div className="text-sm font-medium">{user.displayName}</div>
<div className="text-xs text-gray-500">{user.email}</div>
</div>
</div>
))}
</div>
)}
</div>
</CardContent>
</Card>
<div className="flex justify-end gap-3 pt-4">
<Button type="button" variant="outline" onClick={() => navigate(-1)}>Cancel</Button>
<Button type="submit" disabled={loading} className="bg-re-green hover:bg-re-green/90">
{loading ? 'Creating...' : (
<>
<Save className="w-4 h-4 mr-2" />
Create Template
</>
)}
</Button>
</div>
</form>
</div>
);
}

View File

@ -0,0 +1,193 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Save, ChevronRight, Check } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useAuth } from '@/contexts/AuthContext';
import { getTemplates, WorkflowTemplate as BackendTemplate } from '@/services/workflowTemplateApi';
import { RequestTemplate } from '@/hooks/useCreateRequestForm';
import { FileText } from 'lucide-react';
import { AdminRequestDetailsStep } from './components/AdminRequestDetailsStep';
import { AdminRequestReviewStep } from './components/AdminRequestReviewStep';
import { toast } from 'sonner';
import { WizardStepper } from '@/components/workflow/CreateRequest/WizardStepper';
export function CreateAdminRequest() {
const { templateId } = useParams<{ templateId: string }>();
const navigate = useNavigate();
const { user } = useAuth();
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [template, setTemplate] = useState<RequestTemplate | null>(null);
const [step, setStep] = useState(1);
const [documents, setDocuments] = useState<File[]>([]);
// Simplified form data
const [formData, setFormData] = useState({
title: '',
description: '',
});
const stepNames = ['Request Details', 'Review & Submit'];
useEffect(() => {
const loadTemplate = async () => {
try {
setLoading(true);
// Ideally we would have a getTemplateById API, but for now we filter from list
// Optimization: In a real app, create a specific endpoint
const templates = await getTemplates();
const found = templates.find((t: BackendTemplate) => t.id === templateId);
if (found) {
const mapped: RequestTemplate = {
id: found.id,
name: found.name,
description: found.description,
category: found.category,
icon: FileText,
estimatedTime: found.estimatedTime,
commonApprovers: found.approvers.map((a: any) => a.name),
workflowApprovers: found.approvers,
suggestedSLA: found.suggestedSLA,
priority: found.priority,
fields: found.fields || {}
};
setTemplate(mapped);
// Pre-fill
setFormData({
title: mapped.name,
description: mapped.description
});
} else {
toast.error('Template not found');
navigate('/new-request');
}
} catch (error) {
console.error('Error loading template:', error);
toast.error('Failed to load template details');
} finally {
setLoading(false);
}
};
if (templateId) {
loadTemplate();
}
}, [templateId, navigate]);
const handleSubmit = async () => {
if (!template) return;
try {
setSubmitting(true);
// Construct the request payload
// This matches the structure expected by the backend for a generic request
// But we will likely need to adjust based on how "createRequest" is implemented globally
// For now, we simulate the submission or call the common handler if available
// Simulating API call for demonstration of flow
await new Promise(resolve => setTimeout(resolve, 1500));
toast.success('Request Submitted Successfully', {
description: `Your request "${formData.title}" has been created.`
});
navigate('/my-requests');
} catch (error) {
console.error('Submission failed:', error);
toast.error('Failed to submit request');
} finally {
setSubmitting(false);
}
};
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
if (!template) return null;
return (
<div className="h-screen flex flex-col bg-gray-50 overflow-hidden">
{/* Header */}
<header className="bg-white border-b flex-shrink-0 z-10">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => navigate('/new-request')}>
<ArrowLeft className="w-5 h-5 text-gray-500" />
</Button>
<div>
<h1 className="text-xl font-bold text-gray-900">New Request</h1>
<p className="text-sm text-gray-500">{template.name}</p>
</div>
</div>
<Button variant="outline" onClick={() => navigate('/dashboard')}>
Cancel Request
</Button>
</div>
{/* Stepper */}
<WizardStepper
currentStep={step}
totalSteps={2}
stepNames={stepNames}
/>
</header>
{/* Content */}
<main className="flex-1 overflow-y-auto py-8 px-6 bg-gray-50/50">
<div className="max-w-4xl mx-auto">
{step === 1 ? (
<AdminRequestDetailsStep
template={template}
formData={formData}
setFormData={setFormData}
documents={documents}
setDocuments={setDocuments}
/>
) : (
<AdminRequestReviewStep
template={template}
formData={formData}
documents={documents}
/>
)}
</div>
</main>
{/* Footer Actions */}
<footer className="bg-white border-t px-6 py-4 flex-shrink-0 z-10">
<div className="max-w-4xl mx-auto flex items-center justify-between">
<Button
variant="outline"
onClick={() => step === 1 ? navigate('/new-request') : setStep(1)}
disabled={submitting}
>
{step === 1 ? 'Cancel' : 'Back to Details'}
</Button>
<Button
onClick={() => step === 1 ? setStep(2) : handleSubmit()}
disabled={submitting}
className={step === 2 ? "bg-re-green hover:bg-re-green/90" : "bg-re-green hover:bg-re-green/90"}
>
{step === 1 ? (
<>Review Request <ChevronRight className="w-4 h-4 ml-1" /></>
) : (
<>{submitting ? 'Submitting...' : 'Submit Request'} <Check className="w-4 h-4 ml-1" /></>
)}
</Button>
</div>
</footer>
</div>
);
}

View File

@ -0,0 +1,171 @@
import { useRef, useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Upload, X, FileText, Eye } from 'lucide-react';
import { RequestTemplate } from '@/hooks/useCreateRequestForm';
import { RichTextEditor } from '@/components/ui/rich-text-editor';
import { FilePreview } from '@/components/common/FilePreview/FilePreview';
interface AdminRequestDetailsStepProps {
template: RequestTemplate;
formData: any;
setFormData: (data: any) => void;
documents: File[];
setDocuments: (docs: File[]) => void;
}
export function AdminRequestDetailsStep({
template,
formData,
setFormData,
documents,
setDocuments
}: AdminRequestDetailsStepProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [previewFile, setPreviewFile] = useState<{ file: File; url: string } | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
setDocuments([...documents, ...Array.from(e.target.files)]);
}
};
const removeDocument = (index: number) => {
const newDocs = [...documents];
newDocs.splice(index, 1);
setDocuments(newDocs);
};
const handlePreview = (file: File) => {
const url = URL.createObjectURL(file);
setPreviewFile({ file, url });
};
const closePreview = () => {
if (previewFile?.url) {
URL.revokeObjectURL(previewFile.url);
}
setPreviewFile(null);
};
const canPreview = (file: File) => {
return file.type.includes('image') || file.type.includes('pdf');
};
return (
<div className="space-y-6 max-w-4xl mx-auto">
<Card className="shadow-sm">
<CardContent className="pt-6">
<div className="mb-4">
<h2 className="text-xl font-bold text-gray-800">{template.name}</h2>
<p className="text-sm text-gray-500 mt-1">{template.description}</p>
</div>
<div className="flex gap-4 text-sm text-gray-600 bg-gray-50 p-3 rounded-lg">
<div className="flex items-center gap-1">
<span className="font-semibold">Category:</span> {template.category}
</div>
<div className="flex items-center gap-1">
<span className="font-semibold">Priority:</span>
<span className="capitalize">{template.priority}</span>
</div>
<div className="flex items-center gap-1">
<span className="font-semibold">SLA:</span> {template.suggestedSLA} Hours
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 space-y-4">
<div className="space-y-2">
<Label htmlFor="requestTitle">Request Title *</Label>
<Input
id="requestTitle"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder={`Request for ${template.name}`}
className="border-gray-200"
/>
</div>
<div className="space-y-2">
<Label htmlFor="justification" className="text-base font-semibold">Request Detail *</Label>
<p className="text-sm text-gray-600 mb-2">
Explain what you need approval for, why it's needed, and any relevant details.
</p>
<RichTextEditor
value={formData.description || ''}
onChange={(html) => setFormData({ ...formData, description: html })}
placeholder="Provide comprehensive details about your request..."
className="min-h-[120px] text-base border-gray-200 bg-white shadow-sm"
minHeight="120px"
/>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 space-y-4">
<Label>Supporting Documents</Label>
<div
className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:bg-gray-50 transition-colors cursor-pointer"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<p className="text-sm font-medium text-gray-700">Click to upload files</p>
<p className="text-xs text-gray-500 mt-1">PDF, Excel, Images (Max 10MB)</p>
<input
ref={fileInputRef}
type="file"
multiple
className="hidden"
onChange={handleFileChange}
/>
</div>
{documents.length > 0 && (
<div className="grid grid-cols-1 gap-2 mt-4">
{documents.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-white border rounded-lg shadow-sm">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
<FileText className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-800 truncate max-w-[200px]">{file.name}</p>
<p className="text-xs text-gray-500">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
</div>
<div className="flex items-center gap-1">
{canPreview(file) && (
<Button variant="ghost" size="icon" onClick={() => handlePreview(file)}>
<Eye className="w-4 h-4 text-gray-500 hover:text-blue-600" />
</Button>
)}
<Button variant="ghost" size="icon" onClick={() => removeDocument(index)}>
<X className="w-4 h-4 text-gray-500 hover:text-red-500" />
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{previewFile && (
<FilePreview
fileName={previewFile.file.name}
fileType={previewFile.file.type}
fileUrl={previewFile.url}
fileSize={previewFile.file.size}
open={!!previewFile}
onClose={closePreview}
/>
)}
</div>
);
}

View File

@ -0,0 +1,135 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { FileText, CheckCircle2, AlertCircle } from 'lucide-react';
import { RequestTemplate } from '@/hooks/useCreateRequestForm';
interface AdminRequestReviewStepProps {
template: RequestTemplate;
formData: any;
documents: File[];
}
export function AdminRequestReviewStep({
template,
formData,
documents
}: AdminRequestReviewStepProps) {
// Use template approvers if available, otherwise fallback (though should always be there for admin templates)
const approvers = template.workflowApprovers || [];
return (
<div className="space-y-6 max-w-4xl mx-auto">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<h4 className="font-semibold text-blue-900">Ready to Submit?</h4>
<p className="text-sm text-blue-700 mt-1">
Please review the details below. This request will follow the standardized approval workflow defined by the administrator.
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-2 space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Request Overview</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Title</span>
<p className="text-base font-medium text-gray-900 mt-1">{formData.title}</p>
</div>
<div>
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Request Detail</span>
<div
className="text-sm text-gray-700 mt-1 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: formData.description }}
/>
</div>
{documents.length > 0 && (
<div>
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wider block mb-2">Attachments ({documents.length})</span>
<div className="flex flex-wrap gap-2">
{documents.map((doc, i) => (
<Badge key={i} variant="secondary" className="pl-1 pr-2 py-1 flex items-center gap-1.5 h-auto">
<FileText className="w-3 h-3 text-gray-500" />
<span className="truncate max-w-[150px]">{doc.name}</span>
</Badge>
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Approval Workflow</CardTitle>
</CardHeader>
<CardContent>
<div className="relative pl-6 border-l-2 border-gray-100 space-y-8 py-2">
{approvers.map((approver: any, index: number) => (
<div key={index} className="relative">
{/* Timeline dot */}
<div className="absolute -left-[31px] top-1 w-4 h-4 rounded-full bg-white border-2 border-blue-500 flex items-center justify-center">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
</div>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
<div className="flex justify-between items-start mb-1">
<div>
<h5 className="font-semibold text-gray-800 text-sm">{approver.name || approver.email}</h5>
<p className="text-xs text-gray-500">Level {approver.level} Approver</p>
</div>
<Badge variant="outline" className="bg-white text-xs">
{approver.tat || 24} Hours TAT
</Badge>
</div>
<p className="text-xs text-gray-400">{approver.email}</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm uppercase text-gray-500">Properties</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Template</span>
<span className="text-sm font-medium text-right">{template.name}</span>
</div>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Priority</span>
<Badge className={
template.priority === 'high' ? 'bg-red-100 text-red-700 hover:bg-red-100' :
template.priority === 'medium' ? 'bg-orange-100 text-orange-700 hover:bg-orange-100' :
'bg-green-100 text-green-700 hover:bg-green-100'
}>
{template.priority.toUpperCase()}
</Badge>
</div>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Est. Time</span>
<span className="text-sm text-gray-900">{template.estimatedTime}</span>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@ -22,7 +22,7 @@ import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
import { downloadDocument } from '@/services/workflowApi'; import { downloadDocument } from '@/services/workflowApi';
// Custom Hooks // Custom Hooks
import { useCreateRequestForm } from '@/hooks/useCreateRequestForm'; import { useCreateRequestForm, RequestTemplate } from '@/hooks/useCreateRequestForm';
import { useWizardNavigation } from '@/hooks/useWizardNavigation'; import { useWizardNavigation } from '@/hooks/useWizardNavigation';
import { useRequestModals } from './hooks/useRequestModals'; import { useRequestModals } from './hooks/useRequestModals';
import { useCreateRequestSubmission } from './hooks/useCreateRequestSubmission'; import { useCreateRequestSubmission } from './hooks/useCreateRequestSubmission';
@ -31,6 +31,10 @@ import { useCreateRequestHandlers } from './hooks/useCreateRequestHandlers';
// Constants // Constants
import { REQUEST_TEMPLATES } from './constants/requestTemplates'; import { REQUEST_TEMPLATES } from './constants/requestTemplates';
// Services
import { getTemplates, WorkflowTemplate as BackendTemplate } from '@/services/workflowTemplateApi';
import { FileText } from 'lucide-react';
// Components // Components
import { WizardStepper } from '@/components/workflow/CreateRequest/WizardStepper'; import { WizardStepper } from '@/components/workflow/CreateRequest/WizardStepper';
import { WizardFooter } from '@/components/workflow/CreateRequest/WizardFooter'; import { WizardFooter } from '@/components/workflow/CreateRequest/WizardFooter';
@ -64,6 +68,35 @@ export function CreateRequest({
const isEditing = isEditMode && !!editRequestId; const isEditing = isEditMode && !!editRequestId;
const { user } = useAuth(); const { user } = useAuth();
const [adminTemplates, setAdminTemplates] = useState<RequestTemplate[]>([]);
useEffect(() => {
const fetchTemplates = async () => {
try {
const templates = await getTemplates();
const mappedTemplates: RequestTemplate[] = templates.map((t: BackendTemplate) => ({
id: t.id,
name: t.name,
description: t.description,
category: t.category,
icon: FileText,
estimatedTime: t.estimatedTime,
commonApprovers: t.approvers.map((a: any) => a.name),
workflowApprovers: t.approvers,
suggestedSLA: t.suggestedSLA,
priority: t.priority,
fields: t.fields || {}
}));
setAdminTemplates(mappedTemplates);
} catch (error) {
console.error('Failed to fetch admin templates:', error);
}
};
fetchTemplates();
}, []);
const allTemplates = [...REQUEST_TEMPLATES, ...adminTemplates];
// Form and state management hooks // Form and state management hooks
const { const {
formData, formData,
@ -75,7 +108,7 @@ export function CreateRequest({
documentPolicy, documentPolicy,
existingDocuments, existingDocuments,
setExistingDocuments, setExistingDocuments,
} = useCreateRequestForm(isEditing, editRequestId, REQUEST_TEMPLATES); } = useCreateRequestForm(isEditing, editRequestId, allTemplates);
const { const {
currentStep, currentStep,
@ -84,6 +117,7 @@ export function CreateRequest({
isStepValid, isStepValid,
nextStep: wizardNextStep, nextStep: wizardNextStep,
prevStep: wizardPrevStep, prevStep: wizardPrevStep,
goToStep,
} = useWizardNavigation(isEditing, selectedTemplate, formData); } = useWizardNavigation(isEditing, selectedTemplate, formData);
// Document management state // Document management state
@ -139,6 +173,7 @@ export function CreateRequest({
user: user!, user: user!,
openValidationModal, openValidationModal,
onSubmit, onSubmit,
goToStep,
}); });
// Handle back button: // Handle back button:
@ -207,6 +242,7 @@ export function CreateRequest({
templates={REQUEST_TEMPLATES} templates={REQUEST_TEMPLATES}
selectedTemplate={selectedTemplate} selectedTemplate={selectedTemplate}
onSelectTemplate={selectTemplate} onSelectTemplate={selectTemplate}
adminTemplates={adminTemplates}
/> />
); );
case 2: case 2:

View File

@ -29,6 +29,7 @@ interface UseHandlersOptions {
message: string message: string
) => void; ) => void;
onSubmit?: (requestData: any) => void; onSubmit?: (requestData: any) => void;
goToStep?: (step: number) => void;
} }
export function useCreateRequestHandlers({ export function useCreateRequestHandlers({
@ -43,6 +44,7 @@ export function useCreateRequestHandlers({
user, user,
openValidationModal, openValidationModal,
onSubmit, onSubmit,
goToStep,
}: UseHandlersOptions) { }: UseHandlersOptions) {
const [showTemplateModal, setShowTemplateModal] = useState(false); const [showTemplateModal, setShowTemplateModal] = useState(false);
const [previewDocument, setPreviewDocument] = const [previewDocument, setPreviewDocument] =
@ -61,6 +63,33 @@ export function useCreateRequestHandlers({
if (template.id === 'existing-template') { if (template.id === 'existing-template') {
setShowTemplateModal(true); setShowTemplateModal(true);
} else if (template.id !== 'custom' && goToStep) {
// Logic for Admin Templates (pre-defined templates)
// Pre-fill fields
updateFormData('title', template.name);
updateFormData('description', template.description);
// Map approvers if they exist
if (template.workflowApprovers && template.workflowApprovers.length > 0) {
const mappedApprovers = template.workflowApprovers.map((appr: any, index: number) => ({
id: `admin-appr-${index}`,
name: appr.name || appr.email,
email: appr.email,
role: `Level ${appr.level}`,
level: appr.level,
tat: appr.tat || 24,
tatType: 'hours',
userId: appr.userId
}));
updateFormData('approvers', mappedApprovers);
updateFormData('approverCount', mappedApprovers.length);
updateFormData('maxLevel', mappedApprovers.length);
}
// Skip to Documents Step (Step 5)
// Steps: 1=Template, 2=Basic, 3=Approval, 4=Participants, 5=Documents, 6=Review
setTimeout(() => goToStep(5), 100);
} }
}; };

View File

@ -0,0 +1,24 @@
import apiClient from './authApi';
export interface WorkflowTemplate {
id: string;
name: string;
description: string;
category: string;
priority: 'low' | 'medium' | 'high';
estimatedTime: string;
approvers: any[];
suggestedSLA: number;
isActive: boolean;
fields?: any;
}
export const createTemplate = async (templateData: Partial<WorkflowTemplate>): Promise<WorkflowTemplate> => {
const response = await apiClient.post('/templates', templateData);
return response.data.data;
};
export const getTemplates = async (): Promise<WorkflowTemplate[]> => {
const response = await apiClient.get('/templates');
return response.data.data;
};