import apiClient from './authApi'; export type PriorityUi = 'standard' | 'express'; export interface ApproverFormItem { email: string; name?: string; tat?: number | ''; tatType?: 'hours' | 'days'; } export interface ParticipantItem { id?: string; name: string; email: string; } export interface CreateWorkflowFromFormPayload { templateId?: string | null; templateType: 'CUSTOM' | 'TEMPLATE'; title: string; description: string; priorityUi: PriorityUi; approverCount: number; approvers: ApproverFormItem[]; spectators?: ParticipantItem[]; ccList?: ParticipantItem[]; } // Utility to generate a RFC4122 v4 UUID (fallback if crypto.randomUUID not available) function generateUuid(): string { if (typeof crypto !== 'undefined' && (crypto as any).randomUUID) { return (crypto as any).randomUUID(); } // Fallback return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } export interface CreateWorkflowResponse { id: string; } export async function createWorkflowFromForm(form: CreateWorkflowFromFormPayload): Promise { // Map UI priority to API enum const priority = form.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD'; // Build approval levels to match backend schema const approvalLevels = Array.from({ length: form.approverCount || 1 }, (_, i) => { const idx = i; const a = form.approvers[idx] || {} as ApproverFormItem; const levelNumber = idx + 1; const tatRaw = a.tat ?? ''; let tatHours = 0; if (typeof tatRaw === 'number') { tatHours = a.tatType === 'days' ? tatRaw * 24 : tatRaw; } const approverEmail = a.email || ''; const approverName = (a.name && a.name.trim()) || approverEmail.split('@')[0] || `Approver ${levelNumber}`; return { levelNumber, levelName: `Level ${levelNumber}`, approverId: generateUuid(), approverEmail, approverName, tatHours: tatHours > 0 ? tatHours : 24, isFinalApprover: levelNumber === (form.approverCount || 1), }; }); // Participants -> spectators and ccList const participants = [ ...(form.spectators || []).map(p => ({ userId: generateUuid(), userEmail: p.email, userName: p.name || p.email.split('@')[0] || 'Spectator', participantType: 'SPECTATOR' as const, canComment: true, canViewDocuments: true, canDownloadDocuments: false, notificationEnabled: true, })), ...(form.ccList || []).map(p => ({ userId: generateUuid(), userEmail: p.email, userName: p.name || p.email.split('@')[0] || 'CC', participantType: 'CONSULTATION' as const, canComment: false, canViewDocuments: true, canDownloadDocuments: false, notificationEnabled: true, })), ]; const payload = { templateType: form.templateType, title: form.title, description: form.description, priority, // STANDARD | EXPRESS approvalLevels, participants: participants.length ? participants : undefined, }; const res = await apiClient.post('/workflows', payload); const data = (res.data?.data || res.data) as any; return { id: data.id || data.workflowId || '' }; } export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayload, files: File[], category: 'SUPPORTING' | 'APPROVAL' | 'REFERENCE' | 'FINAL' | 'OTHER' = 'SUPPORTING') { // Simplified payload - backend handles user lookup and participant generation const payload: any = { templateType: form.templateType, title: form.title, description: form.description, priority: form.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD', // 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, tatType: a.tatType || 'hours', }; }), }; // 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 const formData = new FormData(); formData.append('payload', JSON.stringify(payload)); formData.append('category', category); files.forEach(f => formData.append('files', f)); const res = await apiClient.post('/workflows/multipart', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); const data = res.data?.data || res.data; return { id: data?.requestId } as any; } 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, templateType, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } }); return res.data?.data || res.data; } // List requests where user is a participant (not initiator) - for regular users' "All Requests" page // 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, priority, templateType, department, initiator, approver, approverType, slaCompliance, dateRange, startDate, endDate } }); // Response structure: { success, data: { data: [...], pagination: {...} } } return { data: res.data?.data?.data || res.data?.data || [], pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 } }; } // DEPRECATED: Use listParticipantRequests instead // 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, priority, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } }); // Response structure: { success, data: { data: [...], pagination: {...} } } return { data: res.data?.data?.data || res.data?.data || [], pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 } }; } // 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, priority, templateType, department, slaCompliance, dateRange, startDate, endDate } }); // Response structure: { success, data: { data: [...], pagination: {...} } } return { data: res.data?.data?.data || res.data?.data || [], pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 } }; } export async function listOpenForMe(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; sortBy?: string; sortOrder?: string } = {}) { const { page = 1, limit = 20, search, status, priority, templateType, sortBy, sortOrder } = params; const res = await apiClient.get('/workflows/open-for-me', { params: { page, limit, search, status, priority, templateType, sortBy, sortOrder } }); // Response structure: { success, data: { data: [...], pagination: {...} } } return { data: res.data?.data?.data || res.data?.data || [], pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 } }; } export async function listClosedByMe(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; sortBy?: string; sortOrder?: string } = {}) { const { page = 1, limit = 20, search, status, priority, templateType, sortBy, sortOrder } = params; const res = await apiClient.get('/workflows/closed-by-me', { params: { page, limit, search, status, priority, templateType, sortBy, sortOrder } }); // Response structure: { success, data: { data: [...], pagination: {...} } } return { data: res.data?.data?.data || res.data?.data || [], pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 } }; } export async function getWorkflowDetails(requestId: string, _workflowType?: string) { const res = await apiClient.get(`/workflows/${requestId}/details`); return res.data?.data || res.data; } export async function getWorkNotes(requestId: string) { const res = await apiClient.get(`/workflows/${requestId}/work-notes`); return res.data?.data || res.data; } export async function createWorkNoteMultipart(requestId: string, payload: any, files: File[] = []) { const formData = new FormData(); formData.append('payload', JSON.stringify(payload || {})); files.forEach(f => formData.append('files', f)); const res = await apiClient.post(`/workflows/${requestId}/work-notes`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); return res.data?.data || res.data; } export async function addApprover(requestId: string, email: string) { const res = await apiClient.post(`/workflows/${requestId}/participants/approver`, { email }); return res.data?.data || res.data; } export async function addApproverAtLevel( requestId: string, email: string, tatHours: number, level: number ) { 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 }); return res.data?.data || res.data; } export async function addSpectator(requestId: string, email: string) { const res = await apiClient.post(`/workflows/${requestId}/participants/spectator`, { email }); return res.data?.data || res.data; } export async function pauseWorkflow( requestId: string, levelId: string | null, reason: string, resumeDate: Date ) { const res = await apiClient.post(`/workflows/${requestId}/pause`, { levelId, reason, resumeDate: resumeDate.toISOString() }); return res.data?.data || res.data; } export async function resumeWorkflow(requestId: string, notes?: string) { const res = await apiClient.post(`/workflows/${requestId}/resume`, { notes }); return res.data?.data || res.data; } export async function retriggerPause(requestId: string) { const res = await apiClient.post(`/workflows/${requestId}/pause/retrigger`); return res.data?.data || res.data; } export async function getPauseDetails(requestId: string) { const res = await apiClient.get(`/workflows/${requestId}/pause`); return res.data?.data || res.data; } export function getWorkNoteAttachmentPreviewUrl(attachmentId: string): string { const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000'; return `${baseURL}/api/v1/workflows/work-notes/attachments/${attachmentId}/preview`; } export function getDocumentPreviewUrl(documentId: string): string { const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000'; return `${baseURL}/api/v1/workflows/documents/${documentId}/preview`; } /** * Extract filename from Content-Disposition header * Handles both formats: filename="name" and filename*=UTF-8''encoded */ function extractFilenameFromContentDisposition(contentDisposition: string | null): string { 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]) { try { return decodeURIComponent(filenameStarMatch[1]); } catch { // 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]) { // Remove quotes and extract only the filename part (before any semicolon) const filenameValue: string = filenameMatch[1]; const parts = filenameValue.replace(/^"|"$/g, '').split(';'); const extracted = parts[0]?.trim(); return extracted || 'download'; } return 'download'; } export async function downloadDocument(documentId: string): Promise { 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; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); window.URL.revokeObjectURL(url); } catch (error) { console.error('[Download] Failed:', error); throw error; } } 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; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); window.URL.revokeObjectURL(url); } catch (error) { console.error('[Download] Failed:', error); throw error; } } export default { createWorkflowFromForm, createWorkflowMultipart, listWorkflows, // Admin: All organization requests listParticipantRequests, // Regular users: Participant requests only (not initiator) listMyWorkflows, // DEPRECATED: Use listParticipantRequests listMyInitiatedWorkflows, // Regular users: Initiator requests only listOpenForMe, listClosedByMe, submitWorkflow, getWorkflowDetails, getWorkNotes, createWorkNoteMultipart, }; export async function submitWorkflow(requestId: string) { const res = await apiClient.patch(`/workflows/${requestId}/submit`); return res.data?.data || res.data; } export async function updateWorkflow(requestId: string, updateData: any) { const res = await apiClient.put(`/workflows/${requestId}`, updateData); return res.data?.data || res.data; } export async function updateWorkflowMultipart(requestId: string, updateData: any, files?: File[], deleteDocumentIds?: string[]) { const payload = { ...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' }, }); return res.data?.data || res.data; } export async function approveLevel(requestId: string, levelId: string, comments?: string) { const res = await apiClient.patch(`/workflows/${requestId}/approvals/${levelId}/approve`, { action: 'APPROVE', comments: comments || '' }); return res.data?.data || res.data; } export async function rejectLevel(requestId: string, levelId: string, rejectionReason?: string, comments?: string) { const res = await apiClient.patch(`/workflows/${requestId}/approvals/${levelId}/reject`, { action: 'REJECT', rejectionReason: rejectionReason || '', comments: comments || '' }); return res.data?.data || res.data; } export async function updateAndSubmitWorkflow(requestId: string, workflowData: CreateWorkflowFromFormPayload, _files?: File[]) { // First update the workflow const payload: any = { title: workflowData.title, 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`); return res.data?.data || res.data; } /** * Update breach reason for a TAT alert */ export async function updateBreachReason(levelId: string, breachReason: string): Promise { 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'); } } // Also export in default for convenience // Note: keeping separate named export above for tree-shaking