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

@ -12,12 +12,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Switch } from '../ui/switch'; import { Switch } from '../ui/switch';
import { Calendar } from '../ui/calendar'; import { Calendar } from '../ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { import {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
Calendar as CalendarIcon, Calendar as CalendarIcon,
Upload, Upload,
X, X,
FileText, FileText,
Check, Check,
Users Users
@ -150,7 +150,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
onChange={(e) => updateFormData('title', e.target.value)} onChange={(e) => updateFormData('title', e.target.value)}
/> />
</div> </div>
<div> <div>
<Label htmlFor="description">Description *</Label> <Label htmlFor="description">Description *</Label>
<Textarea <Textarea
@ -215,9 +215,9 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
<Label htmlFor="workflow-sequential" className="text-sm">Parallel</Label> <Label htmlFor="workflow-sequential" className="text-sm">Parallel</Label>
</div> </div>
</div> </div>
<div className="p-3 bg-muted/50 rounded-lg text-sm text-muted-foreground"> <div className="p-3 bg-muted/50 rounded-lg text-sm text-muted-foreground">
{formData.workflowType === 'sequential' {formData.workflowType === 'sequential'
? 'Approvers will review the request one after another in the order you specify.' ? 'Approvers will review the request one after another in the order you specify.'
: 'All approvers will review the request simultaneously.' : 'All approvers will review the request simultaneously.'
} }
@ -311,7 +311,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{availableUsers {availableUsers
.filter(user => .filter(user =>
!formData.spectators.find(s => s.id === user.id) && !formData.spectators.find(s => s.id === user.id) &&
!formData.approvers.find(a => a.id === user.id) !formData.approvers.find(a => a.id === user.id)
) )
@ -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

@ -69,7 +69,7 @@ export function DocumentsStep({
// Check file extension // Check file extension
const fileName = file.name.toLowerCase(); const fileName = file.name.toLowerCase();
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1); const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
if (!documentPolicy.allowedFileTypes.includes(fileExtension)) { if (!documentPolicy.allowedFileTypes.includes(fileExtension)) {
validationErrors.push({ validationErrors.push({
fileName: file.name, fileName: file.name,
@ -111,16 +111,16 @@ export function DocumentsStep({
const type = (doc.fileType || doc.file_type || '').toLowerCase(); const type = (doc.fileType || doc.file_type || '').toLowerCase();
const name = (doc.originalFileName || doc.fileName || '').toLowerCase(); const name = (doc.originalFileName || doc.fileName || '').toLowerCase();
return type.includes('image') || type.includes('pdf') || return type.includes('image') || type.includes('pdf') ||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.jpg') || name.endsWith('.jpeg') ||
name.endsWith('.png') || name.endsWith('.gif') || name.endsWith('.png') || name.endsWith('.gif') ||
name.endsWith('.pdf'); name.endsWith('.pdf');
} else { } else {
const type = (doc.type || '').toLowerCase(); const type = (doc.type || '').toLowerCase();
const name = (doc.name || '').toLowerCase(); const name = (doc.name || '').toLowerCase();
return type.includes('image') || type.includes('pdf') || return type.includes('image') || type.includes('pdf') ||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.jpg') || name.endsWith('.jpeg') ||
name.endsWith('.png') || name.endsWith('.gif') || name.endsWith('.png') || name.endsWith('.gif') ||
name.endsWith('.pdf'); name.endsWith('.pdf');
} }
}; };
@ -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"
@ -172,10 +172,10 @@ export function DocumentsStep({
ref={fileInputRef} ref={fileInputRef}
data-testid="documents-file-input" data-testid="documents-file-input"
/> />
<Button <Button
variant="outline" variant="outline"
size="lg" size="lg"
type="button" type="button"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
data-testid="documents-browse-button" data-testid="documents-browse-button"
> >
@ -206,7 +206,7 @@ export function DocumentsStep({
const docId = doc.documentId || doc.document_id || ''; const docId = doc.documentId || doc.document_id || '';
const isDeleted = documentsToDelete.includes(docId); const isDeleted = documentsToDelete.includes(docId);
if (isDeleted) return null; if (isDeleted) return null;
return ( return (
<div key={docId} className="flex items-center justify-between p-4 rounded-lg border bg-gray-50" data-testid={`documents-existing-${docId}`}> <div key={docId} className="flex items-center justify-between p-4 rounded-lg border bg-gray-50" data-testid={`documents-existing-${docId}`}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -222,9 +222,9 @@ export function DocumentsStep({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{canPreview(doc, true) && ( {canPreview(doc, true) && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onPreviewDocument(doc, true)} onClick={() => onPreviewDocument(doc, true)}
data-testid={`documents-existing-${docId}-preview`} data-testid={`documents-existing-${docId}-preview`}
> >
@ -276,9 +276,9 @@ export function DocumentsStep({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{canPreview(file, false) && ( {canPreview(file, false) && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onPreviewDocument(file, false)} onClick={() => onPreviewDocument(file, false)}
data-testid={`documents-new-${index}-preview`} data-testid={`documents-new-${index}-preview`}
> >

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,
}; };
} }
@ -84,7 +80,7 @@ export function validateApproversForSubmission(
approverCount: number approverCount: number
): { valid: boolean; message?: string } { ): { valid: boolean; message?: string } {
const approversToCheck = approvers.slice(0, approverCount); const approversToCheck = approvers.slice(0, approverCount);
// Check if all approvers have valid emails // Check if all approvers have valid emails
const hasMissingEmails = approversToCheck.some( const hasMissingEmails = approversToCheck.some(
(a: any) => !a?.email || !a.email.trim() (a: any) => !a?.email || !a.email.trim()
@ -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>
@ -122,7 +122,7 @@ export function TATBreachReport({
params.set('approver', req.approverId!); params.set('approver', req.approverId!);
params.set('approverType', 'current'); params.set('approverType', 'current');
params.set('slaCompliance', 'breached'); params.set('slaCompliance', 'breached');
if (dateRange) params.set('dateRange', dateRange); if (dateRange) params.set('dateRange', dateRange);
if (customStartDate) params.set('startDate', customStartDate.toISOString()); if (customStartDate) params.set('startDate', customStartDate.toISOString());
if (customEndDate) params.set('endDate', customEndDate.toISOString()); if (customEndDate) params.set('endDate', customEndDate.toISOString());
@ -164,11 +164,10 @@ 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'
}`} }`}
> >
{req.priority} {req.priority}
</Badge> </Badge>

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,15 +118,16 @@ 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);
const tat = typeof a.tat === 'number' ? a.tat : 0; const tat = typeof a.tat === 'number' ? a.tat : 0;
if (!a.email || !a.email.trim()) { if (!a.email || !a.email.trim()) {
throw new Error(`Email is required for approver at level ${i + 1}.`); throw new Error(`Email is required for approver at level ${i + 1}.`);
} }
return { return {
email: a.email, email: a.email,
tat: tat, tat: tat,
@ -132,14 +135,14 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
}; };
}), }),
}; };
// Add spectators if any (simplified - only email required) // Add spectators if any (simplified - only email required)
if (form.spectators && form.spectators.length > 0) { if (form.spectators && form.spectators.length > 0) {
payload.spectators = form.spectators payload.spectators = form.spectators
.filter((s: any) => s?.email) .filter((s: any) => s?.email)
.map((s: any) => ({ email: s.email })); .map((s: any) => ({ email: s.email }));
} }
// Note: participants array is auto-generated by backend from approvers and spectators // Note: participants array is auto-generated by backend from approvers and spectators
// No need to build or send it from frontend // No need to build or send it from frontend
@ -157,22 +160,22 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
export async function listWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; initiator?: string; approver?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) { export async function listWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; initiator?: string; approver?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } = params; const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } = params;
const res = await apiClient.get('/workflows', { const res = await apiClient.get('/workflows', {
params: { params: {
page, page,
limit, limit,
search, search,
status, status,
priority, priority,
templateType, templateType,
department, department,
initiator, initiator,
approver, approver,
slaCompliance, slaCompliance,
dateRange, dateRange,
startDate, startDate,
endDate endDate
} }
}); });
return res.data?.data || res.data; return res.data?.data || res.data;
} }
@ -181,12 +184,12 @@ export async function listWorkflows(params: { page?: number; limit?: number; sea
// SEPARATE from listWorkflows (admin) to avoid interference // SEPARATE from listWorkflows (admin) to avoid interference
export async function listParticipantRequests(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; initiator?: string; approver?: string; approverType?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) { export async function listParticipantRequests(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; initiator?: string; approver?: string; approverType?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, approverType, slaCompliance, dateRange, startDate, endDate } = params; const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, approverType, slaCompliance, dateRange, startDate, endDate } = params;
const res = await apiClient.get('/workflows/participant-requests', { const res = await apiClient.get('/workflows/participant-requests', {
params: { params: {
page, page,
limit, limit,
search, search,
status, status,
priority, priority,
templateType, templateType,
department, department,
@ -210,12 +213,12 @@ export async function listParticipantRequests(params: { page?: number; limit?: n
// List requests where user is a participant (not initiator) - for "All Requests" page // List requests where user is a participant (not initiator) - for "All Requests" page
export async function listMyWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; department?: string; initiator?: string; approver?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) { export async function listMyWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; department?: string; initiator?: string; approver?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
const { page = 1, limit = 20, search, status, priority, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } = params; const { page = 1, limit = 20, search, status, priority, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } = params;
const res = await apiClient.get('/workflows/my', { const res = await apiClient.get('/workflows/my', {
params: { params: {
page, page,
limit, limit,
search, search,
status, status,
priority, priority,
department, department,
initiator, initiator,
@ -224,7 +227,7 @@ export async function listMyWorkflows(params: { page?: number; limit?: number; s
dateRange, dateRange,
startDate, startDate,
endDate endDate
} }
}); });
// Response structure: { success, data: { data: [...], pagination: {...} } } // Response structure: { success, data: { data: [...], pagination: {...} } }
return { return {
@ -236,12 +239,12 @@ export async function listMyWorkflows(params: { page?: number; limit?: number; s
// List requests where user is the initiator - for "My Requests" page // List requests where user is the initiator - for "My Requests" page
export async function listMyInitiatedWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) { export async function listMyInitiatedWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
const { page = 1, limit = 20, search, status, priority, templateType, department, slaCompliance, dateRange, startDate, endDate } = params; const { page = 1, limit = 20, search, status, priority, templateType, department, slaCompliance, dateRange, startDate, endDate } = params;
const res = await apiClient.get('/workflows/my-initiated', { const res = await apiClient.get('/workflows/my-initiated', {
params: { params: {
page, page,
limit, limit,
search, search,
status, status,
priority, priority,
templateType, templateType,
department, department,
@ -249,7 +252,7 @@ export async function listMyInitiatedWorkflows(params: { page?: number; limit?:
dateRange, dateRange,
startDate, startDate,
endDate endDate
} }
}); });
// Response structure: { success, data: { data: [...], pagination: {...} } } // Response structure: { success, data: { data: [...], pagination: {...} } }
return { return {
@ -304,22 +307,22 @@ export async function addApprover(requestId: string, email: string) {
} }
export async function addApproverAtLevel( export async function addApproverAtLevel(
requestId: string, requestId: string,
email: string, email: string,
tatHours: number, tatHours: number,
level: number level: number
) { ) {
const res = await apiClient.post(`/workflows/${requestId}/approvers/at-level`, { const res = await apiClient.post(`/workflows/${requestId}/approvers/at-level`, {
email, email,
tatHours, tatHours,
level level
}); });
return res.data?.data || res.data; return res.data?.data || res.data;
} }
export async function skipApprover(requestId: string, levelId: string, reason?: string) { export async function skipApprover(requestId: string, levelId: string, reason?: string) {
const res = await apiClient.post(`/workflows/${requestId}/approvals/${levelId}/skip`, { const res = await apiClient.post(`/workflows/${requestId}/approvals/${levelId}/skip`, {
reason reason
}); });
return res.data?.data || res.data; return res.data?.data || res.data;
} }
@ -376,7 +379,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
if (!contentDisposition) { if (!contentDisposition) {
return 'download'; return 'download';
} }
// Try to extract from filename* first (RFC 5987 encoded) - preferred for non-ASCII // Try to extract from filename* first (RFC 5987 encoded) - preferred for non-ASCII
const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/); const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
if (filenameStarMatch && filenameStarMatch[1]) { if (filenameStarMatch && filenameStarMatch[1]) {
@ -386,7 +389,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
// If decoding fails, fall back to regular filename // If decoding fails, fall back to regular filename
} }
} }
// Fallback to regular filename (for ASCII-only filenames) // Fallback to regular filename (for ASCII-only filenames)
const filenameMatch = contentDisposition.match(/filename="?([^";]+)"?/); const filenameMatch = contentDisposition.match(/filename="?([^";]+)"?/);
if (filenameMatch && filenameMatch.length > 1 && filenameMatch[1]) { if (filenameMatch && filenameMatch.length > 1 && filenameMatch[1]) {
@ -396,7 +399,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
const extracted = parts[0]?.trim(); const extracted = parts[0]?.trim();
return extracted || 'download'; return extracted || 'download';
} }
return 'download'; return 'download';
} }
@ -404,34 +407,34 @@ export async function downloadDocument(documentId: string): Promise<void> {
const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000'; const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
const downloadUrl = `${baseURL}/api/v1/workflows/documents/${documentId}/download`; const downloadUrl = `${baseURL}/api/v1/workflows/documents/${documentId}/download`;
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production'; const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
try { try {
// Build fetch options // Build fetch options
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
credentials: 'include', // Send cookies in production credentials: 'include', // Send cookies in production
}; };
// In development, add Authorization header from localStorage // In development, add Authorization header from localStorage
if (!isProduction) { if (!isProduction) {
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
fetchOptions.headers = { fetchOptions.headers = {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
}; };
} }
const response = await fetch(downloadUrl, fetchOptions); const response = await fetch(downloadUrl, fetchOptions);
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
throw new Error(`Download failed: ${response.status} - ${errorText}`); throw new Error(`Download failed: ${response.status} - ${errorText}`);
} }
const blob = await response.blob(); const blob = await response.blob();
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const contentDisposition = response.headers.get('Content-Disposition'); const contentDisposition = response.headers.get('Content-Disposition');
const filename = extractFilenameFromContentDisposition(contentDisposition); const filename = extractFilenameFromContentDisposition(contentDisposition);
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
downloadLink.href = url; downloadLink.href = url;
downloadLink.download = filename; downloadLink.download = filename;
@ -449,35 +452,35 @@ export async function downloadWorkNoteAttachment(attachmentId: string): Promise<
const downloadBaseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000'; const downloadBaseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
const downloadUrl = `${downloadBaseURL}/api/v1/workflows/work-notes/attachments/${attachmentId}/download`; const downloadUrl = `${downloadBaseURL}/api/v1/workflows/work-notes/attachments/${attachmentId}/download`;
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production'; const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
try { try {
// Build fetch options // Build fetch options
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
credentials: 'include', // Send cookies in production credentials: 'include', // Send cookies in production
}; };
// In development, add Authorization header from localStorage // In development, add Authorization header from localStorage
if (!isProduction) { if (!isProduction) {
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
fetchOptions.headers = { fetchOptions.headers = {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
}; };
} }
const response = await fetch(downloadUrl, fetchOptions); const response = await fetch(downloadUrl, fetchOptions);
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
throw new Error(`Download failed: ${response.status} - ${errorText}`); throw new Error(`Download failed: ${response.status} - ${errorText}`);
} }
const blob = await response.blob(); const blob = await response.blob();
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
// Get filename from Content-Disposition header or use default // Get filename from Content-Disposition header or use default
const contentDisposition = response.headers.get('Content-Disposition'); const contentDisposition = response.headers.get('Content-Disposition');
const filename = extractFilenameFromContentDisposition(contentDisposition); const filename = extractFilenameFromContentDisposition(contentDisposition);
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
downloadLink.href = url; downloadLink.href = url;
downloadLink.download = filename; downloadLink.download = filename;
@ -522,14 +525,14 @@ export async function updateWorkflowMultipart(requestId: string, updateData: any
...updateData, ...updateData,
deleteDocumentIds: deleteDocumentIds || [] deleteDocumentIds: deleteDocumentIds || []
}; };
const formData = new FormData(); const formData = new FormData();
formData.append('payload', JSON.stringify(payload)); formData.append('payload', JSON.stringify(payload));
formData.append('category', 'SUPPORTING'); formData.append('category', 'SUPPORTING');
if (files && files.length > 0) { if (files && files.length > 0) {
files.forEach(f => formData.append('files', f)); files.forEach(f => formData.append('files', f));
} }
const res = await apiClient.put(`/workflows/${requestId}/multipart`, formData, { const res = await apiClient.put(`/workflows/${requestId}/multipart`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }, headers: { 'Content-Type': 'multipart/form-data' },
}); });
@ -560,10 +563,10 @@ export async function updateAndSubmitWorkflow(requestId: string, workflowData: C
description: workflowData.description, description: workflowData.description,
priority: workflowData.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD', priority: workflowData.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD',
}; };
// Update workflow details // Update workflow details
await apiClient.put(`/workflows/${requestId}`, payload); await apiClient.put(`/workflows/${requestId}`, payload);
// If files provided, update documents (this would need backend support for updating documents) // If files provided, update documents (this would need backend support for updating documents)
// For now, we'll just submit the updated workflow // For now, we'll just submit the updated workflow
const res = await apiClient.patch(`/workflows/${requestId}/submit`); const res = await apiClient.patch(`/workflows/${requestId}/submit`);
@ -577,7 +580,7 @@ export async function updateBreachReason(levelId: string, breachReason: string):
const response = await apiClient.put(`/tat/breach-reason/${levelId}`, { const response = await apiClient.put(`/tat/breach-reason/${levelId}`, {
breachReason breachReason
}); });
if (!response.data.success) { if (!response.data.success) {
throw new Error(response.data.error || 'Failed to update breach reason'); throw new Error(response.data.error || 'Failed to update breach reason');
} }