diff --git a/src/App.tsx b/src/App.tsx index 92fcbb4..a9a2fef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import { useAuth, hasManagementAccess } from '@/contexts/AuthContext'; import { ApproverPerformance } from '@/pages/ApproverPerformance/ApproverPerformance'; import { Profile } from '@/pages/Profile'; import { Settings } from '@/pages/Settings'; +import { SecuritySettings } from '@/pages/Settings/SecuritySettings'; import { Notifications } from '@/pages/Notifications'; import { DetailedReports } from '@/pages/DetailedReports'; @@ -609,6 +610,16 @@ function AppRoutes({ onLogout }: AppProps) { } /> + {/* Security Settings */} + + + + } + /> + {/* Notifications */} ([]); + const [isLoading, setIsLoading] = useState(true); + const [showCreateModal, setShowCreateModal] = useState(false); + const [newTokenName, setNewTokenName] = useState(''); + const [newTokenExpiry, setNewTokenExpiry] = useState(''); + const [generatedToken, setGeneratedToken] = useState(null); + const [isCreating, setIsCreating] = useState(false); + const [copied, setCopied] = useState(false); + const [tokenToRevoke, setTokenToRevoke] = useState(null); + + useEffect(() => { + fetchTokens(); + }, []); + + const fetchTokens = async () => { + try { + setIsLoading(true); + const response = await axios.get('/api-tokens'); + setTokens(response.data.data.tokens); + } catch (error) { + console.error('Failed to fetch API tokens:', error); + toast.error('Failed to load API tokens'); + } finally { + setIsLoading(false); + } + }; + + const handleCreateToken = async () => { + if (!newTokenName.trim()) return; + + try { + setIsCreating(true); + const payload: any = { name: newTokenName }; + if (newTokenExpiry) { + payload.expiresInDays = Number(newTokenExpiry); + } + + const response = await axios.post('/api-tokens', payload); + setGeneratedToken(response.data.data.token); + toast.success('API Token created successfully'); + fetchTokens(); // Refresh list + } catch (error) { + console.error('Failed to create token:', error); + toast.error('Failed to create API token'); + } finally { + setIsCreating(false); + } + }; + + const handleRevokeToken = (token: ApiToken) => { + setTokenToRevoke(token); + }; + + const confirmRevokeToken = async () => { + if (!tokenToRevoke) return; + + + try { + await axios.delete(`/api-tokens/${tokenToRevoke.id}`); + toast.success('Token revoked successfully'); + setTokens(tokens.filter(t => t.id !== tokenToRevoke.id)); + setTokenToRevoke(null); + } catch (error) { + console.error('Failed to revoke token:', error); + toast.error('Failed to revoke token'); + } + }; + + const copyToClipboard = () => { + if (generatedToken) { + navigator.clipboard.writeText(generatedToken); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + toast.success('Token copied to clipboard'); + } + }; + + const resetCreateModal = () => { + setShowCreateModal(false); + setNewTokenName(''); + setNewTokenExpiry(''); + setGeneratedToken(null); + }; + + return ( +
+
+
+

API Tokens

+

Manage personal access tokens for external integrations

+
+ +
+ {isLoading ? ( +
Loading tokens...
+ ) : tokens.length === 0 ? ( +
+ +

No API tokens found

+

Generate a token to access the API programmatically

+
+ ) : ( +
+ + + + Name + Prefix + Last Used + Expires + Actions + + + + {tokens.map((token) => ( + + {token.name} + {token.prefix}... + + {token.lastUsedAt ? format(new Date(token.lastUsedAt), 'MMM d, yyyy') : 'Never'} + + + {token.expiresAt ? format(new Date(token.expiresAt), 'MMM d, yyyy') : 'No Expiry'} + + + + + + ))} + +
+
+ )} + + {/* Create Token Modal */} + !open && resetCreateModal()}> + + + Generate API Token + + Create a new token to access the API. Treat this token like a password. + + + + {!generatedToken ? ( +
+
+ + setNewTokenName(e.target.value)} + /> +
+
+ + { + const val = e.target.value; + if (val === '') { + setNewTokenExpiry(''); + } else { + const num = parseInt(val); + // Prevent negative numbers + if (!isNaN(num) && num >= 1) { + setNewTokenExpiry(num); + } + } + }} + /> +
+
+ ) : ( +
+ + + Token Generated Successfully + + Please copy your token now. You won't be able to see it again! + + + +
+
+ {generatedToken} +
+ +
+
+ )} + + + {!generatedToken ? ( + <> + + + + ) : ( + + )} + +
+
+ + !open && setTokenToRevoke(null)}> + + + Revoke API Token + + Are you sure you want to revoke the token {tokenToRevoke?.name}? + This action cannot be undone and any applications using this token will lose access immediately. + + + + Cancel + + Revoke Token + + + + +
+ ); +} diff --git a/src/dealer-claim/components/request-detail/WorkflowTab.tsx b/src/dealer-claim/components/request-detail/WorkflowTab.tsx index ac4fa89..e791b01 100644 --- a/src/dealer-claim/components/request-detail/WorkflowTab.tsx +++ b/src/dealer-claim/components/request-detail/WorkflowTab.tsx @@ -904,7 +904,9 @@ export function DealerClaimWorkflowTab({ utgstAmt: item.utgstAmt, cessRate: item.cessRate, cessAmt: item.cessAmt, - totalAmt: item.totalAmt + totalAmt: item.totalAmt, + quantity: item.quantity, + hsnCode: item.hsnCode })), totalEstimatedBudget: totalBudget, expectedCompletionDate: data.expectedCompletionDate, @@ -1175,7 +1177,9 @@ export function DealerClaimWorkflowTab({ utgstAmt: item.utgstAmt, cessRate: item.cessRate, cessAmt: item.cessAmt, - totalAmt: item.totalAmt + totalAmt: item.totalAmt, + quantity: item.quantity, + hsnCode: item.hsnCode })); // Submit completion documents using dealer claim API diff --git a/src/dealer-claim/components/request-detail/claim-cards/ActivityInformationCard.tsx b/src/dealer-claim/components/request-detail/claim-cards/ActivityInformationCard.tsx index 44e200d..ec6e399 100644 --- a/src/dealer-claim/components/request-detail/claim-cards/ActivityInformationCard.tsx +++ b/src/dealer-claim/components/request-detail/claim-cards/ActivityInformationCard.tsx @@ -123,7 +123,11 @@ export function ActivityInformationCard({

- {formatCurrency(activityInfo.closedExpenses)} + {formatCurrency( + activityInfo.closedExpensesBreakdown && activityInfo.closedExpensesBreakdown.length > 0 + ? activityInfo.closedExpensesBreakdown.reduce((sum, item: any) => sum + (item.totalAmt || ((item.amount * (item.quantity || 1)) + (item.gstAmt || 0))), 0) + : activityInfo.closedExpenses + )}

)} @@ -152,6 +156,7 @@ export function ActivityInformationCard({ Description + Qty Base GST Total @@ -164,18 +169,19 @@ export function ActivityInformationCard({ {item.description} {item.gstRate ? {item.gstRate}% GST : null} + {item.quantity || 1} {formatCurrency(item.amount)} {formatCurrency(item.gstAmt || 0)} - {formatCurrency(item.totalAmt || (item.amount + (item.gstAmt || 0)))} + {formatCurrency(item.totalAmt || ((item.amount * (item.quantity || 1)) + (item.gstAmt || 0)))} ))} - Final Claim Amount + Final Claim Amount {formatCurrency( - activityInfo.closedExpensesBreakdown.reduce((sum: number, item: any) => sum + (item.totalAmt || (item.amount + (item.gstAmt || 0))), 0) + activityInfo.closedExpensesBreakdown.reduce((sum: number, item: any) => sum + (item.totalAmt || ((item.amount * (item.quantity || 1)) + (item.gstAmt || 0))), 0) )} diff --git a/src/dealer-claim/components/request-detail/claim-cards/ProposalDetailsCard.tsx b/src/dealer-claim/components/request-detail/claim-cards/ProposalDetailsCard.tsx index 438b313..80464fa 100644 --- a/src/dealer-claim/components/request-detail/claim-cards/ProposalDetailsCard.tsx +++ b/src/dealer-claim/components/request-detail/claim-cards/ProposalDetailsCard.tsx @@ -16,6 +16,7 @@ interface ProposalCostItem { cgstAmt?: number; sgstAmt?: number; igstAmt?: number; + quantity?: number; totalAmt?: number; } @@ -45,7 +46,11 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta if (proposalDetails.costBreakup && proposalDetails.costBreakup.length > 0) { const total = proposalDetails.costBreakup.reduce((sum, item) => { const amount = item.amount || 0; - return sum + (Number.isNaN(amount) ? 0 : amount); + const quantity = item.quantity || 1; + const baseTotal = amount * quantity; + const gst = item.gstAmt || 0; + const lineTotal = item.totalAmt || (baseTotal + gst); + return sum + (Number.isNaN(lineTotal) ? 0 : lineTotal); }, 0); return total; } @@ -106,6 +111,9 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta Item Description + + Qty + Base Amount @@ -128,6 +136,9 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta ) : null} + + {item.quantity || 1} + {formatCurrency(item.amount)} @@ -135,12 +146,12 @@ export function ProposalDetailsCard({ proposalDetails, className }: ProposalDeta {formatCurrency(item.gstAmt)} - {formatCurrency(item.totalAmt || (item.amount || 0) + (item.gstAmt || 0))} + {formatCurrency(item.totalAmt || ((item.amount || 0) * (item.quantity || 1)) + (item.gstAmt || 0))} ))} - + Estimated Budget (Total Inclusive of GST) diff --git a/src/dealer-claim/components/request-detail/modals/DMSPushModal.tsx b/src/dealer-claim/components/request-detail/modals/DMSPushModal.tsx index 944cd83..8317520 100644 --- a/src/dealer-claim/components/request-detail/modals/DMSPushModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/DMSPushModal.tsx @@ -118,7 +118,9 @@ export function DMSPushModal({ 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); + const gst = typeof item === 'object' ? ((item as any).gstAmt || 0) : 0; + const total = (item as any).totalAmt || (amount + gst); + return sum + (Number(total) || 0); }, 0); } return 0; @@ -387,38 +389,73 @@ export function DMSPushModal({ {/* Expense Breakdown Card */} {completionDetails?.closedExpenses && completionDetails.closedExpenses.length > 0 && ( - + Expense Breakdown - Review closed expenses before generation + Review closed expenses breakdown (Base + GST) -
- {completionDetails.closedExpenses.map((expense, index) => ( -
-
-

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

-
-
-

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

-
-
- ))} + {/* Table Header */} +
+
Description
+
Base
+
GST Rate
+
GST Amt
+
Total
-
- Total: + +
+ {completionDetails.closedExpenses.map((expense, index) => { + const amount = typeof expense === 'object' ? (expense.amount || 0) : 0; // Base Amount + const gstRate = typeof expense === 'object' ? ((expense as any).gstRate || 0) : 0; + const gstAmt = typeof expense === 'object' ? ((expense as any).gstAmt || 0) : 0; // GST Amount + const total = typeof expense === 'object' ? ((expense as any).totalAmt || (amount + gstAmt)) : 0; // Total Amount + + return ( +
+ {/* Mobile View: Stacked */} +
+ {expense.description || `Expense ${index + 1}`} + {formatCurrency(total)} +
+
+ Base: {formatCurrency(amount)} + GST: {gstRate}% ({formatCurrency(gstAmt)}) +
+ + {/* Desktop View: Grid */} +
+

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

+
+
+ {formatCurrency(amount)} +
+
+ {gstRate}% +
+
+ {formatCurrency(gstAmt)} +
+
+ {formatCurrency(total)} +
+
+ ); + })} +
+ +
+ Total Closed Expenses: {formatCurrency(totalClosedExpenses)} diff --git a/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.css b/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.css index 3a85076..9e8ec2b 100644 --- a/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.css +++ b/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.css @@ -25,7 +25,7 @@ @media (min-width: 1024px) { .dealer-completion-documents-modal { width: 90vw !important; - max-width: 1000px !important; + max-width: 1200px !important; } } @@ -33,7 +33,7 @@ @media (min-width: 1536px) { .dealer-completion-documents-modal { width: 90vw !important; - max-width: 1000px !important; + max-width: 1200px !important; } } @@ -64,5 +64,4 @@ right: 0.5rem; cursor: pointer; opacity: 1; -} - +} \ No newline at end of file diff --git a/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx b/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx index c291e11..dbcc1ca 100644 --- a/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/DealerCompletionDocumentsModal.tsx @@ -30,6 +30,8 @@ interface ExpenseItem { amount: number; gstRate: number; gstAmt: number; + quantity: number; + hsnCode: string; cgstRate: number; cgstAmt: number; sgstRate: number; @@ -148,9 +150,10 @@ export function DealerCompletionDocumentsModal({ }, [expenseItems]); // GST Calculation Helper - const calculateGST = (amount: number, rate: number, type: 'IGST' | 'CGST_SGST' = 'CGST_SGST') => { - const gstAmt = (amount * rate) / 100; - const totalAmt = amount + gstAmt; + const calculateGST = (amount: number, rate: number, quantity: number = 1, type: 'IGST' | 'CGST_SGST' = 'CGST_SGST') => { + const baseTotal = amount * quantity; + const gstAmt = (baseTotal * rate) / 100; + const totalAmt = baseTotal + gstAmt; if (type === 'IGST') { return { @@ -201,6 +204,8 @@ export function DealerCompletionDocumentsModal({ amount: 0, gstRate: 0, gstAmt: 0, + quantity: 1, + hsnCode: '', cgstRate: 0, cgstAmt: 0, sgstRate: 0, @@ -222,16 +227,18 @@ export function DealerCompletionDocumentsModal({ if (item.id === id) { const updatedItem = { ...item, [field]: value }; - // Re-calculate GST if amount or rate changes - if (field === 'amount' || field === 'gstRate') { + // Re-calculate GST if amount, rate or quantity changes + if (field === 'amount' || field === 'gstRate' || field === 'quantity') { const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount; const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate; - const gst = calculateGST(amount, rate); + const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity; + const gst = calculateGST(amount, rate, quantity); return { ...updatedItem, amount, gstRate: rate, + quantity, ...gst }; } @@ -458,6 +465,8 @@ export function DealerCompletionDocumentsModal({ amount: 0, gstRate: 0, gstAmt: 0, + quantity: 1, + hsnCode: '', cgstRate: 0, cgstAmt: 0, sgstRate: 0, @@ -580,6 +589,29 @@ export function DealerCompletionDocumentsModal({ />
+
+ + + handleExpenseChange(item.id, 'hsnCode', e.target.value) + } + className="w-full bg-white text-sm" + /> +
+
+ + + handleExpenseChange(item.id, 'quantity', parseInt(e.target.value) || 1) + } + className="w-full bg-white text-sm" + /> +
{ - const gstAmt = (amount * rate) / 100; - const totalAmt = amount + gstAmt; + const calculateGST = (amount: number, rate: number, quantity: number = 1, type: 'IGST' | 'CGST_SGST' = 'CGST_SGST') => { + const baseTotal = amount * quantity; + const gstAmt = (baseTotal * rate) / 100; + const totalAmt = baseTotal + gstAmt; if (type === 'IGST') { return { @@ -313,6 +318,8 @@ export function DealerProposalSubmissionModal({ amount: 0, gstRate: 0, gstAmt: 0, + quantity: 1, + hsnCode: '', cgstRate: 0, cgstAmt: 0, sgstRate: 0, @@ -340,16 +347,18 @@ export function DealerProposalSubmissionModal({ if (item.id === id) { const updatedItem = { ...item, [field]: value }; - // Re-calculate GST if amount or rate changes - if (field === 'amount' || field === 'gstRate') { + // Re-calculate GST if amount, rate or quantity changes + if (field === 'amount' || field === 'gstRate' || field === 'quantity') { const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount; const rate = field === 'gstRate' ? parseFloat(value) || 0 : item.gstRate; - const gst = calculateGST(amount, rate); + const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity; + const gst = calculateGST(amount, rate, quantity); return { ...updatedItem, amount, gstRate: rate, + quantity, ...gst }; } @@ -413,6 +422,8 @@ export function DealerProposalSubmissionModal({ amount: 0, gstRate: 0, gstAmt: 0, + quantity: 1, + hsnCode: '', cgstRate: 0, cgstAmt: 0, sgstRate: 0, @@ -581,6 +592,7 @@ export function DealerProposalSubmissionModal({ Description + Qty Amount @@ -588,13 +600,16 @@ export function DealerProposalSubmissionModal({ {(previousProposalData.costItems || previousProposalData.costBreakup).map((item: any, idx: number) => ( {item.description} + + {item.quantity || 1} + ₹{Number(item.amount).toLocaleString('en-IN')} ))} - Total + Total ₹{Number(previousProposalData.totalEstimatedBudget || previousProposalData.totalBudget || 0).toLocaleString('en-IN')} @@ -841,57 +856,87 @@ export function DealerProposalSubmissionModal({
{costItems.map((item) => (
-
-
- - - handleCostItemChange(item.id, 'description', e.target.value) - } - className="w-full bg-white" - /> +
+ {/* Row 1: Description and Close Button */} +
+
+ + + handleCostItemChange(item.id, 'description', e.target.value) + } + className="w-full bg-white" + /> +
+
-
- - - handleCostItemChange(item.id, 'amount', e.target.value) - } - className="w-full bg-white" - /> + + {/* Row 2: Numeric Fields */} +
+
+ + + handleCostItemChange(item.id, 'amount', e.target.value) + } + className="w-full bg-white" + /> +
+
+ + + handleCostItemChange(item.id, 'hsnCode', e.target.value) + } + className="w-full bg-white" + /> +
+
+ + + handleCostItemChange(item.id, 'quantity', parseInt(e.target.value) || 1) + } + className="w-full bg-white" + /> +
+
+ + + handleCostItemChange(item.id, 'gstRate', e.target.value) + } + className="w-full bg-white" + /> +
-
- - - handleCostItemChange(item.id, 'gstRate', e.target.value) - } - className="w-full bg-white" - /> -
-
{item.gstAmt > 0 && ( diff --git a/src/dealer-claim/components/request-detail/modals/InitiatorProposalApprovalModal.tsx b/src/dealer-claim/components/request-detail/modals/InitiatorProposalApprovalModal.tsx index 2e83b3e..3e7d688 100644 --- a/src/dealer-claim/components/request-detail/modals/InitiatorProposalApprovalModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/InitiatorProposalApprovalModal.tsx @@ -16,12 +16,12 @@ import { import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; -import { - CheckCircle, - XCircle, - FileText, - IndianRupee, - Calendar, +import { + CheckCircle, + XCircle, + FileText, + IndianRupee, + Calendar, MessageSquare, Download, Eye, @@ -39,6 +39,7 @@ interface CostItem { id: string; description: string; amount: number; + quantity?: number; } interface ProposalData { @@ -89,7 +90,7 @@ export function InitiatorProposalApprovalModal({ const [submitting, setSubmitting] = useState(false); const [actionType, setActionType] = useState<'approve' | 'reject' | 'revision' | null>(null); const [showPreviousProposal, setShowPreviousProposal] = useState(false); - + // Check if IO is blocked (IO blocking moved to Requestor Evaluation level) const internalOrder = request?.internalOrder || request?.internal_order; const ioBlockedAmount = internalOrder?.ioBlockedAmount || internalOrder?.io_blocked_amount || 0; @@ -105,19 +106,23 @@ export function InitiatorProposalApprovalModal({ // Calculate total budget const totalBudget = useMemo(() => { if (!proposalData?.costBreakup) return 0; - + // Ensure costBreakup is an array - const costBreakup = Array.isArray(proposalData.costBreakup) - ? proposalData.costBreakup - : (typeof proposalData.costBreakup === 'string' - ? JSON.parse(proposalData.costBreakup) - : []); - + const costBreakup = Array.isArray(proposalData.costBreakup) + ? proposalData.costBreakup + : (typeof proposalData.costBreakup === 'string' + ? JSON.parse(proposalData.costBreakup) + : []); + if (!Array.isArray(costBreakup)) return 0; - + return costBreakup.reduce((sum: number, item: any) => { const amount = typeof item === 'object' ? (item.amount || 0) : 0; - return sum + (Number(amount) || 0); + const quantity = typeof item === 'object' ? (item.quantity || 1) : 1; + const baseTotal = amount * quantity; + const gst = typeof item === 'object' ? (item.gstAmt || 0) : 0; + const total = item.totalAmt || (baseTotal + gst); + return sum + (Number(total) || 0); }, 0); }, [proposalData]); @@ -141,11 +146,11 @@ export function InitiatorProposalApprovalModal({ 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'); + name.endsWith('.jpg') || + name.endsWith('.jpeg') || + name.endsWith('.png') || + name.endsWith('.gif') || + name.endsWith('.webp'); }; // Handle document preview - leverage FilePreview's internal fetching @@ -296,11 +301,11 @@ export function InitiatorProposalApprovalModal({
- + {/* Previous Proposal Reference Section */} {previousProposalData && (
-
setShowPreviousProposal(!showPreviousProposal)} > @@ -316,48 +321,48 @@ export function InitiatorProposalApprovalModal({ {showPreviousProposal ? : }
- + {showPreviousProposal && (
{/* Header Info: Date & Document */}
- {previousProposalData.expectedCompletionDate && ( -
- - Expected Completion: - {new Date(previousProposalData.expectedCompletionDate).toLocaleDateString('en-IN')} -
- )} + {previousProposalData.expectedCompletionDate && ( +
+ + Expected Completion: + {new Date(previousProposalData.expectedCompletionDate).toLocaleDateString('en-IN')} +
+ )} - {previousProposalData.documentUrl && ( -
- {canPreviewDocument({ name: previousProposalData.documentUrl }) ? ( - <> - - - View Previous Document - - - ) : ( - <> - - - Download Previous Document - - - )} -
- )} + {previousProposalData.documentUrl && ( +
+ {canPreviewDocument({ name: previousProposalData.documentUrl }) ? ( + <> + + + View Previous Document + + + ) : ( + <> + + + Download Previous Document + + + )} +
+ )}
{/* Cost Breakdown */} @@ -395,43 +400,43 @@ export function InitiatorProposalApprovalModal({
)} - + {/* Additional/Supporting Documents */} {previousProposalData.otherDocuments && previousProposalData.otherDocuments.length > 0 && ( -
-

- - Supporting Documents -

-
- {previousProposalData.otherDocuments.map((doc: any, idx: number) => ( - handlePreviewDocument(doc) : undefined} - onDownload={async (id) => { - if (id) { - await downloadDocument(id); - } else { - let downloadUrl = doc.storageUrl || doc.documentUrl; - if (downloadUrl && !downloadUrl.startsWith('http')) { - const baseUrl = import.meta.env.VITE_BASE_URL || ''; - const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; - const cleanFileUrl = downloadUrl.startsWith('/') ? downloadUrl : `/${downloadUrl}`; - downloadUrl = `${cleanBaseUrl}${cleanFileUrl}`; - } - if (downloadUrl) window.open(downloadUrl, '_blank'); - } - }} - /> - ))} -
+
+

+ + Supporting Documents +

+
+ {previousProposalData.otherDocuments.map((doc: any, idx: number) => ( + handlePreviewDocument(doc) : undefined} + onDownload={async (id) => { + if (id) { + await downloadDocument(id); + } else { + let downloadUrl = doc.storageUrl || doc.documentUrl; + if (downloadUrl && !downloadUrl.startsWith('http')) { + const baseUrl = import.meta.env.VITE_BASE_URL || ''; + const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; + const cleanFileUrl = downloadUrl.startsWith('/') ? downloadUrl : `/${downloadUrl}`; + downloadUrl = `${cleanBaseUrl}${cleanFileUrl}`; + } + if (downloadUrl) window.open(downloadUrl, '_blank'); + } + }} + /> + ))}
+
)} {/* Comments */} @@ -453,247 +458,262 @@ export function InitiatorProposalApprovalModal({ )}
- {/* Left Column - Documents */} -
- {/* Proposal Document Section */} -
-
-

- - Proposal Document -

-
- {proposalData?.proposalDocument ? ( -
-
- -
-

- {proposalData.proposalDocument.name} -

- {proposalData?.submittedAt && ( -

- Submitted on {formatDate(proposalData.submittedAt)} -

- )} -
-
-
- {proposalData.proposalDocument.id && ( - <> - {canPreviewDocument(proposalData.proposalDocument) && ( - - )} - - - )} -
-
- ) : ( -

No proposal document available

- )} -
- - {/* Other Supporting Documents */} - {proposalData?.otherDocuments && proposalData.otherDocuments.length > 0 && ( -
-
-

- - Other Supporting Documents -

- - {proposalData.otherDocuments.length} file(s) - -
-
- {proposalData.otherDocuments.map((doc, index) => ( -
-
- -

- {doc.name} -

-
- {doc.id && ( -
- {canPreviewDocument(doc) && ( - - )} - -
- )} -
- ))} -
-
- )} -
- - {/* Right Column - Planning & Details */} -
- {/* Cost Breakup Section */} -
-
-

- - Cost Breakup -

-
- {(() => { - // Ensure costBreakup is an array - const costBreakup = proposalData?.costBreakup - ? (Array.isArray(proposalData.costBreakup) - ? proposalData.costBreakup - : (typeof proposalData.costBreakup === 'string' - ? JSON.parse(proposalData.costBreakup) - : [])) - : []; - - return costBreakup && Array.isArray(costBreakup) && costBreakup.length > 0 ? ( - <> -
-
-
-
Item Description
-
Amount
-
-
-
- {costBreakup.map((item: any, index: number) => ( -
-
{item?.description || 'N/A'}
-
- ₹{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -
-
- ))} -
-
-
-
-
- - Total Estimated Budget -
-
- ₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -
-
-
- - ) : ( -

No cost breakdown available

- ); - })()} -
- - {/* Timeline Section */} -
-
-

- - Expected Completion Date -

-
-
-

- {proposalData?.expectedCompletionDate ? formatDate(proposalData.expectedCompletionDate) : 'Not specified'} -

-
-
-
- - {/* Comments Section - Side by Side */} -
-
- {/* Dealer Comments */} + {/* Left Column - Documents */} +
+ {/* Proposal Document Section */}

- - Dealer Comments + + Proposal Document

-
-

- {proposalData?.dealerComments || 'No comments provided'} + {proposalData?.proposalDocument ? ( +

+
+ +
+

+ {proposalData.proposalDocument.name} +

+ {proposalData?.submittedAt && ( +

+ Submitted on {formatDate(proposalData.submittedAt)} +

+ )} +
+
+
+ {proposalData.proposalDocument.id && ( + <> + {canPreviewDocument(proposalData.proposalDocument) && ( + + )} + + + )} +
+
+ ) : ( +

No proposal document available

+ )} +
+ + {/* Other Supporting Documents */} + {proposalData?.otherDocuments && proposalData.otherDocuments.length > 0 && ( +
+
+

+ + Other Supporting Documents +

+ + {proposalData.otherDocuments.length} file(s) + +
+
+ {proposalData.otherDocuments.map((doc, index) => ( +
+
+ +

+ {doc.name} +

+
+ {doc.id && ( +
+ {canPreviewDocument(doc) && ( + + )} + +
+ )} +
+ ))} +
+
+ )} +
+ + {/* Right Column - Planning & Details */} +
+ {/* Cost Breakup Section */} +
+
+

+ + Cost Breakup +

+
+ {(() => { + // Ensure costBreakup is an array + const costBreakup = proposalData?.costBreakup + ? (Array.isArray(proposalData.costBreakup) + ? proposalData.costBreakup + : (typeof proposalData.costBreakup === 'string' + ? JSON.parse(proposalData.costBreakup) + : [])) + : []; + + return costBreakup && Array.isArray(costBreakup) && costBreakup.length > 0 ? ( + <> +
+
+
+
Item Description
+
Qty
+
Base
+
GST
+
Total
+
+
+
+ {costBreakup.map((item: any, index: number) => ( +
+
+ {item?.description || 'N/A'} + {item?.gstRate ? {item.gstRate}% GST : null} +
+
+ {item?.quantity || 1} +
+
+ ₹{(Number(item?.amount) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+
+ ₹{(Number(item?.gstAmt) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+
+ ₹{(Number(item?.totalAmt || ((item?.amount || 0) * (item?.quantity || 1) + (item?.gstAmt || 0))) || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+
+ ))} +
+
+
+
+
+ + Total Estimated Budget +
+
+ ₹{totalBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+
+
+ + ) : ( +

No cost breakdown available

+ ); + })()} +
+ + {/* Timeline Section */} +
+
+

+ + Expected Completion Date +

+
+
+

+ {proposalData?.expectedCompletionDate ? formatDate(proposalData.expectedCompletionDate) : 'Not specified'}

+
- {/* Your Decision & Comments */} -
-

Your Decision & Comments

-