/** * DMSPushModal Component * Modal for Step 6: Push to DMS Verification * Allows user to verify completion details and expenses before pushing to DMS */ import { useState, useMemo, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Receipt, DollarSign, TriangleAlert, Activity, CheckCircle2, Download, Eye, Loader2, } from 'lucide-react'; import { toast } from 'sonner'; import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi'; import '@/components/common/FilePreview/FilePreview.css'; import './DMSPushModal.css'; interface ExpenseItem { description: string; amount: number; } interface CompletionDetails { activityCompletionDate?: string; numberOfParticipants?: number; closedExpenses?: ExpenseItem[]; totalClosedExpenses?: number; completionDescription?: string; } interface IODetails { ioNumber?: string; blockedAmount?: number; availableBalance?: number; remainingBalance?: number; } interface CompletionDocuments { completionDocuments?: Array<{ name: string; url?: string; id?: string; }>; activityPhotos?: Array<{ name: string; url?: string; id?: string; }>; invoicesReceipts?: Array<{ name: string; url?: string; id?: string; }>; attendanceSheet?: { name: string; url?: string; id?: string; }; } interface DMSPushModalProps { isOpen: boolean; onClose: () => void; onPush: (comments: string) => Promise; completionDetails?: CompletionDetails | null; ioDetails?: IODetails | null; completionDocuments?: CompletionDocuments | null; requestTitle?: string; requestNumber?: string; } export function DMSPushModal({ isOpen, onClose, onPush, completionDetails, ioDetails, completionDocuments, requestTitle, requestNumber, }: DMSPushModalProps) { const [comments, setComments] = useState(''); const [submitting, setSubmitting] = useState(false); const [previewDocument, setPreviewDocument] = useState<{ name: string; url: string; type?: string; size?: number; } | null>(null); const [previewLoading, setPreviewLoading] = useState(false); const commentsChars = comments.length; const maxCommentsChars = 500; // Calculate total closed expenses const totalClosedExpenses = useMemo(() => { if (completionDetails?.totalClosedExpenses) { return completionDetails.totalClosedExpenses; } if (completionDetails?.closedExpenses && Array.isArray(completionDetails.closedExpenses)) { return completionDetails.closedExpenses.reduce((sum, item) => { const amount = typeof item === 'object' ? (item.amount || 0) : 0; return sum + (Number(amount) || 0); }, 0); } return 0; }, [completionDetails]); // Format date const formatDate = (dateString?: string) => { if (!dateString) return '—'; try { const date = new Date(dateString); return date.toLocaleDateString('en-IN', { year: 'numeric', month: 'long', day: 'numeric', }); } catch { return dateString; } }; // Format currency const formatCurrency = (amount: number) => { return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; // Check if document can be previewed const canPreviewDocument = (doc: { name: string; url?: string; id?: string }): boolean => { if (!doc.name) return false; const name = doc.name.toLowerCase(); return name.endsWith('.pdf') || name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') || name.endsWith('.gif') || name.endsWith('.webp'); }; // Handle document preview - fetch as blob to avoid CSP issues const handlePreviewDocument = async (doc: { name: string; url?: string; id?: string }) => { if (!doc.id) { toast.error('Document preview not available - document ID missing'); return; } setPreviewLoading(true); try { const previewUrl = getDocumentPreviewUrl(doc.id); // Determine file type from name const fileName = doc.name.toLowerCase(); const isPDF = fileName.endsWith('.pdf'); const isImage = fileName.match(/\.(jpg|jpeg|png|gif|webp)$/i); // Fetch document as a blob to create a blob URL (CSP compliant) const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production'; const token = isProduction ? null : localStorage.getItem('accessToken'); const headers: HeadersInit = { 'Accept': isPDF ? 'application/pdf' : '*/*' }; if (!isProduction && token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(previewUrl, { headers, credentials: 'include', mode: 'cors' }); if (!response.ok) { throw new Error(`Failed to load file: ${response.status} ${response.statusText}`); } const blob = await response.blob(); if (blob.size === 0) { throw new Error('File is empty or could not be loaded'); } // Create blob URL (CSP compliant - uses 'blob:' protocol) const blobUrl = window.URL.createObjectURL(blob); setPreviewDocument({ name: doc.name, url: blobUrl, type: blob.type || (isPDF ? 'application/pdf' : isImage ? 'image' : undefined), size: blob.size, }); } catch (error) { console.error('Failed to load document preview:', error); toast.error('Failed to load document preview'); } finally { setPreviewLoading(false); } }; // Cleanup blob URLs on unmount useEffect(() => { return () => { if (previewDocument?.url && previewDocument.url.startsWith('blob:')) { window.URL.revokeObjectURL(previewDocument.url); } }; }, [previewDocument]); const handleSubmit = async () => { if (!comments.trim()) { toast.error('Please provide comments before pushing to DMS'); return; } try { setSubmitting(true); await onPush(comments.trim()); handleReset(); onClose(); } catch (error) { console.error('Failed to push to DMS:', error); toast.error('Failed to push to DMS. Please try again.'); } finally { setSubmitting(false); } }; const handleReset = () => { setComments(''); }; const handleClose = () => { if (!submitting) { handleReset(); onClose(); } }; return (
Push to DMS - Verification Review completion details and expenses before pushing to DMS for e-invoice generation
{/* Request Info Card - Grid layout */}
Workflow Step: Requestor Claim Approval
{requestNumber && (
Request Number:

{requestNumber}

)}
Title:

{requestTitle || '—'}

Action: PUSH TO DMS
{/* Grid layout for all three cards on larger screens */}
{/* Completion Details Card */} {completionDetails && ( Completion Details Review activity completion information {completionDetails.activityCompletionDate && (
Activity Completion Date: {formatDate(completionDetails.activityCompletionDate)}
)} {completionDetails.numberOfParticipants !== undefined && (
Number of Participants: {completionDetails.numberOfParticipants}
)} {completionDetails.completionDescription && (

Completion Description:

{completionDetails.completionDescription}

)}
)} {/* IO Details Card */} {ioDetails && ioDetails.ioNumber && ( IO Details Internal Order information for budget reference
IO Number: {ioDetails.ioNumber}
{ioDetails.blockedAmount !== undefined && ioDetails.blockedAmount > 0 && (
Blocked Amount: {formatCurrency(ioDetails.blockedAmount)}
)} {ioDetails.remainingBalance !== undefined && ioDetails.remainingBalance !== null && (
Remaining Balance: {formatCurrency(ioDetails.remainingBalance)}
)}
)} {/* Expense Breakdown Card */} {completionDetails?.closedExpenses && completionDetails.closedExpenses.length > 0 && ( Expense Breakdown Review closed expenses before pushing to DMS
{completionDetails.closedExpenses.map((expense, index) => (

{expense.description || `Expense ${index + 1}`}

{formatCurrency(typeof expense === 'object' ? (expense.amount || 0) : 0)}

))}
Total: {formatCurrency(totalClosedExpenses)}
)}
{/* Completion Documents Section */} {completionDocuments && (
{/* Completion Documents */} {completionDocuments.completionDocuments && completionDocuments.completionDocuments.length > 0 && (

Completion Documents

{completionDocuments.completionDocuments.length} file(s)
{completionDocuments.completionDocuments.map((doc, index) => (

{doc.name}

{doc.id && (
{canPreviewDocument(doc) && ( )}
)}
))}
)} {/* Activity Photos */} {completionDocuments.activityPhotos && completionDocuments.activityPhotos.length > 0 && (

Activity Photos

{completionDocuments.activityPhotos.length} file(s)
{completionDocuments.activityPhotos.map((doc, index) => (

{doc.name}

{doc.id && (
{canPreviewDocument(doc) && ( )}
)}
))}
)} {/* Invoices / Receipts */} {completionDocuments.invoicesReceipts && completionDocuments.invoicesReceipts.length > 0 && (

Invoices / Receipts

{completionDocuments.invoicesReceipts.length} file(s)
{completionDocuments.invoicesReceipts.map((doc, index) => (

{doc.name}

{doc.id && (
{canPreviewDocument(doc) && ( )}
)}
))}
)} {/* Attendance Sheet */} {completionDocuments.attendanceSheet && (

Attendance Sheet

{completionDocuments.attendanceSheet.name}

{completionDocuments.attendanceSheet.id && (
{canPreviewDocument(completionDocuments.attendanceSheet) && ( )}
)}
)}
)} {/* Verification Warning */}

Please verify all details before pushing to DMS

Once pushed, the system will automatically generate an e-invoice and log it as an activity.

{/* Comments & Remarks */}