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 },
// 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 },

View File

@ -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"

View 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`}
>

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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 {

View File

@ -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 };
}

View File

@ -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>

View File

@ -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');
}