diff --git a/src/App.tsx b/src/App.tsx index a7691e0..b335d11 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,8 @@ import { Settings } from '@/pages/Settings'; import { Notifications } from '@/pages/Notifications'; import { DetailedReports } from '@/pages/DetailedReports'; 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 { Toaster } from '@/components/ui/sonner'; import { toast } from 'sonner'; @@ -483,6 +485,33 @@ function AppRoutes({ onLogout }: AppProps) { } /> + {/* Admin Routes - Placed higher to prevent matching issues */} + + + + } + /> + + {/* Create Request from Admin Template (Dedicated Flow) */} + + } + /> + + + + + } + /> + {/* Open Requests */} - {/* Admin Control Panel */} - - - - } - /> + + + { try { @@ -55,12 +55,13 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on return 'U'; } }; - + const menuItems = useMemo(() => { const items = [ { id: 'dashboard', label: 'Dashboard', icon: Home }, // Add "All Requests" for all users (admin sees org-level, regular users see their participant requests) { 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 @@ -83,7 +84,7 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on // Mark as read if (!notification.isRead) { await notificationApi.markAsRead(notification.notificationId); - setNotifications(prev => + setNotifications(prev => prev.map(n => n.notificationId === notification.notificationId ? { ...n, isRead: true } : n) ); setUnreadCount(prev => Math.max(0, prev - 1)); @@ -96,14 +97,14 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on if (requestNumber) { // Determine which tab to open based on notification type let navigationUrl = `request/${requestNumber}`; - + // Work note related notifications should open Work Notes tab - if (notification.notificationType === 'mention' || - notification.notificationType === 'comment' || - notification.notificationType === 'worknote') { + if (notification.notificationType === 'mention' || + notification.notificationType === 'comment' || + notification.notificationType === 'worknote') { navigationUrl += '?tab=worknotes'; } - + // Navigate to request detail page onNavigate(navigationUrl); } @@ -137,7 +138,7 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on try { const result = await notificationApi.list({ page: 1, limit: 4, unreadOnly: false }); if (!mounted) return; - + const notifs = result.data?.notifications || []; setNotifications(notifs); setUnreadCount(result.data?.unreadCount || 0); @@ -158,7 +159,7 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on // Listen for new notifications const handleNewNotification = (data: { notification: Notification }) => { if (!mounted) return; - + setNotifications(prev => [data.notification, ...prev].slice(0, 4)); // Keep latest 4 for dropdown setUnreadCount(prev => prev + 1); }; @@ -199,7 +200,7 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
{/* Mobile Overlay */} {sidebarOpen && ( -
setSidebarOpen(false)} /> @@ -223,9 +224,9 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
- Royal Enfield Logo

RE Flow

@@ -233,28 +234,31 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
- {menuItems.map((item) => ( + {menuItems.filter(item => !item.adminOnly || (user as any)?.role === 'ADMIN').map((item) => ( ))}
- + {/* Quick Action in Sidebar - Right below menu items */}
+ {/* Search bar commented out */} {/*
@@ -342,9 +346,8 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on {notifications.map((notif) => (
handleNotificationClick(notif)} >
@@ -403,8 +406,8 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on Settings - setShowLogoutDialog(true)} + setShowLogoutDialog(true)} className="text-red-600 focus:text-red-600" > diff --git a/src/components/workflow/CreateRequest/TemplateSelectionStep.tsx b/src/components/workflow/CreateRequest/TemplateSelectionStep.tsx index 48d6b26..c967286 100644 --- a/src/components/workflow/CreateRequest/TemplateSelectionStep.tsx +++ b/src/components/workflow/CreateRequest/TemplateSelectionStep.tsx @@ -1,15 +1,19 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; -import { Check, Clock, Users, Info, Flame, Target, TrendingUp } from 'lucide-react'; +import { Check, Clock, Users, Info, Flame, Target, TrendingUp, FolderOpen, ArrowLeft } from 'lucide-react'; import { RequestTemplate } from '@/hooks/useCreateRequestForm'; +import { Button } from '@/components/ui/button'; interface TemplateSelectionStepProps { templates: RequestTemplate[]; selectedTemplate: RequestTemplate | null; onSelectTemplate: (template: RequestTemplate) => void; + adminTemplates?: RequestTemplate[]; } const getPriorityIcon = (priority: string) => { @@ -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({ templates, selectedTemplate, - onSelectTemplate + onSelectTemplate, + adminTemplates = [] }: TemplateSelectionStepProps) { + const [viewMode, setViewMode] = useState<'main' | 'admin'>('main'); + + const navigate = useNavigate(); + + const handleTemplateClick = (template: RequestTemplate) => { + if (template.id === 'admin-templates-category') { + setViewMode('admin'); + } else { + if (viewMode === 'admin') { + // If selecting an actual admin template, redirect to dedicated flow + navigate(`/create-admin-request/${template.id}`); + } else { + // Default behavior for standard templates + onSelectTemplate(template); + } + } + }; + + const displayTemplates = viewMode === 'main' + ? [ + ...templates, + { + id: 'admin-templates-category', + name: 'Admin Templates', + description: 'Browse standardized request workflows created by your organization administrators', + category: 'Organization', + icon: FolderOpen, + estimatedTime: 'Variable', + commonApprovers: [], + suggestedSLA: 0, + priority: 'medium', + fields: {} + } as any + ] + : adminTemplates; + return (

- Choose Your Request Type + {viewMode === 'main' ? 'Choose Your Request Type' : 'Organization Templates'}

- 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.'}

+ {viewMode === 'admin' && ( +
+ +
+ )} + {/* Template Cards Grid */} -
- {templates.map((template) => { - const isComingSoon = template.id === 'existing-template'; - const isDisabled = isComingSoon; - - return ( - - onSelectTemplate(template) : undefined} - data-testid={`template-card-${template.id}-clickable`} + {displayTemplates.length === 0 && viewMode === 'admin' ? ( +
+ +

No admin templates available yet.

+
+ ) : ( + displayTemplates.map((template) => { + const isComingSoon = template.id === 'existing-template' && viewMode === 'main'; // Only show coming soon for placeholder + const isDisabled = isComingSoon; + const isCategoryCard = template.id === 'admin-templates-category'; + const isCustomCard = template.id === 'custom'; + const isSelected = selectedTemplate?.id === template.id; + + return ( + - -
-
- -
- {selectedTemplate?.id === template.id && ( - -
- -
-
- )} -
-
-
- - {template.name} - - {isComingSoon && ( - - Coming Soon - - )} -
-
- - {template.category} - - {getPriorityIcon(template.priority)} -
-
-
- -

handleTemplateClick(template) : undefined} + data-testid={`template-card-${template.id}-clickable`} > - {template.description} -

- -
-
- - {template.estimatedTime} -
-
- - {template.commonApprovers.length} approvers -
-
-
-
-
- ); - })} + +
+
+ +
+ {isSelected && ( + +
+ +
+
+ )} +
+
+
+ + {template.name} + + {isComingSoon && ( + + Coming Soon + + )} +
+
+ + {template.category} + + {getPriorityIcon(template.priority)} +
+
+
+ +

+ {template.description} +

+ + {!isCategoryCard && ( + <> + +
+
+ + {template.estimatedTime} +
+
+ + {template.commonApprovers?.length || 0} approvers +
+
+ + )} + {isCategoryCard && ( +
+

+ Click to browse templates → +

+
+ )} +
+ + + ); + }) + )}
{/* Template Details Card */} @@ -183,7 +250,7 @@ export function TemplateSelectionStep({
-

{selectedTemplate.suggestedSLA} days

+

{selectedTemplate.suggestedSLA} hours

@@ -198,18 +265,22 @@ export function TemplateSelectionStep({
- +
- {selectedTemplate.commonApprovers.map((approver, index) => ( - - {approver} - - ))} + {selectedTemplate.commonApprovers?.length > 0 ? ( + selectedTemplate.commonApprovers.map((approver, index) => ( + + {approver} + + )) + ) : ( + No specific approvers defined + )}
diff --git a/src/components/workflow/CreateRequest/WizardStepper.tsx b/src/components/workflow/CreateRequest/WizardStepper.tsx index d25284a..c0e400a 100644 --- a/src/components/workflow/CreateRequest/WizardStepper.tsx +++ b/src/components/workflow/CreateRequest/WizardStepper.tsx @@ -19,17 +19,20 @@ interface WizardStepperProps { export function WizardStepper({ currentStep, totalSteps, stepNames }: WizardStepperProps) { 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 ( -
-
+
{/* Mobile: Current step indicator only */}
-
@@ -51,11 +54,11 @@ export function WizardStepper({ currentStep, totalSteps, stepNames }: WizardStep
{/* Progress bar */} -
-
-
+
{stepNames.map((_, index) => ( -
-
+
{index + 1 < currentStep ? ( @@ -85,26 +87,24 @@ export function WizardStepper({ currentStep, totalSteps, stepNames }: WizardStep )}
{index < stepNames.length - 1 && ( -
)}
))}
-
{stepNames.map((step, index) => ( - {step} diff --git a/src/hooks/useCreateRequestForm.ts b/src/hooks/useCreateRequestForm.ts index 23514c4..d3b39d5 100644 --- a/src/hooks/useCreateRequestForm.ts +++ b/src/hooks/useCreateRequestForm.ts @@ -10,6 +10,7 @@ export interface RequestTemplate { icon: React.ComponentType; estimatedTime: string; commonApprovers: string[]; + workflowApprovers?: any[]; // Full approver objects for Admin Templates suggestedSLA: number; priority: 'high' | 'medium' | 'low'; fields: { @@ -199,7 +200,7 @@ export function useCreateRequestForm( const approvals = Array.isArray(details.approvals) ? details.approvals : []; const participants = Array.isArray(details.participants) ? details.participants : []; const documents = Array.isArray(details.documents) ? details.documents.filter((d: any) => !d.isDeleted) : []; - + // Store existing documents for tracking setExistingDocuments(documents); diff --git a/src/pages/Admin/Templates/CreateTemplate.tsx b/src/pages/Admin/Templates/CreateTemplate.tsx new file mode 100644 index 0000000..aaf2cf9 --- /dev/null +++ b/src/pages/Admin/Templates/CreateTemplate.tsx @@ -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) => { + 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 ( +
+
+ +
+

Create Workflow Template

+

Define a new standardized request workflow

+
+
+ +
+ + + Basic Information + General details about the template + + +
+
+ + +
+
+ +
+ {/* Simple text input for now, could be select */} + +
+
+
+ +
+ +