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 },
|
{ 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 },
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user