@@ -272,7 +272,7 @@ export function AddSpectatorModal({
-
+
{/* Description */}
Add a spectator to this request. They will receive notifications but cannot approve or reject.
@@ -341,7 +341,7 @@ export function AddSpectatorModal({
{/* Action Buttons */}
-
+
@@ -1522,6 +1588,19 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
requestTitle={requestInfo.title}
existingParticipants={existingParticipants}
/>
+
+ {/* Add Approver Modal */}
+ {isInitiator && (
+
setShowAddApproverModal(false)}
+ onConfirm={handleAddApproverInternal}
+ requestIdDisplay={effectiveRequestId}
+ requestTitle={requestInfo.title}
+ existingParticipants={existingParticipants}
+ currentLevels={currentLevels}
+ />
+ )}
);
}
\ No newline at end of file
diff --git a/src/pages/CreateRequest/CreateRequest.tsx b/src/pages/CreateRequest/CreateRequest.tsx
index 0c41693..07f86d2 100644
--- a/src/pages/CreateRequest/CreateRequest.tsx
+++ b/src/pages/CreateRequest/CreateRequest.tsx
@@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { searchUsers, ensureUserExists, type UserSummary } from '@/services/userApi';
-import { createWorkflowMultipart, submitWorkflow, getWorkflowDetails, updateWorkflow, updateWorkflowMultipart } from '@/services/workflowApi';
+import { createWorkflowMultipart, submitWorkflow, getWorkflowDetails, updateWorkflow, updateWorkflowMultipart, getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -15,6 +15,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { motion, AnimatePresence } from 'framer-motion';
import { TemplateSelectionModal } from '@/components/modals/TemplateSelectionModal';
+import { FilePreview } from '@/components/common/FilePreview';
import {
ArrowLeft,
ArrowRight,
@@ -222,6 +223,7 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
const [loadingDraft, setLoadingDraft] = useState(isEditing);
const [existingDocuments, setExistingDocuments] = useState
([]); // Track documents from backend
const [documentsToDelete, setDocumentsToDelete] = useState([]); // Track document IDs to delete
+ const [previewDocument, setPreviewDocument] = useState<{ fileName: string; fileType: string; fileUrl: string; fileSize?: number; file?: File; documentId?: string } | null>(null);
// Validation modal states
const [validationModal, setValidationModal] = useState<{
@@ -2322,9 +2324,32 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
-
-
-
+ {/* Preview button - only for images and PDFs */}
+ {(() => {
+ 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');
+ })() && (
+ {
+ setPreviewDocument({
+ fileName: doc.originalFileName || doc.fileName || 'Document',
+ fileType: doc.fileType || doc.file_type || 'application/octet-stream',
+ fileUrl: getDocumentPreviewUrl(docId),
+ fileSize: Number(doc.fileSize || doc.file_size || 0),
+ documentId: docId
+ } as any);
+ }}
+ >
+
+
+ )}
-
-
-
+ {/* Preview button - only for images and PDFs */}
+ {(() => {
+ const type = (file.type || '').toLowerCase();
+ const name = (file.name || '').toLowerCase();
+ return type.includes('image') || type.includes('pdf') ||
+ name.endsWith('.jpg') || name.endsWith('.jpeg') ||
+ name.endsWith('.png') || name.endsWith('.gif') ||
+ name.endsWith('.pdf');
+ })() && (
+ {
+ // Create object URL for the file
+ const fileUrl = URL.createObjectURL(file);
+ setPreviewDocument({
+ fileName: file.name,
+ fileType: file.type || 'application/octet-stream',
+ fileUrl: fileUrl,
+ fileSize: file.size,
+ file: file
+ });
+ }}
+ >
+
+
+ )}
setShowTemplateModal(false)}
onSelectTemplate={handleTemplateSelection}
/>
+
+ {/* File Preview Modal */}
+ {previewDocument && (
+ {
+ // Clean up object URL to prevent memory leaks
+ if (previewDocument.fileUrl) {
+ URL.revokeObjectURL(previewDocument.fileUrl);
+ }
+ setPreviewDocument(null);
+ }}
+ onDownload={async () => {
+ // For new uploads (File object), download using browser download
+ if (previewDocument.file) {
+ const link = document.createElement('a');
+ link.href = previewDocument.fileUrl;
+ link.download = previewDocument.fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ } else if (previewDocument.documentId) {
+ // For existing documents from draft, use the API download function
+ await downloadDocument(previewDocument.documentId);
+ }
+ }}
+ attachmentId={previewDocument.documentId}
+ />
+ )}
{/* Validation Error Modal */}
diff --git a/src/pages/MyRequests/MyRequests.tsx b/src/pages/MyRequests/MyRequests.tsx
index 4b0a677..6e0f8ad 100644
--- a/src/pages/MyRequests/MyRequests.tsx
+++ b/src/pages/MyRequests/MyRequests.tsx
@@ -23,7 +23,7 @@ import workflowApi from '@/services/workflowApi';
// SLATracker removed - not needed on MyRequests (only for OpenRequests where user is approver)
interface MyRequestsProps {
- onViewRequest: (requestId: string, requestTitle?: string) => void;
+ onViewRequest: (requestId: string, requestTitle?: string, status?: string) => void;
dynamicRequests?: any[];
}
@@ -318,7 +318,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
>
onViewRequest(request.id, request.title)}
+ onClick={() => onViewRequest(request.id, request.title, request.status)}
>
diff --git a/src/pages/OpenRequests/OpenRequests.tsx b/src/pages/OpenRequests/OpenRequests.tsx
index 1525f1f..fc7fe40 100644
--- a/src/pages/OpenRequests/OpenRequests.tsx
+++ b/src/pages/OpenRequests/OpenRequests.tsx
@@ -89,53 +89,60 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+
+ const fetchRequests = async () => {
+ try {
+ setLoading(true);
+ const result = await workflowApi.listOpenForMe({ page: 1, limit: 50 });
+ const data = Array.isArray((result as any)?.data)
+ ? (result as any).data
+ : Array.isArray((result as any)?.data?.data)
+ ? (result as any).data.data
+ : Array.isArray(result as any)
+ ? (result as any)
+ : [];
+
+ const mapped: Request[] = data.map((r: any) => {
+ const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at;
+
+ return {
+ id: r.requestNumber || r.request_number || r.requestId,
+ requestId: r.requestId,
+ displayId: r.requestNumber || r.request_number || r.requestId,
+ title: r.title,
+ description: r.description,
+ status: ((r.status || '').toString().toUpperCase() === 'IN_PROGRESS') ? 'in-review' : 'pending',
+ priority: (r.priority || '').toString().toLowerCase(),
+ initiator: {
+ name: (r.initiator?.displayName || r.initiator?.email || 'β'),
+ avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase())
+ },
+ currentApprover: r.currentApprover ? {
+ name: (r.currentApprover.name || r.currentApprover.email || 'β'),
+ avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()),
+ sla: r.currentApprover.sla // β Backend-calculated SLA
+ } : undefined,
+ createdAt: createdAt || 'β',
+ approvalStep: r.currentLevel ? `Step ${r.currentLevel} of ${r.totalLevels || '?'}` : undefined,
+ department: r.department,
+ currentLevelSLA: r.currentLevelSLA, // β Backend-calculated current level SLA
+ };
+ });
+ setItems(mapped);
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ const handleRefresh = () => {
+ setRefreshing(true);
+ fetchRequests();
+ };
useEffect(() => {
- let mounted = true;
- (async () => {
- try {
- setLoading(true);
- const result = await workflowApi.listOpenForMe({ page: 1, limit: 50 });
- const data = Array.isArray((result as any)?.data)
- ? (result as any).data
- : Array.isArray((result as any)?.data?.data)
- ? (result as any).data.data
- : Array.isArray(result as any)
- ? (result as any)
- : [];
- if (!mounted) return;
- const mapped: Request[] = data.map((r: any) => {
- const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at;
-
- return {
- id: r.requestNumber || r.request_number || r.requestId,
- requestId: r.requestId,
- displayId: r.requestNumber || r.request_number || r.requestId,
- title: r.title,
- description: r.description,
- status: ((r.status || '').toString().toUpperCase() === 'IN_PROGRESS') ? 'in-review' : 'pending',
- priority: (r.priority || '').toString().toLowerCase(),
- initiator: {
- name: (r.initiator?.displayName || r.initiator?.email || 'β'),
- avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase())
- },
- currentApprover: r.currentApprover ? {
- name: (r.currentApprover.name || r.currentApprover.email || 'β'),
- avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()),
- sla: r.currentApprover.sla // β Backend-calculated SLA
- } : undefined,
- createdAt: createdAt || 'β',
- approvalStep: r.currentLevel ? `Step ${r.currentLevel} of ${r.totalLevels || '?'}` : undefined,
- department: r.department,
- currentLevelSLA: r.currentLevelSLA, // β Backend-calculated current level SLA
- };
- });
- setItems(mapped);
- } finally {
- if (mounted) setLoading(false);
- }
- })();
- return () => { mounted = false; };
+ fetchRequests();
}, []);
const filteredAndSortedRequests = useMemo(() => {
@@ -221,9 +228,15 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{loading ? 'Loadingβ¦' : `${filteredAndSortedRequests.length} open`}
requests
-
-
- Refresh
+
+
+ {refreshing ? 'Refreshing...' : 'Refresh'}