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 { Button } from '@/components/ui/button';
|
||||
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 { formatHoursMinutes } from '@/utils/slaTracker';
|
||||
import {
|
||||
@ -189,6 +189,9 @@ export function DealerClaimWorkflowTab({
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [expandedVersionSteps, setExpandedVersionSteps] = useState<Set<number>>(new Set());
|
||||
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
|
||||
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
||||
@ -1238,7 +1241,14 @@ export function DealerClaimWorkflowTab({
|
||||
handleRefresh();
|
||||
} catch (error: any) {
|
||||
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);
|
||||
throw error;
|
||||
}
|
||||
@ -1502,14 +1512,56 @@ export function DealerClaimWorkflowTab({
|
||||
return;
|
||||
}
|
||||
|
||||
const token = TokenManager.getAccessToken();
|
||||
// Construct API URL for PDF preview
|
||||
const previewUrl = `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1'}/dealer-claims/${requestId}/e-invoice/pdf?token=${token}`;
|
||||
setInvoicePdfLoading(true);
|
||||
setShowInvoicePdfModal(true);
|
||||
|
||||
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) {
|
||||
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}
|
||||
previousProposalData={versionHistory?.find(v => v.snapshotType === 'PROPOSAL')?.snapshotData}
|
||||
documentPolicy={documentPolicy}
|
||||
taxationType={request?.claimDetails?.taxationType}
|
||||
/>
|
||||
|
||||
{/* Initiator Proposal Approval Modal */}
|
||||
@ -2458,6 +2511,7 @@ export function DealerClaimWorkflowTab({
|
||||
// proposalSnapshots[1] is the previous proposal (last iteration - 1)
|
||||
return proposalSnapshots.length > 1 ? proposalSnapshots[1].snapshotData : null;
|
||||
})()}
|
||||
taxationType={request?.claimDetails?.taxationType}
|
||||
/>
|
||||
|
||||
{/* Dept Lead IO Approval Modal */}
|
||||
@ -2484,6 +2538,7 @@ export function DealerClaimWorkflowTab({
|
||||
defaultGstRate={request?.claimDetails?.defaultGstRate}
|
||||
requestId={request?.id || request?.requestId}
|
||||
documentPolicy={documentPolicy}
|
||||
taxationType={request?.claimDetails?.taxationType}
|
||||
/>
|
||||
|
||||
{/* DMS Push Modal */}
|
||||
@ -2818,6 +2873,65 @@ export function DealerClaimWorkflowTab({
|
||||
type={viewSnapshot?.type || 'PROPOSAL'}
|
||||
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;
|
||||
allowedFileTypes: string[];
|
||||
};
|
||||
taxationType?: string | null;
|
||||
}
|
||||
|
||||
export function DealerCompletionDocumentsModal({
|
||||
@ -84,6 +85,7 @@ export function DealerCompletionDocumentsModal({
|
||||
requestId: _requestId,
|
||||
defaultGstRate = 18,
|
||||
documentPolicy,
|
||||
taxationType,
|
||||
}: DealerCompletionDocumentsModalProps) {
|
||||
const [activityCompletionDate, setActivityCompletionDate] = 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]" />
|
||||
Activity Completion Documents
|
||||
</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">
|
||||
Step 5: Upload completion proof and final documents
|
||||
</DialogDescription>
|
||||
|
||||
@ -72,6 +72,7 @@ interface DealerProposalSubmissionModalProps {
|
||||
maxFileSizeMB: number;
|
||||
allowedFileTypes: string[];
|
||||
};
|
||||
taxationType?: string | null;
|
||||
}
|
||||
|
||||
export function DealerProposalSubmissionModal({
|
||||
@ -85,6 +86,7 @@ export function DealerProposalSubmissionModal({
|
||||
previousProposalData,
|
||||
defaultGstRate = 18,
|
||||
documentPolicy,
|
||||
taxationType,
|
||||
}: DealerProposalSubmissionModalProps) {
|
||||
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]" />
|
||||
Dealer Proposal Submission
|
||||
</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">
|
||||
Step 1: Upload proposal and planning details
|
||||
</DialogDescription>
|
||||
|
||||
@ -71,6 +71,7 @@ interface InitiatorProposalApprovalModalProps {
|
||||
requestId?: string;
|
||||
request?: any; // Request object to check IO blocking status
|
||||
previousProposalData?: any;
|
||||
taxationType?: string | null;
|
||||
}
|
||||
|
||||
export function InitiatorProposalApprovalModal({
|
||||
@ -85,6 +86,7 @@ export function InitiatorProposalApprovalModal({
|
||||
requestId: _requestId, // Prefix with _ to indicate intentionally unused
|
||||
request,
|
||||
previousProposalData,
|
||||
taxationType,
|
||||
}: InitiatorProposalApprovalModalProps) {
|
||||
const [comments, setComments] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@ -285,6 +287,11 @@ export function InitiatorProposalApprovalModal({
|
||||
<DialogDescription className="text-xs lg:text-sm">
|
||||
Step 2: Review dealer proposal and make a decision
|
||||
</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="flex flex-wrap gap-x-4 gap-y-1">
|
||||
<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[]> => {
|
||||
const response = await apiClient.get('/config/activity-types');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user