save draft an submit rquest adddd isDraft flag to support postman submit and dealer related code commented and made it completely non-templatized for production

This commit is contained in:
laxmanhalaki 2026-02-06 20:12:28 +05:30
parent 1d205a4038
commit c97053e0e3
11 changed files with 156 additions and 251 deletions

View File

@ -73,13 +73,14 @@ 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/templates', label: 'Admin Templates', icon: Plus, adminOnly: true }, { id: 'my-requests', label: 'My Requests', icon: User, adminOnly: false },
// { id: 'admin/templates', label: 'Admin Templates', icon: Plus, adminOnly: true },
]; ];
// Add remaining menu items (exclude "My Requests" for dealers) // Add remaining menu items (exclude "My Requests" for dealers)
if (!isDealer) { // if (!isDealer) {
items.push({ id: 'my-requests', label: 'My Requests', icon: User }); // items.push({ id: 'my-requests', label: 'My Requests', icon: User });
} // }
items.push( items.push(
{ id: 'open-requests', label: 'Open Requests', icon: FileText }, { id: 'open-requests', label: 'Open Requests', icon: FileText },

View File

@ -378,7 +378,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center"> <div className="border-2 border-dashed border-border rounded-lg p-6 text-center">
<Upload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" /> <Upload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground mb-2"> <p className="text-sm text-muted-foreground mb-2">
Drag and drop files here, or click to browse click to browse
</p> </p>
<input <input
type="file" type="file"

View File

@ -160,7 +160,7 @@ export function DocumentsStep({
<Upload className="h-12 w-12 mx-auto mb-4 text-gray-400" /> <Upload className="h-12 w-12 mx-auto mb-4 text-gray-400" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Upload Files</h3> <h3 className="text-lg font-semibold text-gray-900 mb-2">Upload Files</h3>
<p className="text-gray-600 mb-4"> <p className="text-gray-600 mb-4">
Drag and drop files here, or click to browse click to browse
</p> </p>
<input <input
type="file" type="file"

View File

@ -52,18 +52,18 @@ export function TemplateSelectionStep({
const displayTemplates = viewMode === 'main' const displayTemplates = viewMode === 'main'
? [ ? [
...templates, ...templates,
{ // {
id: 'admin-templates-category', // id: 'admin-templates-category',
name: 'Admin Templates', // name: 'Admin Templates',
description: 'Browse standardized request workflows created by your organization administrators', // description: 'Browse standardized request workflows created by your organization administrators',
category: 'Organization', // category: 'Organization',
icon: FolderOpen, // icon: FolderOpen,
estimatedTime: 'Variable', // estimatedTime: 'Variable',
commonApprovers: [], // commonApprovers: [],
suggestedSLA: 0, // suggestedSLA: 0,
priority: 'medium', // priority: 'medium',
fields: {} // fields: {}
} as any // } as any
] ]
: adminTemplates; : adminTemplates;

View File

@ -1,22 +1,12 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Plus, Pencil, Trash2, Search, FileText, AlertTriangle } from 'lucide-react'; import { Plus, Pencil, Search, FileText } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { import { getTemplates, WorkflowTemplate, getCachedTemplates } from '@/services/workflowTemplateApi';
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'; import { toast } from 'sonner';
export function AdminTemplatesList() { export function AdminTemplatesList() {
@ -25,8 +15,6 @@ export function AdminTemplatesList() {
// Only show full loading skeleton if we don't have any data yet // Only show full loading skeleton if we don't have any data yet
const [loading, setLoading] = useState(() => !getCachedTemplates()); const [loading, setLoading] = useState(() => !getCachedTemplates());
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const fetchTemplates = async () => { const fetchTemplates = async () => {
try { try {
@ -49,22 +37,6 @@ export function AdminTemplatesList() {
fetchTemplates(); 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 => const filteredTemplates = templates.filter(template =>
template.name.toLowerCase().includes(searchQuery.toLowerCase()) || template.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
@ -152,7 +124,7 @@ export function AdminTemplatesList() {
</Badge> </Badge>
</div> </div>
<CardTitle className="line-clamp-1 text-lg">{template.name}</CardTitle> <CardTitle className="line-clamp-1 text-lg">{template.name}</CardTitle>
<CardDescription className="line-clamp-2 h-10"> <CardDescription className="line-clamp-3 min-h-[4.5rem]">
{template.description} {template.description}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@ -181,14 +153,6 @@ export function AdminTemplatesList() {
<Pencil className="w-4 h-4 mr-2" /> <Pencil className="w-4 h-4 mr-2" />
Edit Edit
</Button> </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> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -196,33 +160,6 @@ export function AdminTemplatesList() {
</div> </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> </div>
); );
} }

View File

@ -11,8 +11,6 @@ import {
validateApproversForSubmission, validateApproversForSubmission,
} from '../utils/payloadBuilders'; } from '../utils/payloadBuilders';
import { import {
createAndSubmitWorkflow,
updateAndSubmitWorkflow,
createWorkflow, createWorkflow,
updateWorkflowRequest, updateWorkflowRequest,
} from '../services/createRequestService'; } from '../services/createRequestService';
@ -59,14 +57,15 @@ export function useCreateRequestSubmission({
try { try {
if (isEditing && editRequestId) { if (isEditing && editRequestId) {
// Update existing workflow // Update existing workflow with isDraft: false (Submit)
const updatePayload = buildUpdatePayload( const updatePayload = buildUpdatePayload(
formData, formData,
user, user,
documentsToDelete documentsToDelete,
false
); );
await updateAndSubmitWorkflow( await updateWorkflowRequest(
editRequestId, editRequestId,
updatePayload, updatePayload,
documents, documents,
@ -85,14 +84,15 @@ export function useCreateRequestSubmission({
template: selectedTemplate, template: selectedTemplate,
}); });
} else { } else {
// Create new workflow // Create new workflow with isDraft: false (Submit)
const createPayload = buildCreatePayload( const createPayload = buildCreatePayload(
formData, formData,
selectedTemplate, selectedTemplate,
user user,
false
); );
const result = await createAndSubmitWorkflow(createPayload, documents); const result = await createWorkflow(createPayload, documents);
// Show toast after backend confirmation // Show toast after backend confirmation
toast.success('Request Submitted Successfully!', { toast.success('Request Submitted Successfully!', {
@ -133,11 +133,12 @@ export function useCreateRequestSubmission({
try { try {
if (isEditing && editRequestId) { if (isEditing && editRequestId) {
// Update existing draft // Update existing draft with isDraft: true
const updatePayload = buildUpdatePayload( const updatePayload = buildUpdatePayload(
formData, formData,
user, user,
documentsToDelete documentsToDelete,
true
); );
await updateWorkflowRequest( await updateWorkflowRequest(
@ -158,11 +159,12 @@ export function useCreateRequestSubmission({
template: selectedTemplate, template: selectedTemplate,
}); });
} else { } else {
// Create new draft // Create new draft with isDraft: true
const createPayload = buildCreatePayload( const createPayload = buildCreatePayload(
formData, formData,
selectedTemplate, selectedTemplate,
user user,
true
); );
const result = await createWorkflow(createPayload, documents); const result = await createWorkflow(createPayload, documents);

View File

@ -4,7 +4,6 @@
import { import {
createWorkflowMultipart, createWorkflowMultipart,
submitWorkflow,
updateWorkflow, updateWorkflow,
updateWorkflowMultipart, updateWorkflowMultipart,
} from '@/services/workflowApi'; } from '@/services/workflowApi';
@ -14,7 +13,7 @@ import {
} from '../types/createRequest.types'; } from '../types/createRequest.types';
/** /**
* Create a new workflow * Create a new workflow (supports both draft and direct submission via isDraft flag)
*/ */
export async function createWorkflow( export async function createWorkflow(
payload: CreateWorkflowPayload, payload: CreateWorkflowPayload,
@ -29,7 +28,7 @@ export async function createWorkflow(
} }
/** /**
* Update an existing workflow * Update an existing workflow (supports both draft and direct submission via isDraft flag)
*/ */
export async function updateWorkflowRequest( export async function updateWorkflowRequest(
requestId: string, requestId: string,
@ -51,36 +50,3 @@ export async function updateWorkflowRequest(
await updateWorkflow(requestId, payload); await updateWorkflow(requestId, payload);
} }
} }
/**
* Submit a workflow
*/
export async function submitWorkflowRequest(requestId: string): Promise<void> {
await submitWorkflow(requestId);
}
/**
* Create and submit a workflow in one operation
*/
export async function createAndSubmitWorkflow(
payload: CreateWorkflowPayload,
documents: File[]
): Promise<{ id: string }> {
const result = await createWorkflow(payload, documents);
await submitWorkflowRequest(result.id);
return result;
}
/**
* Update and submit a workflow in one operation
*/
export async function updateAndSubmitWorkflow(
requestId: string,
payload: UpdateWorkflowPayload,
documents: File[],
documentsToDelete: string[]
): Promise<void> {
await updateWorkflowRequest(requestId, payload, documents, documentsToDelete);
await submitWorkflowRequest(requestId);
}

View File

@ -67,6 +67,7 @@ export interface CreateWorkflowPayload {
email: string; email: string;
}>; }>;
participants: Participant[]; participants: Participant[];
isDraft?: boolean;
} }
export interface UpdateWorkflowPayload { export interface UpdateWorkflowPayload {
@ -76,6 +77,7 @@ export interface UpdateWorkflowPayload {
approvalLevels: ApprovalLevel[]; approvalLevels: ApprovalLevel[];
participants: Participant[]; participants: Participant[];
deleteDocumentIds?: string[]; deleteDocumentIds?: string[];
isDraft?: boolean;
} }
export interface ValidationModalState { export interface ValidationModalState {

View File

@ -17,16 +17,9 @@ import { buildApprovalLevels } from './approvalLevelBuilders';
export function buildCreatePayload( export function buildCreatePayload(
formData: FormData, formData: FormData,
selectedTemplate: RequestTemplate | null, selectedTemplate: RequestTemplate | null,
_user: any _user: any,
isDraft: boolean = false
): CreateWorkflowPayload { ): CreateWorkflowPayload {
// Filter out spectators who are also approvers (backend will handle validation)
const approverEmails = new Set(
(formData.approvers || []).map((a: any) => a?.email?.toLowerCase()).filter(Boolean)
);
const filteredSpectators = (formData.spectators || []).filter(
(s: any) => s?.email && !approverEmails.has(s.email.toLowerCase())
);
return { return {
templateId: selectedTemplate?.id || null, templateId: selectedTemplate?.id || null,
templateType: selectedTemplate?.id === 'custom' ? 'CUSTOM' : 'TEMPLATE', templateType: selectedTemplate?.id === 'custom' ? 'CUSTOM' : 'TEMPLATE',
@ -38,16 +31,17 @@ export function buildCreatePayload(
userId: a?.userId || '', userId: a?.userId || '',
email: a?.email || '', email: a?.email || '',
name: a?.name, name: a?.name,
tat: a?.tat || '', tat: a?.tat || 24,
tatType: a?.tatType || 'hours', tatType: a?.tatType || 'hours',
})), })),
spectators: filteredSpectators.map((s: any) => ({ spectators: (formData.spectators || []).map((s: any) => ({
userId: s?.userId || '', userId: s?.userId || '',
name: s?.name || '', name: s?.name || '',
email: s?.email || '', email: s?.email || '',
})), })),
ccList: [], // Auto-generated by backend ccList: [], // Auto-generated by backend
participants: [], // Auto-generated by backend from approvers and spectators participants: [], // Auto-generated by backend from approvers and spectators
isDraft,
}; };
} }
@ -58,7 +52,8 @@ export function buildCreatePayload(
export function buildUpdatePayload( export function buildUpdatePayload(
formData: FormData, formData: FormData,
_user: any, _user: any,
documentsToDelete: string[] documentsToDelete: string[],
isDraft: boolean = false
): UpdateWorkflowPayload { ): UpdateWorkflowPayload {
const approvalLevels = buildApprovalLevels( const approvalLevels = buildApprovalLevels(
formData.approvers || [], formData.approvers || [],
@ -72,6 +67,7 @@ export function buildUpdatePayload(
approvalLevels, approvalLevels,
participants: [], // Auto-generated by backend from approval levels participants: [], // Auto-generated by backend from approval levels
deleteDocumentIds: documentsToDelete.length > 0 ? documentsToDelete : undefined, deleteDocumentIds: documentsToDelete.length > 0 ? documentsToDelete : undefined,
isDraft,
}; };
} }
@ -112,4 +108,3 @@ export function validateApproversForSubmission(
return { valid: true }; return { valid: true };
} }

View File

@ -62,7 +62,7 @@ export function TATBreachReport({
</div> </div>
</div> </div>
<Badge variant="destructive" className="text-sm font-medium self-start sm:self-auto"> <Badge variant="destructive" className="text-sm font-medium self-start sm:self-auto">
{breachedRequests.length} {breachedRequests.length === 1 ? 'Breach' : 'Breaches'} {pagination.totalRecords} {pagination.totalRecords === 1 ? 'Breach' : 'Breaches'}
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
@ -164,8 +164,7 @@ export function TATBreachReport({
<td className="py-3 px-4"> <td className="py-3 px-4">
<Badge <Badge
variant="outline" variant="outline"
className={`text-xs font-medium ${ className={`text-xs font-medium ${req.priority === 'express'
req.priority === 'express'
? 'bg-orange-100 text-orange-800 border-orange-200' ? 'bg-orange-100 text-orange-800 border-orange-200'
: 'bg-blue-100 text-blue-800 border-blue-200' : 'bg-blue-100 text-blue-800 border-blue-200'
}`} }`}

View File

@ -25,6 +25,7 @@ export interface CreateWorkflowFromFormPayload {
approvers: ApproverFormItem[]; approvers: ApproverFormItem[];
spectators?: ParticipantItem[]; spectators?: ParticipantItem[];
ccList?: ParticipantItem[]; ccList?: ParticipantItem[];
isDraft?: boolean; // Added isDraft to the payload interface
} }
// Utility to generate a RFC4122 v4 UUID (fallback if crypto.randomUUID not available) // Utility to generate a RFC4122 v4 UUID (fallback if crypto.randomUUID not available)
@ -102,6 +103,7 @@ export async function createWorkflowFromForm(form: CreateWorkflowFromFormPayload
priority, // STANDARD | EXPRESS priority, // STANDARD | EXPRESS
approvalLevels, approvalLevels,
participants: participants.length ? participants : undefined, participants: participants.length ? participants : undefined,
isDraft: form.isDraft, // Added isDraft to the payload
}; };
const res = await apiClient.post('/workflows', payload); const res = await apiClient.post('/workflows', payload);
@ -116,6 +118,7 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
title: form.title, title: form.title,
description: form.description, description: form.description,
priority: form.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD', priority: form.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD',
isDraft: form.isDraft, // Added isDraft to the payload
// Simplified approvers format - only email and tatHours required // Simplified approvers format - only email and tatHours required
approvers: Array.from({ length: form.approverCount || 1 }, (_, i) => { approvers: Array.from({ length: form.approverCount || 1 }, (_, i) => {
const a = form.approvers[i] || ({} as any); const a = form.approvers[i] || ({} as any);