invoice elae changes done
This commit is contained in:
parent
32a486d6f4
commit
170f9a1788
@ -10,7 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle, History, ChevronDown, ChevronUp, RefreshCw, RotateCw, Eye, FileSpreadsheet } from 'lucide-react';
|
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle, History, ChevronDown, ChevronUp, RefreshCw, RotateCw, Eye, FileSpreadsheet, X, Loader2 } from 'lucide-react';
|
||||||
import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
|
import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
|
||||||
import { formatHoursMinutes } from '@/utils/slaTracker';
|
import { formatHoursMinutes } from '@/utils/slaTracker';
|
||||||
import {
|
import {
|
||||||
@ -189,6 +189,9 @@ export function DealerClaimWorkflowTab({
|
|||||||
const [showHistory, setShowHistory] = useState(false);
|
const [showHistory, setShowHistory] = useState(false);
|
||||||
const [expandedVersionSteps, setExpandedVersionSteps] = useState<Set<number>>(new Set());
|
const [expandedVersionSteps, setExpandedVersionSteps] = useState<Set<number>>(new Set());
|
||||||
const [viewSnapshot, setViewSnapshot] = useState<{ data: any, type: 'PROPOSAL' | 'COMPLETION', title?: string } | null>(null);
|
const [viewSnapshot, setViewSnapshot] = useState<{ data: any, type: 'PROPOSAL' | 'COMPLETION', title?: string } | null>(null);
|
||||||
|
const [invoicePdfUrl, setInvoicePdfUrl] = useState<string | null>(null);
|
||||||
|
const [showInvoicePdfModal, setShowInvoicePdfModal] = useState(false);
|
||||||
|
const [invoicePdfLoading, setInvoicePdfLoading] = useState(false);
|
||||||
|
|
||||||
// Load approval flows from real API
|
// Load approval flows from real API
|
||||||
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
||||||
@ -1238,7 +1241,14 @@ export function DealerClaimWorkflowTab({
|
|||||||
handleRefresh();
|
handleRefresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[DealerClaimWorkflowTab] Error generating e-invoice:', error);
|
console.error('[DealerClaimWorkflowTab] Error generating e-invoice:', error);
|
||||||
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to generate e-invoice. Please try again.';
|
// Backend now translates PWC error codes to user-friendly messages in 'message' field
|
||||||
|
// Prefer 'message' (user-friendly) over 'error' (raw technical details)
|
||||||
|
const responseMessage = error?.response?.data?.message;
|
||||||
|
let errorMessage = responseMessage || error?.message || 'E-Invoice generation failed. Please try again.';
|
||||||
|
// Truncate very long error messages for toast display (keep first 300 chars)
|
||||||
|
if (errorMessage.length > 300) {
|
||||||
|
errorMessage = errorMessage.substring(0, 300) + '...';
|
||||||
|
}
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -1502,14 +1512,56 @@ export function DealerClaimWorkflowTab({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = TokenManager.getAccessToken();
|
setInvoicePdfLoading(true);
|
||||||
// Construct API URL for PDF preview
|
setShowInvoicePdfModal(true);
|
||||||
const previewUrl = `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1'}/dealer-claims/${requestId}/e-invoice/pdf?token=${token}`;
|
|
||||||
|
|
||||||
window.open(previewUrl, '_blank');
|
// Fetch PDF securely via Authorization header (not in URL query)
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
|
||||||
|
const response = await fetch(`${baseUrl}/dealer-claims/${requestId}/e-invoice/pdf`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${TokenManager.getAccessToken()}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch invoice PDF');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Revoke previous blob URL to prevent memory leaks
|
||||||
|
if (invoicePdfUrl) {
|
||||||
|
window.URL.revokeObjectURL(invoicePdfUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInvoicePdfUrl(blobUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to preview invoice:', error);
|
console.error('Failed to preview invoice:', error);
|
||||||
toast.error('Failed to open invoice preview');
|
toast.error('Failed to load invoice preview');
|
||||||
|
setShowInvoicePdfModal(false);
|
||||||
|
} finally {
|
||||||
|
setInvoicePdfLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseInvoicePdf = () => {
|
||||||
|
setShowInvoicePdfModal(false);
|
||||||
|
if (invoicePdfUrl) {
|
||||||
|
window.URL.revokeObjectURL(invoicePdfUrl);
|
||||||
|
setInvoicePdfUrl(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadInvoicePdf = () => {
|
||||||
|
if (invoicePdfUrl) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = invoicePdfUrl;
|
||||||
|
a.download = `Invoice_${request.requestNumber || 'Download'}.pdf`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
toast.success('Invoice PDF downloaded');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2435,6 +2487,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
requestId={request?.id || request?.requestId}
|
requestId={request?.id || request?.requestId}
|
||||||
previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData}
|
previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData}
|
||||||
documentPolicy={documentPolicy}
|
documentPolicy={documentPolicy}
|
||||||
|
taxationType={request?.claimDetails?.taxationType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Initiator Proposal Approval Modal */}
|
{/* Initiator Proposal Approval Modal */}
|
||||||
@ -2458,6 +2511,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
// proposalSnapshots[1] is the previous proposal (last iteration - 1)
|
// proposalSnapshots[1] is the previous proposal (last iteration - 1)
|
||||||
return proposalSnapshots.length > 1 ? proposalSnapshots[1].snapshotData : null;
|
return proposalSnapshots.length > 1 ? proposalSnapshots[1].snapshotData : null;
|
||||||
})()}
|
})()}
|
||||||
|
taxationType={request?.claimDetails?.taxationType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Dept Lead IO Approval Modal */}
|
{/* Dept Lead IO Approval Modal */}
|
||||||
@ -2484,6 +2538,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
defaultGstRate={request?.claimDetails?.defaultGstRate}
|
defaultGstRate={request?.claimDetails?.defaultGstRate}
|
||||||
requestId={request?.id || request?.requestId}
|
requestId={request?.id || request?.requestId}
|
||||||
documentPolicy={documentPolicy}
|
documentPolicy={documentPolicy}
|
||||||
|
taxationType={request?.claimDetails?.taxationType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* DMS Push Modal */}
|
{/* DMS Push Modal */}
|
||||||
@ -2818,6 +2873,65 @@ export function DealerClaimWorkflowTab({
|
|||||||
type={viewSnapshot?.type || 'PROPOSAL'}
|
type={viewSnapshot?.type || 'PROPOSAL'}
|
||||||
title={viewSnapshot?.title}
|
title={viewSnapshot?.title}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Invoice PDF Viewer Modal */}
|
||||||
|
{showInvoicePdfModal && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div className="absolute inset-0 bg-black/60" onClick={handleCloseInvoicePdf} />
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative w-[95vw] max-w-5xl h-[90vh] bg-white rounded-xl shadow-2xl flex flex-col overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-5 py-3 border-b bg-gray-50">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Receipt className="w-5 h-5 text-amber-600" />
|
||||||
|
<h3 className="font-semibold text-gray-900">Invoice Preview</h3>
|
||||||
|
<Badge className="bg-amber-100 text-amber-800 text-xs">{request.requestNumber}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{invoicePdfUrl && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDownloadInvoicePdf}
|
||||||
|
className="gap-1.5 text-xs"
|
||||||
|
>
|
||||||
|
<Download className="w-3.5 h-3.5" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleCloseInvoicePdf}
|
||||||
|
className="h-8 w-8 hover:bg-gray-200"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Body */}
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{invoicePdfLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full gap-3">
|
||||||
|
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" />
|
||||||
|
<p className="text-sm text-gray-500">Loading invoice...</p>
|
||||||
|
</div>
|
||||||
|
) : invoicePdfUrl ? (
|
||||||
|
<iframe
|
||||||
|
src={invoicePdfUrl}
|
||||||
|
className="w-full h-full border-0"
|
||||||
|
title="Invoice PDF Preview"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<p className="text-sm text-gray-500">Failed to load invoice</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,6 +72,7 @@ interface DealerCompletionDocumentsModalProps {
|
|||||||
maxFileSizeMB: number;
|
maxFileSizeMB: number;
|
||||||
allowedFileTypes: string[];
|
allowedFileTypes: string[];
|
||||||
};
|
};
|
||||||
|
taxationType?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DealerCompletionDocumentsModal({
|
export function DealerCompletionDocumentsModal({
|
||||||
@ -84,6 +85,7 @@ export function DealerCompletionDocumentsModal({
|
|||||||
requestId: _requestId,
|
requestId: _requestId,
|
||||||
defaultGstRate = 18,
|
defaultGstRate = 18,
|
||||||
documentPolicy,
|
documentPolicy,
|
||||||
|
taxationType,
|
||||||
}: DealerCompletionDocumentsModalProps) {
|
}: DealerCompletionDocumentsModalProps) {
|
||||||
const [activityCompletionDate, setActivityCompletionDate] = useState('');
|
const [activityCompletionDate, setActivityCompletionDate] = useState('');
|
||||||
const [numberOfParticipants, setNumberOfParticipants] = useState('');
|
const [numberOfParticipants, setNumberOfParticipants] = useState('');
|
||||||
@ -618,6 +620,11 @@ export function DealerCompletionDocumentsModal({
|
|||||||
<Upload className="w-5 h-5 sm:w-6 sm:h-6 text-[--re-green]" />
|
<Upload className="w-5 h-5 sm:w-6 sm:h-6 text-[--re-green]" />
|
||||||
Activity Completion Documents
|
Activity Completion Documents
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
{taxationType && (
|
||||||
|
<Badge className={`mt-1 mb-1 w-fit ${taxationType === 'GST' ? 'bg-[#2d4a3e] text-white hover:bg-[#2d4a3e]/90' : 'bg-slate-200 text-slate-800 hover:bg-slate-200/90'}`}>
|
||||||
|
{taxationType === 'GST' ? 'GST Claim' : 'Non-GST Claim'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
<DialogDescription className="text-sm sm:text-base">
|
<DialogDescription className="text-sm sm:text-base">
|
||||||
Step 5: Upload completion proof and final documents
|
Step 5: Upload completion proof and final documents
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|||||||
@ -72,6 +72,7 @@ interface DealerProposalSubmissionModalProps {
|
|||||||
maxFileSizeMB: number;
|
maxFileSizeMB: number;
|
||||||
allowedFileTypes: string[];
|
allowedFileTypes: string[];
|
||||||
};
|
};
|
||||||
|
taxationType?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DealerProposalSubmissionModal({
|
export function DealerProposalSubmissionModal({
|
||||||
@ -85,6 +86,7 @@ export function DealerProposalSubmissionModal({
|
|||||||
previousProposalData,
|
previousProposalData,
|
||||||
defaultGstRate = 18,
|
defaultGstRate = 18,
|
||||||
documentPolicy,
|
documentPolicy,
|
||||||
|
taxationType,
|
||||||
}: DealerProposalSubmissionModalProps) {
|
}: DealerProposalSubmissionModalProps) {
|
||||||
const [proposalDocument, setProposalDocument] = useState<File | null>(null);
|
const [proposalDocument, setProposalDocument] = useState<File | null>(null);
|
||||||
|
|
||||||
@ -548,6 +550,11 @@ export function DealerProposalSubmissionModal({
|
|||||||
<Upload className="w-5 h-5 lg:w-6 lg:h-6 text-[--re-green]" />
|
<Upload className="w-5 h-5 lg:w-6 lg:h-6 text-[--re-green]" />
|
||||||
Dealer Proposal Submission
|
Dealer Proposal Submission
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
{taxationType && (
|
||||||
|
<Badge className={`mt-1 mb-1 w-fit ${taxationType === 'GST' ? 'bg-[#2d4a3e] text-white hover:bg-[#2d4a3e]/90' : 'bg-slate-200 text-slate-800 hover:bg-slate-200/90'}`}>
|
||||||
|
{taxationType === 'GST' ? 'GST Claim' : 'Non-GST Claim'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
<DialogDescription className="text-sm lg:text-base">
|
<DialogDescription className="text-sm lg:text-base">
|
||||||
Step 1: Upload proposal and planning details
|
Step 1: Upload proposal and planning details
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|||||||
@ -71,6 +71,7 @@ interface InitiatorProposalApprovalModalProps {
|
|||||||
requestId?: string;
|
requestId?: string;
|
||||||
request?: any; // Request object to check IO blocking status
|
request?: any; // Request object to check IO blocking status
|
||||||
previousProposalData?: any;
|
previousProposalData?: any;
|
||||||
|
taxationType?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InitiatorProposalApprovalModal({
|
export function InitiatorProposalApprovalModal({
|
||||||
@ -85,6 +86,7 @@ export function InitiatorProposalApprovalModal({
|
|||||||
requestId: _requestId, // Prefix with _ to indicate intentionally unused
|
requestId: _requestId, // Prefix with _ to indicate intentionally unused
|
||||||
request,
|
request,
|
||||||
previousProposalData,
|
previousProposalData,
|
||||||
|
taxationType,
|
||||||
}: InitiatorProposalApprovalModalProps) {
|
}: InitiatorProposalApprovalModalProps) {
|
||||||
const [comments, setComments] = useState('');
|
const [comments, setComments] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@ -285,6 +287,11 @@ export function InitiatorProposalApprovalModal({
|
|||||||
<DialogDescription className="text-xs lg:text-sm">
|
<DialogDescription className="text-xs lg:text-sm">
|
||||||
Step 2: Review dealer proposal and make a decision
|
Step 2: Review dealer proposal and make a decision
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
{taxationType && (
|
||||||
|
<Badge className={`mt-1 mb-1 w-fit ${taxationType === 'GST' ? 'bg-[#2d4a3e] text-white hover:bg-[#2d4a3e]/90' : 'bg-slate-200 text-slate-800 hover:bg-slate-200/90'}`}>
|
||||||
|
{taxationType === 'GST' ? 'GST Claim' : 'Non-GST Claim'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
<div className="space-y-1 mt-2 text-xs text-gray-600">
|
<div className="space-y-1 mt-2 text-xs text-gray-600">
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -135,7 +135,7 @@ export const bulkImportHolidays = async (holidays: Partial<Holiday>[]): Promise<
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all activity types (public endpoint - no auth required)
|
* Get all active activity types (requires authentication)
|
||||||
*/
|
*/
|
||||||
export const getActivityTypes = async (): Promise<ActivityType[]> => {
|
export const getActivityTypes = async (): Promise<ActivityType[]> => {
|
||||||
const response = await apiClient.get('/config/activity-types');
|
const response = await apiClient.get('/config/activity-types');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user