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:
parent
1d205a4038
commit
c97053e0e3
@ -73,13 +73,14 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
|
||||
{ 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/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)
|
||||
if (!isDealer) {
|
||||
items.push({ id: 'my-requests', label: 'My Requests', icon: User });
|
||||
}
|
||||
// if (!isDealer) {
|
||||
// items.push({ id: 'my-requests', label: 'My Requests', icon: User });
|
||||
// }
|
||||
|
||||
items.push(
|
||||
{ id: 'open-requests', label: 'Open Requests', icon: FileText },
|
||||
|
||||
@ -12,12 +12,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Switch } from '../ui/switch';
|
||||
import { Calendar } from '../ui/calendar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Calendar as CalendarIcon,
|
||||
Upload,
|
||||
X,
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Calendar as CalendarIcon,
|
||||
Upload,
|
||||
X,
|
||||
FileText,
|
||||
Check,
|
||||
Users
|
||||
@ -150,7 +150,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
|
||||
onChange={(e) => updateFormData('title', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">Description *</Label>
|
||||
<Textarea
|
||||
@ -215,9 +215,9 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
|
||||
<Label htmlFor="workflow-sequential" className="text-sm">Parallel</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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.'
|
||||
: 'All approvers will review the request simultaneously.'
|
||||
}
|
||||
@ -311,7 +311,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableUsers
|
||||
.filter(user =>
|
||||
.filter(user =>
|
||||
!formData.spectators.find(s => s.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">
|
||||
<Upload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Drag and drop files here, or click to browse
|
||||
click to browse
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
|
||||
@ -69,7 +69,7 @@ export function DocumentsStep({
|
||||
// Check file extension
|
||||
const fileName = file.name.toLowerCase();
|
||||
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
|
||||
|
||||
|
||||
if (!documentPolicy.allowedFileTypes.includes(fileExtension)) {
|
||||
validationErrors.push({
|
||||
fileName: file.name,
|
||||
@ -111,16 +111,16 @@ export function DocumentsStep({
|
||||
const type = (doc.fileType || doc.file_type || '').toLowerCase();
|
||||
const name = (doc.originalFileName || doc.fileName || '').toLowerCase();
|
||||
return type.includes('image') || type.includes('pdf') ||
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') ||
|
||||
name.endsWith('.png') || name.endsWith('.gif') ||
|
||||
name.endsWith('.pdf');
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') ||
|
||||
name.endsWith('.png') || name.endsWith('.gif') ||
|
||||
name.endsWith('.pdf');
|
||||
} else {
|
||||
const type = (doc.type || '').toLowerCase();
|
||||
const name = (doc.name || '').toLowerCase();
|
||||
return type.includes('image') || type.includes('pdf') ||
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') ||
|
||||
name.endsWith('.png') || name.endsWith('.gif') ||
|
||||
name.endsWith('.pdf');
|
||||
name.endsWith('.jpg') || name.endsWith('.jpeg') ||
|
||||
name.endsWith('.png') || name.endsWith('.gif') ||
|
||||
name.endsWith('.pdf');
|
||||
}
|
||||
};
|
||||
|
||||
@ -160,7 +160,7 @@ export function DocumentsStep({
|
||||
<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>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Drag and drop files here, or click to browse
|
||||
click to browse
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
@ -172,10 +172,10 @@ export function DocumentsStep({
|
||||
ref={fileInputRef}
|
||||
data-testid="documents-file-input"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
type="button"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
data-testid="documents-browse-button"
|
||||
>
|
||||
@ -206,7 +206,7 @@ export function DocumentsStep({
|
||||
const docId = doc.documentId || doc.document_id || '';
|
||||
const isDeleted = documentsToDelete.includes(docId);
|
||||
if (isDeleted) return null;
|
||||
|
||||
|
||||
return (
|
||||
<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">
|
||||
@ -222,9 +222,9 @@ export function DocumentsStep({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{canPreview(doc, true) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onPreviewDocument(doc, true)}
|
||||
data-testid={`documents-existing-${docId}-preview`}
|
||||
>
|
||||
@ -276,9 +276,9 @@ export function DocumentsStep({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{canPreview(file, false) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onPreviewDocument(file, false)}
|
||||
data-testid={`documents-new-${index}-preview`}
|
||||
>
|
||||
|
||||
@ -52,18 +52,18 @@ export function TemplateSelectionStep({
|
||||
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
|
||||
// {
|
||||
// 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;
|
||||
|
||||
|
||||
@ -1,22 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
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 { 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 { getTemplates, WorkflowTemplate, getCachedTemplates } from '@/services/workflowTemplateApi';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function AdminTemplatesList() {
|
||||
@ -25,8 +15,6 @@ export function AdminTemplatesList() {
|
||||
// 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 {
|
||||
@ -49,22 +37,6 @@ export function AdminTemplatesList() {
|
||||
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()) ||
|
||||
@ -152,7 +124,7 @@ export function AdminTemplatesList() {
|
||||
</Badge>
|
||||
</div>
|
||||
<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}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@ -181,14 +153,6 @@ export function AdminTemplatesList() {
|
||||
<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>
|
||||
@ -196,33 +160,6 @@ export function AdminTemplatesList() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,8 +11,6 @@ import {
|
||||
validateApproversForSubmission,
|
||||
} from '../utils/payloadBuilders';
|
||||
import {
|
||||
createAndSubmitWorkflow,
|
||||
updateAndSubmitWorkflow,
|
||||
createWorkflow,
|
||||
updateWorkflowRequest,
|
||||
} from '../services/createRequestService';
|
||||
@ -59,14 +57,15 @@ export function useCreateRequestSubmission({
|
||||
|
||||
try {
|
||||
if (isEditing && editRequestId) {
|
||||
// Update existing workflow
|
||||
// Update existing workflow with isDraft: false (Submit)
|
||||
const updatePayload = buildUpdatePayload(
|
||||
formData,
|
||||
user,
|
||||
documentsToDelete
|
||||
documentsToDelete,
|
||||
false
|
||||
);
|
||||
|
||||
await updateAndSubmitWorkflow(
|
||||
await updateWorkflowRequest(
|
||||
editRequestId,
|
||||
updatePayload,
|
||||
documents,
|
||||
@ -85,14 +84,15 @@ export function useCreateRequestSubmission({
|
||||
template: selectedTemplate,
|
||||
});
|
||||
} else {
|
||||
// Create new workflow
|
||||
// Create new workflow with isDraft: false (Submit)
|
||||
const createPayload = buildCreatePayload(
|
||||
formData,
|
||||
selectedTemplate,
|
||||
user
|
||||
user,
|
||||
false
|
||||
);
|
||||
|
||||
const result = await createAndSubmitWorkflow(createPayload, documents);
|
||||
const result = await createWorkflow(createPayload, documents);
|
||||
|
||||
// Show toast after backend confirmation
|
||||
toast.success('Request Submitted Successfully!', {
|
||||
@ -133,11 +133,12 @@ export function useCreateRequestSubmission({
|
||||
|
||||
try {
|
||||
if (isEditing && editRequestId) {
|
||||
// Update existing draft
|
||||
// Update existing draft with isDraft: true
|
||||
const updatePayload = buildUpdatePayload(
|
||||
formData,
|
||||
user,
|
||||
documentsToDelete
|
||||
documentsToDelete,
|
||||
true
|
||||
);
|
||||
|
||||
await updateWorkflowRequest(
|
||||
@ -158,11 +159,12 @@ export function useCreateRequestSubmission({
|
||||
template: selectedTemplate,
|
||||
});
|
||||
} else {
|
||||
// Create new draft
|
||||
// Create new draft with isDraft: true
|
||||
const createPayload = buildCreatePayload(
|
||||
formData,
|
||||
selectedTemplate,
|
||||
user
|
||||
user,
|
||||
true
|
||||
);
|
||||
|
||||
const result = await createWorkflow(createPayload, documents);
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
|
||||
import {
|
||||
createWorkflowMultipart,
|
||||
submitWorkflow,
|
||||
updateWorkflow,
|
||||
updateWorkflowMultipart,
|
||||
} from '@/services/workflowApi';
|
||||
@ -14,7 +13,7 @@ import {
|
||||
} 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(
|
||||
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(
|
||||
requestId: string,
|
||||
@ -51,36 +50,3 @@ export async function updateWorkflowRequest(
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -67,6 +67,7 @@ export interface CreateWorkflowPayload {
|
||||
email: string;
|
||||
}>;
|
||||
participants: Participant[];
|
||||
isDraft?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateWorkflowPayload {
|
||||
@ -76,6 +77,7 @@ export interface UpdateWorkflowPayload {
|
||||
approvalLevels: ApprovalLevel[];
|
||||
participants: Participant[];
|
||||
deleteDocumentIds?: string[];
|
||||
isDraft?: boolean;
|
||||
}
|
||||
|
||||
export interface ValidationModalState {
|
||||
|
||||
@ -17,16 +17,9 @@ import { buildApprovalLevels } from './approvalLevelBuilders';
|
||||
export function buildCreatePayload(
|
||||
formData: FormData,
|
||||
selectedTemplate: RequestTemplate | null,
|
||||
_user: any
|
||||
_user: any,
|
||||
isDraft: boolean = false
|
||||
): 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 {
|
||||
templateId: selectedTemplate?.id || null,
|
||||
templateType: selectedTemplate?.id === 'custom' ? 'CUSTOM' : 'TEMPLATE',
|
||||
@ -38,16 +31,17 @@ export function buildCreatePayload(
|
||||
userId: a?.userId || '',
|
||||
email: a?.email || '',
|
||||
name: a?.name,
|
||||
tat: a?.tat || '',
|
||||
tat: a?.tat || 24,
|
||||
tatType: a?.tatType || 'hours',
|
||||
})),
|
||||
spectators: filteredSpectators.map((s: any) => ({
|
||||
spectators: (formData.spectators || []).map((s: any) => ({
|
||||
userId: s?.userId || '',
|
||||
name: s?.name || '',
|
||||
email: s?.email || '',
|
||||
})),
|
||||
ccList: [], // Auto-generated by backend
|
||||
participants: [], // Auto-generated by backend from approvers and spectators
|
||||
isDraft,
|
||||
};
|
||||
}
|
||||
|
||||
@ -58,7 +52,8 @@ export function buildCreatePayload(
|
||||
export function buildUpdatePayload(
|
||||
formData: FormData,
|
||||
_user: any,
|
||||
documentsToDelete: string[]
|
||||
documentsToDelete: string[],
|
||||
isDraft: boolean = false
|
||||
): UpdateWorkflowPayload {
|
||||
const approvalLevels = buildApprovalLevels(
|
||||
formData.approvers || [],
|
||||
@ -72,6 +67,7 @@ export function buildUpdatePayload(
|
||||
approvalLevels,
|
||||
participants: [], // Auto-generated by backend from approval levels
|
||||
deleteDocumentIds: documentsToDelete.length > 0 ? documentsToDelete : undefined,
|
||||
isDraft,
|
||||
};
|
||||
}
|
||||
|
||||
@ -84,7 +80,7 @@ export function validateApproversForSubmission(
|
||||
approverCount: number
|
||||
): { valid: boolean; message?: string } {
|
||||
const approversToCheck = approvers.slice(0, approverCount);
|
||||
|
||||
|
||||
// Check if all approvers have valid emails
|
||||
const hasMissingEmails = approversToCheck.some(
|
||||
(a: any) => !a?.email || !a.email.trim()
|
||||
@ -112,4 +108,3 @@ export function validateApproversForSubmission(
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ export function TATBreachReport({
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@ -122,7 +122,7 @@ export function TATBreachReport({
|
||||
params.set('approver', req.approverId!);
|
||||
params.set('approverType', 'current');
|
||||
params.set('slaCompliance', 'breached');
|
||||
|
||||
|
||||
if (dateRange) params.set('dateRange', dateRange);
|
||||
if (customStartDate) params.set('startDate', customStartDate.toISOString());
|
||||
if (customEndDate) params.set('endDate', customEndDate.toISOString());
|
||||
@ -164,11 +164,10 @@ export function TATBreachReport({
|
||||
<td className="py-3 px-4">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs font-medium ${
|
||||
req.priority === 'express'
|
||||
className={`text-xs font-medium ${req.priority === 'express'
|
||||
? 'bg-orange-100 text-orange-800 border-orange-200'
|
||||
: 'bg-blue-100 text-blue-800 border-blue-200'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{req.priority}
|
||||
</Badge>
|
||||
|
||||
@ -25,6 +25,7 @@ export interface CreateWorkflowFromFormPayload {
|
||||
approvers: ApproverFormItem[];
|
||||
spectators?: ParticipantItem[];
|
||||
ccList?: ParticipantItem[];
|
||||
isDraft?: boolean; // Added isDraft to the payload interface
|
||||
}
|
||||
|
||||
// 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
|
||||
approvalLevels,
|
||||
participants: participants.length ? participants : undefined,
|
||||
isDraft: form.isDraft, // Added isDraft to the payload
|
||||
};
|
||||
|
||||
const res = await apiClient.post('/workflows', payload);
|
||||
@ -116,15 +118,16 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
|
||||
title: form.title,
|
||||
description: form.description,
|
||||
priority: form.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD',
|
||||
isDraft: form.isDraft, // Added isDraft to the payload
|
||||
// Simplified approvers format - only email and tatHours required
|
||||
approvers: Array.from({ length: form.approverCount || 1 }, (_, i) => {
|
||||
const a = form.approvers[i] || ({} as any);
|
||||
const tat = typeof a.tat === 'number' ? a.tat : 0;
|
||||
|
||||
|
||||
if (!a.email || !a.email.trim()) {
|
||||
throw new Error(`Email is required for approver at level ${i + 1}.`);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
email: a.email,
|
||||
tat: tat,
|
||||
@ -132,14 +135,14 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
// Add spectators if any (simplified - only email required)
|
||||
if (form.spectators && form.spectators.length > 0) {
|
||||
payload.spectators = form.spectators
|
||||
.filter((s: any) => s?.email)
|
||||
.map((s: any) => ({ email: s.email }));
|
||||
}
|
||||
|
||||
|
||||
// Note: participants array is auto-generated by backend from approvers and spectators
|
||||
// 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 } = {}) {
|
||||
const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } = params;
|
||||
const res = await apiClient.get('/workflows', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
status,
|
||||
priority,
|
||||
const res = await apiClient.get('/workflows', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
status,
|
||||
priority,
|
||||
templateType,
|
||||
department,
|
||||
initiator,
|
||||
approver,
|
||||
slaCompliance,
|
||||
dateRange,
|
||||
startDate,
|
||||
endDate
|
||||
}
|
||||
department,
|
||||
initiator,
|
||||
approver,
|
||||
slaCompliance,
|
||||
dateRange,
|
||||
startDate,
|
||||
endDate
|
||||
}
|
||||
});
|
||||
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
|
||||
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 res = await apiClient.get('/workflows/participant-requests', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
status,
|
||||
const res = await apiClient.get('/workflows/participant-requests', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
status,
|
||||
priority,
|
||||
templateType,
|
||||
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
|
||||
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 res = await apiClient.get('/workflows/my', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
status,
|
||||
const res = await apiClient.get('/workflows/my', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
status,
|
||||
priority,
|
||||
department,
|
||||
initiator,
|
||||
@ -224,7 +227,7 @@ export async function listMyWorkflows(params: { page?: number; limit?: number; s
|
||||
dateRange,
|
||||
startDate,
|
||||
endDate
|
||||
}
|
||||
}
|
||||
});
|
||||
// Response structure: { success, data: { data: [...], pagination: {...} } }
|
||||
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
|
||||
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 res = await apiClient.get('/workflows/my-initiated', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
status,
|
||||
const res = await apiClient.get('/workflows/my-initiated', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
status,
|
||||
priority,
|
||||
templateType,
|
||||
department,
|
||||
@ -249,7 +252,7 @@ export async function listMyInitiatedWorkflows(params: { page?: number; limit?:
|
||||
dateRange,
|
||||
startDate,
|
||||
endDate
|
||||
}
|
||||
}
|
||||
});
|
||||
// Response structure: { success, data: { data: [...], pagination: {...} } }
|
||||
return {
|
||||
@ -304,22 +307,22 @@ export async function addApprover(requestId: string, email: string) {
|
||||
}
|
||||
|
||||
export async function addApproverAtLevel(
|
||||
requestId: string,
|
||||
email: string,
|
||||
tatHours: number,
|
||||
requestId: string,
|
||||
email: string,
|
||||
tatHours: number,
|
||||
level: number
|
||||
) {
|
||||
const res = await apiClient.post(`/workflows/${requestId}/approvers/at-level`, {
|
||||
email,
|
||||
tatHours,
|
||||
level
|
||||
const res = await apiClient.post(`/workflows/${requestId}/approvers/at-level`, {
|
||||
email,
|
||||
tatHours,
|
||||
level
|
||||
});
|
||||
return res.data?.data || res.data;
|
||||
}
|
||||
|
||||
export async function skipApprover(requestId: string, levelId: string, reason?: string) {
|
||||
const res = await apiClient.post(`/workflows/${requestId}/approvals/${levelId}/skip`, {
|
||||
reason
|
||||
const res = await apiClient.post(`/workflows/${requestId}/approvals/${levelId}/skip`, {
|
||||
reason
|
||||
});
|
||||
return res.data?.data || res.data;
|
||||
}
|
||||
@ -376,7 +379,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
|
||||
if (!contentDisposition) {
|
||||
return 'download';
|
||||
}
|
||||
|
||||
|
||||
// Try to extract from filename* first (RFC 5987 encoded) - preferred for non-ASCII
|
||||
const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
|
||||
if (filenameStarMatch && filenameStarMatch[1]) {
|
||||
@ -386,7 +389,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
|
||||
// If decoding fails, fall back to regular filename
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fallback to regular filename (for ASCII-only filenames)
|
||||
const filenameMatch = contentDisposition.match(/filename="?([^";]+)"?/);
|
||||
if (filenameMatch && filenameMatch.length > 1 && filenameMatch[1]) {
|
||||
@ -396,7 +399,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
|
||||
const extracted = parts[0]?.trim();
|
||||
return extracted || '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 downloadUrl = `${baseURL}/api/v1/workflows/documents/${documentId}/download`;
|
||||
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||
|
||||
|
||||
try {
|
||||
// Build fetch options
|
||||
const fetchOptions: RequestInit = {
|
||||
credentials: 'include', // Send cookies in production
|
||||
};
|
||||
|
||||
|
||||
// In development, add Authorization header from localStorage
|
||||
if (!isProduction) {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
fetchOptions.headers = {
|
||||
'Authorization': `Bearer ${token}`
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const response = await fetch(downloadUrl, fetchOptions);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Download failed: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
const filename = extractFilenameFromContentDisposition(contentDisposition);
|
||||
|
||||
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = url;
|
||||
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 downloadUrl = `${downloadBaseURL}/api/v1/workflows/work-notes/attachments/${attachmentId}/download`;
|
||||
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
|
||||
|
||||
|
||||
try {
|
||||
// Build fetch options
|
||||
const fetchOptions: RequestInit = {
|
||||
credentials: 'include', // Send cookies in production
|
||||
};
|
||||
|
||||
|
||||
// In development, add Authorization header from localStorage
|
||||
if (!isProduction) {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
fetchOptions.headers = {
|
||||
'Authorization': `Bearer ${token}`
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const response = await fetch(downloadUrl, fetchOptions);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Download failed: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
|
||||
// Get filename from Content-Disposition header or use default
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
const filename = extractFilenameFromContentDisposition(contentDisposition);
|
||||
|
||||
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = filename;
|
||||
@ -522,14 +525,14 @@ export async function updateWorkflowMultipart(requestId: string, updateData: any
|
||||
...updateData,
|
||||
deleteDocumentIds: deleteDocumentIds || []
|
||||
};
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
formData.append('category', 'SUPPORTING');
|
||||
if (files && files.length > 0) {
|
||||
files.forEach(f => formData.append('files', f));
|
||||
}
|
||||
|
||||
|
||||
const res = await apiClient.put(`/workflows/${requestId}/multipart`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
@ -560,10 +563,10 @@ export async function updateAndSubmitWorkflow(requestId: string, workflowData: C
|
||||
description: workflowData.description,
|
||||
priority: workflowData.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD',
|
||||
};
|
||||
|
||||
|
||||
// Update workflow details
|
||||
await apiClient.put(`/workflows/${requestId}`, payload);
|
||||
|
||||
|
||||
// If files provided, update documents (this would need backend support for updating documents)
|
||||
// For now, we'll just submit the updated workflow
|
||||
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}`, {
|
||||
breachReason
|
||||
});
|
||||
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.error || 'Failed to update breach reason');
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user