admin template flow addd with edit and delete option
This commit is contained in:
parent
ec8987032f
commit
1b4091c3d3
16
src/App.tsx
16
src/App.tsx
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, useNavigate } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, useNavigate, Outlet } from 'react-router-dom';
|
||||||
import { PageLayout } from '@/components/layout/PageLayout';
|
import { PageLayout } from '@/components/layout/PageLayout';
|
||||||
import { Dashboard } from '@/pages/Dashboard';
|
import { Dashboard } from '@/pages/Dashboard';
|
||||||
import { OpenRequests } from '@/pages/OpenRequests';
|
import { OpenRequests } from '@/pages/OpenRequests';
|
||||||
@ -20,6 +20,7 @@ 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 { AdminTemplatesList } from '@/pages/Admin/Templates/AdminTemplatesList';
|
||||||
import { CreateTemplate } from '@/pages/Admin/Templates/CreateTemplate';
|
import { CreateTemplate } from '@/pages/Admin/Templates/CreateTemplate';
|
||||||
import { CreateAdminRequest } from '@/pages/CreateAdminRequest/CreateAdminRequest';
|
import { CreateAdminRequest } from '@/pages/CreateAdminRequest/CreateAdminRequest';
|
||||||
import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
|
import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal';
|
||||||
@ -485,15 +486,18 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Admin Routes - Placed higher to prevent matching issues */}
|
{/* Admin Routes Group with Shared Layout */}
|
||||||
<Route
|
<Route
|
||||||
path="/admin/create-template"
|
|
||||||
element={
|
element={
|
||||||
<PageLayout currentPage="admin" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
|
<PageLayout currentPage="admin-templates" onNavigate={handleNavigate} onNewRequest={handleNewRequest} onLogout={onLogout}>
|
||||||
<CreateTemplate />
|
<Outlet />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<Route path="/admin/create-template" element={<CreateTemplate />} />
|
||||||
|
<Route path="/admin/edit-template/:templateId" element={<CreateTemplate />} />
|
||||||
|
<Route path="/admin/templates" element={<AdminTemplatesList />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* Create Request from Admin Template (Dedicated Flow) */}
|
{/* Create Request from Admin Template (Dedicated Flow) */}
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@ -61,7 +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
|
{ id: 'admin/templates', label: 'Admin Templates', icon: Plus, adminOnly: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add remaining menu items
|
// Add remaining menu items
|
||||||
@ -238,8 +238,8 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
|
|||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item.id === 'admin/create-template') {
|
if (item.id === 'admin/templates') {
|
||||||
onNavigate?.('admin/create-template');
|
onNavigate?.('admin/templates');
|
||||||
} else {
|
} else {
|
||||||
onNavigate?.(item.id);
|
onNavigate?.(item.id);
|
||||||
}
|
}
|
||||||
|
|||||||
228
src/pages/Admin/Templates/AdminTemplatesList.tsx
Normal file
228
src/pages/Admin/Templates/AdminTemplatesList.tsx
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Plus, Pencil, Trash2, Search, FileText, AlertTriangle } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { getTemplates, deleteTemplate, WorkflowTemplate, getCachedTemplates } from '@/services/workflowTemplateApi';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export function AdminTemplatesList() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [templates, setTemplates] = useState<WorkflowTemplate[]>(() => getCachedTemplates() || []);
|
||||||
|
// Only show full loading skeleton if we don't have any data yet
|
||||||
|
const [loading, setLoading] = useState(() => !getCachedTemplates());
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
|
const fetchTemplates = async () => {
|
||||||
|
try {
|
||||||
|
// If we didn't have cache, we are already loading.
|
||||||
|
// If we HAD cache, we don't want to set loading=true (flashing skeletons),
|
||||||
|
// we just want to update the data in background.
|
||||||
|
if (templates.length === 0) setLoading(true);
|
||||||
|
|
||||||
|
const data = await getTemplates();
|
||||||
|
setTemplates(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch templates:', error);
|
||||||
|
toast.error('Failed to load templates');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTemplates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!deleteId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setDeleting(true);
|
||||||
|
await deleteTemplate(deleteId);
|
||||||
|
toast.success('Template deleted successfully');
|
||||||
|
setTemplates(prev => prev.filter(t => t.id !== deleteId));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete template:', error);
|
||||||
|
toast.error('Failed to delete template');
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
setDeleteId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredTemplates = templates.filter(template =>
|
||||||
|
template.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
template.category.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const getPriorityColor = (priority: string) => {
|
||||||
|
switch (priority.toLowerCase()) {
|
||||||
|
case 'high': return 'bg-red-100 text-red-700 border-red-200';
|
||||||
|
case 'medium': return 'bg-orange-100 text-orange-700 border-orange-200';
|
||||||
|
case 'low': return 'bg-green-100 text-green-700 border-green-200';
|
||||||
|
default: return 'bg-gray-100 text-gray-700 border-gray-200';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Admin Templates</h1>
|
||||||
|
<p className="text-gray-500">Manage workflow templates for your organization</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate('/admin/create-template')}
|
||||||
|
className="bg-re-green hover:bg-re-green/90"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Create New Template
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search templates..."
|
||||||
|
className="pl-10 border-gray-200"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{[1, 2, 3].map(i => (
|
||||||
|
<Card key={i} className="h-48">
|
||||||
|
<CardHeader>
|
||||||
|
<Skeleton className="h-6 w-3/4 mb-2" />
|
||||||
|
<Skeleton className="h-4 w-1/2" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Skeleton className="h-4 w-full mb-2" />
|
||||||
|
<Skeleton className="h-4 w-2/3" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : filteredTemplates.length === 0 ? (
|
||||||
|
<div className="text-center py-16 bg-white rounded-lg border border-dashed border-gray-300">
|
||||||
|
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<FileText className="w-8 h-8 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No templates found</h3>
|
||||||
|
<p className="text-gray-500 max-w-sm mx-auto mb-6">
|
||||||
|
{searchQuery ? 'Try adjusting your search terms' : 'Get started by creating your first workflow template'}
|
||||||
|
</p>
|
||||||
|
{!searchQuery && (
|
||||||
|
<Button onClick={() => navigate('/admin/create-template')} variant="outline">
|
||||||
|
Create Template
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{filteredTemplates.map((template) => (
|
||||||
|
<Card key={template.id} className="hover:shadow-md transition-shadow duration-200 group">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex justify-between items-start gap-2">
|
||||||
|
<div className="p-2 bg-blue-50 rounded-lg text-blue-600 mb-2 w-fit">
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className={getPriorityColor(template.priority)}>
|
||||||
|
{template.priority}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<CardTitle className="line-clamp-1 text-lg">{template.name}</CardTitle>
|
||||||
|
<CardDescription className="line-clamp-2 h-10">
|
||||||
|
{template.description}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-sm text-gray-500 mb-4 space-y-1">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Category:</span>
|
||||||
|
<span className="font-medium text-gray-900">{template.category}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>SLA:</span>
|
||||||
|
<span className="font-medium text-gray-900">{template.suggestedSLA} hours</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Approvers:</span>
|
||||||
|
<span className="font-medium text-gray-900">{template.approvers?.length || 0} levels</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2 border-t mt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1 text-blue-600 hover:text-blue-700 hover:bg-blue-50 border-blue-100"
|
||||||
|
onClick={() => navigate(`/admin/edit-template/${template.id}`)}
|
||||||
|
>
|
||||||
|
<Pencil className="w-4 h-4 mr-2" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1 text-red-600 hover:text-red-700 hover:bg-red-50 border-red-100"
|
||||||
|
onClick={() => setDeleteId(template.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||||
|
Delete Template
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete this template? This action cannot be undone.
|
||||||
|
Active requests using this template will not be affected.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={deleting}>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleDelete();
|
||||||
|
}}
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{deleting ? 'Deleting...' : 'Delete'}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@ -8,14 +8,18 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
import { X, Save, ArrowLeft } from 'lucide-react';
|
import { X, Save, ArrowLeft, Loader2, Clock } from 'lucide-react';
|
||||||
import { useUserSearch } from '@/hooks/useUserSearch';
|
import { useUserSearch } from '@/hooks/useUserSearch';
|
||||||
import { createTemplate } from '@/services/workflowTemplateApi';
|
import { createTemplate, updateTemplate, getTemplates, WorkflowTemplate } from '@/services/workflowTemplateApi';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export function CreateTemplate() {
|
export function CreateTemplate() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { templateId } = useParams<{ templateId: string }>();
|
||||||
|
const isEditing = !!templateId;
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [fetching, setFetching] = useState(false);
|
||||||
const { searchResults, searchLoading, searchUsersDebounced, clearSearch } = useUserSearch();
|
const { searchResults, searchLoading, searchUsersDebounced, clearSearch } = useUserSearch();
|
||||||
const [approverSearchInput, setApproverSearchInput] = useState('');
|
const [approverSearchInput, setApproverSearchInput] = useState('');
|
||||||
|
|
||||||
@ -29,6 +33,39 @@ export function CreateTemplate() {
|
|||||||
approvers: [] as any[]
|
approvers: [] as any[]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing && templateId) {
|
||||||
|
const fetchTemplate = async () => {
|
||||||
|
try {
|
||||||
|
setFetching(true);
|
||||||
|
const templates = await getTemplates();
|
||||||
|
const template = templates.find((t: WorkflowTemplate) => t.id === templateId);
|
||||||
|
|
||||||
|
if (template) {
|
||||||
|
setFormData({
|
||||||
|
name: template.name,
|
||||||
|
description: template.description,
|
||||||
|
category: template.category,
|
||||||
|
priority: template.priority,
|
||||||
|
estimatedTime: template.estimatedTime,
|
||||||
|
suggestedSLA: template.suggestedSLA,
|
||||||
|
approvers: template.approvers || []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error('Template not found');
|
||||||
|
navigate('/admin/templates');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load template:', error);
|
||||||
|
toast.error('Failed to load template details');
|
||||||
|
} finally {
|
||||||
|
setFetching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchTemplate();
|
||||||
|
}
|
||||||
|
}, [isEditing, templateId, navigate]);
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
@ -40,13 +77,34 @@ export function CreateTemplate() {
|
|||||||
|
|
||||||
const handleApproverSearch = (val: string) => {
|
const handleApproverSearch = (val: string) => {
|
||||||
setApproverSearchInput(val);
|
setApproverSearchInput(val);
|
||||||
if (val.length >= 2) {
|
// Only trigger search if specifically starting with '@'
|
||||||
|
// This prevents triggering on email addresses like "user@example.com"
|
||||||
|
if (val.startsWith('@')) {
|
||||||
|
const query = val.slice(1);
|
||||||
|
// Search if we have at least 1 character after @
|
||||||
|
// This allows searching for "L" in "@L"
|
||||||
|
if (query.length >= 1) {
|
||||||
|
// Pass the full query starting with @, as useUserSearch expects it
|
||||||
searchUsersDebounced(val, 5);
|
searchUsersDebounced(val, 5);
|
||||||
} else {
|
return;
|
||||||
clearSearch();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// If no @ at start or query too short, clear results
|
||||||
|
clearSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ... (rest of the component)
|
||||||
|
|
||||||
|
// In the return JSX:
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Type '@' to search user by name or email..."
|
||||||
|
value={approverSearchInput}
|
||||||
|
onChange={(e) => handleApproverSearch(e.target.value)}
|
||||||
|
className="border-gray-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
const addApprover = (user: any) => {
|
const addApprover = (user: any) => {
|
||||||
if (formData.approvers.some(a => a.userId === user.userId)) {
|
if (formData.approvers.some(a => a.userId === user.userId)) {
|
||||||
toast.error('Approver already added');
|
toast.error('Approver already added');
|
||||||
@ -59,7 +117,8 @@ export function CreateTemplate() {
|
|||||||
name: user.displayName || user.email,
|
name: user.displayName || user.email,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
level: prev.approvers.length + 1,
|
level: prev.approvers.length + 1,
|
||||||
tat: 24 // Default TAT in hours
|
tat: 24, // Default TAT in hours
|
||||||
|
tatType: 'hours' // Default unit
|
||||||
}]
|
}]
|
||||||
}));
|
}));
|
||||||
setApproverSearchInput('');
|
setApproverSearchInput('');
|
||||||
@ -81,28 +140,68 @@ export function CreateTemplate() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (formData.approvers.length === 0) {
|
||||||
|
toast.error('Please add at least one approver');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare payload with TAT conversion
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
approvers: formData.approvers.map(a => ({
|
||||||
|
...a,
|
||||||
|
tat: a.tatType === 'days' ? (parseInt(a.tat) * 24) : parseInt(a.tat)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await createTemplate(formData);
|
if (isEditing && templateId) {
|
||||||
|
await updateTemplate(templateId, payload);
|
||||||
|
toast.success('Template updated successfully');
|
||||||
|
} else {
|
||||||
|
await createTemplate(payload);
|
||||||
toast.success('Template created successfully');
|
toast.success('Template created successfully');
|
||||||
navigate('/dashboard'); // Or back to list
|
}
|
||||||
|
navigate('/admin/templates'); // Back to list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to create template');
|
toast.error(isEditing ? 'Failed to update template' : 'Failed to create template');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (fetching) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-96 items-center justify-center">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormValid = formData.name &&
|
||||||
|
formData.description &&
|
||||||
|
formData.approvers.length > 0 &&
|
||||||
|
formData.approvers.every((a: any) => {
|
||||||
|
const val = parseInt(String(a.tat)) || 0;
|
||||||
|
const max = a.tatType === 'days' ? 7 : 24;
|
||||||
|
return val >= 1 && val <= max;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<div className="flex items-center gap-4 mb-6">
|
||||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
<Button variant="ghost" size="icon" onClick={() => navigate('/admin/templates')}>
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Create Workflow Template</h1>
|
<h1 className="text-3xl font-bold text-gray-900">
|
||||||
<p className="text-gray-500">Define a new standardized request workflow</p>
|
{isEditing ? 'Edit Workflow Template' : 'Create Workflow Template'}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
{isEditing ? 'Update existing workflow configuration' : 'Define a new standardized request workflow'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -122,6 +221,7 @@ export function CreateTemplate() {
|
|||||||
placeholder="e.g., Office Stationery Request"
|
placeholder="e.g., Office Stationery Request"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
className="border-gray-200"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -135,6 +235,7 @@ export function CreateTemplate() {
|
|||||||
placeholder="e.g., Admin, HR, Finance"
|
placeholder="e.g., Admin, HR, Finance"
|
||||||
value={formData.category}
|
value={formData.category}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
className="border-gray-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -148,6 +249,7 @@ export function CreateTemplate() {
|
|||||||
placeholder="Describe what this request is for..."
|
placeholder="Describe what this request is for..."
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
className="border-gray-200"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -178,6 +280,7 @@ export function CreateTemplate() {
|
|||||||
placeholder="e.g., 2 days"
|
placeholder="e.g., 2 days"
|
||||||
value={formData.estimatedTime}
|
value={formData.estimatedTime}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
className="border-gray-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -189,6 +292,7 @@ export function CreateTemplate() {
|
|||||||
placeholder="24"
|
placeholder="24"
|
||||||
value={formData.suggestedSLA}
|
value={formData.suggestedSLA}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
className="border-gray-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -209,19 +313,50 @@ export function CreateTemplate() {
|
|||||||
<div className="font-medium text-gray-900">{approver.name}</div>
|
<div className="font-medium text-gray-900">{approver.name}</div>
|
||||||
<div className="text-sm text-gray-500">{approver.email}</div>
|
<div className="text-sm text-gray-500">{approver.email}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-32 flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label htmlFor={`tat-${index}`} className="text-xs whitespace-nowrap">TAT (Hrs)</Label>
|
<div className="flex flex-col gap-1">
|
||||||
|
<Label htmlFor={`tat-${index}`} className="text-xs whitespace-nowrap">TAT</Label>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<Input
|
<Input
|
||||||
id={`tat-${index}`}
|
id={`tat-${index}`}
|
||||||
type="number"
|
type="number"
|
||||||
className="h-8 w-16"
|
className="h-8 w-16 border-gray-200"
|
||||||
value={approver.tat || 24}
|
value={approver.tat || ''}
|
||||||
|
min={1}
|
||||||
|
max={approver.tatType === 'days' ? 7 : 24}
|
||||||
|
placeholder={approver.tatType === 'days' ? '1' : '24'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
const val = parseInt(e.target.value) || 0;
|
||||||
|
const max = approver.tatType === 'days' ? 7 : 24;
|
||||||
|
// Optional: strict clamping or just allow typing and validate later
|
||||||
|
// For better UX, let's allow typing but validate in isFormValid
|
||||||
|
// But prevent entering negative numbers
|
||||||
|
if (val < 0) return;
|
||||||
|
|
||||||
const newApprovers = [...formData.approvers];
|
const newApprovers = [...formData.approvers];
|
||||||
newApprovers[index].tat = parseInt(e.target.value) || 0;
|
newApprovers[index].tat = e.target.value; // Store as string to allow clearing
|
||||||
setFormData(prev => ({ ...prev, approvers: newApprovers }));
|
setFormData(prev => ({ ...prev, approvers: newApprovers }));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
value={approver.tatType || 'hours'}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
const newApprovers = [...formData.approvers];
|
||||||
|
newApprovers[index].tatType = val;
|
||||||
|
newApprovers[index].tat = 1; // Reset to 1 on change
|
||||||
|
setFormData(prev => ({ ...prev, approvers: newApprovers }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-20 text-xs px-2">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="hours">Hours</SelectItem>
|
||||||
|
<SelectItem value="days">Days</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="ghost" size="sm" onClick={() => removeApprover(index)}>
|
<Button type="button" variant="ghost" size="sm" onClick={() => removeApprover(index)}>
|
||||||
<X className="w-4 h-4 text-gray-500 hover:text-red-600" />
|
<X className="w-4 h-4 text-gray-500 hover:text-red-600" />
|
||||||
@ -240,9 +375,10 @@ export function CreateTemplate() {
|
|||||||
<Label>Add Approver</Label>
|
<Label>Add Approver</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search user by name or email..."
|
placeholder="Type '@' to search user by name or email..."
|
||||||
value={approverSearchInput}
|
value={approverSearchInput}
|
||||||
onChange={(e) => handleApproverSearch(e.target.value)}
|
onChange={(e) => handleApproverSearch(e.target.value)}
|
||||||
|
className="border-gray-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -267,16 +403,96 @@ export function CreateTemplate() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* TAT Summary */}
|
||||||
|
{formData.approvers.length > 0 && (
|
||||||
|
<div className="mt-6 p-4 bg-gradient-to-r from-emerald-50 to-teal-50 rounded-lg border border-emerald-200">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Clock className="w-5 h-5 text-emerald-600 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="font-semibold text-emerald-900">TAT Summary</h4>
|
||||||
|
<div className="text-right">
|
||||||
|
{(() => {
|
||||||
|
const totalCalendarDays = formData.approvers.reduce((sum: number, a: any) => {
|
||||||
|
const tat = Number(a.tat || 0);
|
||||||
|
const tatType = a.tatType || 'hours';
|
||||||
|
if (tatType === 'days') {
|
||||||
|
return sum + tat;
|
||||||
|
} else {
|
||||||
|
return sum + (tat / 24);
|
||||||
|
}
|
||||||
|
}, 0) || 0;
|
||||||
|
const displayDays = Math.ceil(totalCalendarDays);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-lg font-bold text-emerald-800">{displayDays} {displayDays === 1 ? 'Day' : 'Days'}</div>
|
||||||
|
<div className="text-xs text-emerald-600">Total Duration</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{formData.approvers.map((approver: any, idx: number) => {
|
||||||
|
const tat = Number(approver.tat || 0);
|
||||||
|
const tatType = approver.tatType || 'hours';
|
||||||
|
const hours = tatType === 'days' ? tat * 24 : tat;
|
||||||
|
if (!tat) return null;
|
||||||
|
return (
|
||||||
|
<div key={idx} className="bg-white/60 p-2 rounded border border-emerald-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-emerald-900">Level {idx + 1}</span>
|
||||||
|
<span className="text-sm text-emerald-700">{hours} {hours === 1 ? 'hour' : 'hours'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{(() => {
|
||||||
|
const totalHours = formData.approvers.reduce((sum: number, a: any) => {
|
||||||
|
const tat = Number(a.tat || 0);
|
||||||
|
const tatType = a.tatType || 'hours';
|
||||||
|
if (tatType === 'days') {
|
||||||
|
return sum + (tat * 24);
|
||||||
|
} else {
|
||||||
|
return sum + tat;
|
||||||
|
}
|
||||||
|
}, 0) || 0;
|
||||||
|
const workingDays = Math.ceil(totalHours / 8);
|
||||||
|
if (totalHours === 0) return null;
|
||||||
|
return (
|
||||||
|
<div className="bg-white/80 p-3 rounded border border-emerald-200">
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-lg font-bold text-emerald-800">{totalHours}h</div>
|
||||||
|
<div className="text-xs text-emerald-600">Total Hours</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-lg font-bold text-emerald-800">{workingDays}</div>
|
||||||
|
<div className="text-xs text-emerald-600">Working Days*</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-emerald-600 mt-2 text-center">*Based on 8-hour working days</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-4">
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
<Button type="button" variant="outline" onClick={() => navigate(-1)}>Cancel</Button>
|
<Button type="button" variant="outline" onClick={() => navigate('/admin/templates')}>Cancel</Button>
|
||||||
<Button type="submit" disabled={loading} className="bg-re-green hover:bg-re-green/90">
|
<Button type="submit" disabled={loading || !isFormValid} className="bg-re-green hover:bg-re-green/90">
|
||||||
{loading ? 'Creating...' : (
|
{loading ? 'Saving...' : (
|
||||||
<>
|
<>
|
||||||
<Save className="w-4 h-4 mr-2" />
|
<Save className="w-4 h-4 mr-2" />
|
||||||
Create Template
|
{isEditing ? 'Update Template' : 'Create Template'}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { ArrowLeft, Save, ChevronRight, Check } from 'lucide-react';
|
import { ArrowLeft, ChevronRight, Check } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { getTemplates, WorkflowTemplate as BackendTemplate } from '@/services/workflowTemplateApi';
|
import { getTemplates, WorkflowTemplate as BackendTemplate } from '@/services/workflowTemplateApi';
|
||||||
|
import { createWorkflowMultipart, submitWorkflow, CreateWorkflowFromFormPayload } from '@/services/workflowApi';
|
||||||
import { RequestTemplate } from '@/hooks/useCreateRequestForm';
|
import { RequestTemplate } from '@/hooks/useCreateRequestForm';
|
||||||
import { FileText } from 'lucide-react';
|
import { FileText } from 'lucide-react';
|
||||||
import { AdminRequestDetailsStep } from './components/AdminRequestDetailsStep';
|
import { AdminRequestDetailsStep } from './components/AdminRequestDetailsStep';
|
||||||
@ -15,7 +16,7 @@ import { WizardStepper } from '@/components/workflow/CreateRequest/WizardStepper
|
|||||||
export function CreateAdminRequest() {
|
export function CreateAdminRequest() {
|
||||||
const { templateId } = useParams<{ templateId: string }>();
|
const { templateId } = useParams<{ templateId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user: _ } = useAuth(); // Keeping hook call but ignoring return if needed for auth check side effect, or just remove destructuring
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@ -63,7 +64,8 @@ export function CreateAdminRequest() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error('Template not found');
|
toast.error('Template not found');
|
||||||
navigate('/new-request');
|
// navigate('/new-request'); // Removed to prevent potential redirect loops
|
||||||
|
// We will show the "Template not found" UI below since template is null
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading template:', error);
|
console.error('Error loading template:', error);
|
||||||
@ -84,13 +86,28 @@ export function CreateAdminRequest() {
|
|||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
// Construct the request payload
|
const formPayload: CreateWorkflowFromFormPayload = {
|
||||||
// This matches the structure expected by the backend for a generic request
|
templateId: template.id,
|
||||||
// But we will likely need to adjust based on how "createRequest" is implemented globally
|
templateType: 'TEMPLATE',
|
||||||
// For now, we simulate the submission or call the common handler if available
|
title: formData.title,
|
||||||
|
description: formData.description,
|
||||||
|
priorityUi: template.priority === 'high' ? 'express' : 'standard',
|
||||||
|
approverCount: template.workflowApprovers?.length || 0,
|
||||||
|
approvers: (template.workflowApprovers || []).map((a: any) => ({
|
||||||
|
email: a.email,
|
||||||
|
name: a.name,
|
||||||
|
tat: a.tat,
|
||||||
|
tatType: 'hours'
|
||||||
|
})),
|
||||||
|
spectators: [],
|
||||||
|
ccList: []
|
||||||
|
};
|
||||||
|
|
||||||
// Simulating API call for demonstration of flow
|
const response = await createWorkflowMultipart(formPayload, documents);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
||||||
|
if (response && response.id) {
|
||||||
|
await submitWorkflow(response.id);
|
||||||
|
}
|
||||||
|
|
||||||
toast.success('Request Submitted Successfully', {
|
toast.success('Request Submitted Successfully', {
|
||||||
description: `Your request "${formData.title}" has been created.`
|
description: `Your request "${formData.title}" has been created.`
|
||||||
@ -114,7 +131,29 @@ export function CreateAdminRequest() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!template) return null;
|
if (!template) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-4">
|
||||||
|
<div className="bg-white p-8 rounded-lg shadow-md max-w-md w-full text-center">
|
||||||
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<FileText className="w-8 h-8 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">Template Not Found</h2>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
The requested template could not be loaded. It may have been deleted or you do not have permission to view it.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3 justify-center">
|
||||||
|
<Button variant="outline" onClick={() => navigate('/dashboard')}>
|
||||||
|
Go to Dashboard
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => navigate('/new-request')}>
|
||||||
|
Browse Templates
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-gray-50 overflow-hidden">
|
<div className="h-screen flex flex-col bg-gray-50 overflow-hidden">
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
* - components/ - UI components
|
* - components/ - UI components
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { TemplateSelectionModal } from '@/components/modals/TemplateSelectionModal';
|
import { TemplateSelectionModal } from '@/components/modals/TemplateSelectionModal';
|
||||||
@ -95,7 +95,7 @@ export function CreateRequest({
|
|||||||
fetchTemplates();
|
fetchTemplates();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const allTemplates = [...REQUEST_TEMPLATES, ...adminTemplates];
|
const allTemplates = useMemo(() => [...REQUEST_TEMPLATES, ...adminTemplates], [adminTemplates]);
|
||||||
|
|
||||||
// Form and state management hooks
|
// Form and state management hooks
|
||||||
const {
|
const {
|
||||||
@ -180,16 +180,30 @@ export function CreateRequest({
|
|||||||
// - Steps 1, 3, or 4: Navigate back to previous screen (browser history)
|
// - Steps 1, 3, or 4: Navigate back to previous screen (browser history)
|
||||||
// - Other steps: Go to previous step in wizard
|
// - Other steps: Go to previous step in wizard
|
||||||
const handleBackButton = useCallback(() => {
|
const handleBackButton = useCallback(() => {
|
||||||
if (currentStep === 1 || currentStep === 3 || currentStep === 4) {
|
// If on the first step (Template Selection), always go back to dashboard
|
||||||
// On steps 1, 3, or 4, navigate back to previous screen using browser history
|
// This prevents infinite loops if the user was redirected here from an error page
|
||||||
|
if (currentStep === 1) {
|
||||||
|
navigate('/dashboard', { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other major steps (3=Approval, 4=Participants), we might want to go back to prev screen
|
||||||
|
// But for consistency and safety against loops, let's treat "Back" as "Previous Step"
|
||||||
|
// or explicit exit if at the start of a flow.
|
||||||
|
|
||||||
|
// Actually, keep the history logic ONLY for later steps if needed, but Step 1 MUST be explicit.
|
||||||
|
if (currentStep === 3 || currentStep === 4) {
|
||||||
|
// ... existing logic for these steps if we want to keep it,
|
||||||
|
// but typically "Back" in a wizard should go to previous wizard step.
|
||||||
|
// However, the original code had this specific logic.
|
||||||
|
// Let's defer to prevStep() for wizard navigation, and only use history/dashboard for exit.
|
||||||
|
|
||||||
if (onBack) {
|
if (onBack) {
|
||||||
onBack();
|
onBack();
|
||||||
} else {
|
} else {
|
||||||
// Use window.history.back() as fallback for more reliable navigation
|
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
} else {
|
} else {
|
||||||
// If no history, navigate to dashboard
|
|
||||||
navigate('/dashboard', { replace: true });
|
navigate('/dashboard', { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,12 +13,35 @@ export interface WorkflowTemplate {
|
|||||||
fields?: any;
|
fields?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simple in-memory cache
|
||||||
|
let templatesCache: WorkflowTemplate[] | null = null;
|
||||||
|
|
||||||
|
export const getCachedTemplates = () => templatesCache;
|
||||||
|
|
||||||
export const createTemplate = async (templateData: Partial<WorkflowTemplate>): Promise<WorkflowTemplate> => {
|
export const createTemplate = async (templateData: Partial<WorkflowTemplate>): Promise<WorkflowTemplate> => {
|
||||||
const response = await apiClient.post('/templates', templateData);
|
const response = await apiClient.post('/templates', templateData);
|
||||||
|
// Invalidate cache or add to it
|
||||||
|
if (templatesCache) templatesCache.push(response.data.data);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTemplates = async (): Promise<WorkflowTemplate[]> => {
|
export const getTemplates = async (): Promise<WorkflowTemplate[]> => {
|
||||||
const response = await apiClient.get('/templates');
|
const response = await apiClient.get('/templates');
|
||||||
|
templatesCache = response.data.data;
|
||||||
|
return response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTemplate = async (id: string): Promise<void> => {
|
||||||
|
await apiClient.delete(`/templates/${id}`);
|
||||||
|
if (templatesCache) {
|
||||||
|
templatesCache = templatesCache.filter(t => t.id !== id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTemplate = async (id: string, templateData: Partial<WorkflowTemplate>): Promise<WorkflowTemplate> => {
|
||||||
|
const response = await apiClient.put(`/templates/${id}`, templateData);
|
||||||
|
if (templatesCache) {
|
||||||
|
templatesCache = templatesCache.map(t => t.id === id ? response.data.data : t);
|
||||||
|
}
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user