297 lines
12 KiB
TypeScript
297 lines
12 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { CheckCircle2, AlertCircle, Ban } from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { StatusChip } from './StatusChip';
|
|
import { TimelineStep } from './TimelineStep';
|
|
import { contactAdminForForm16Mismatch } from '@/services/form16Api';
|
|
import { toast } from 'sonner';
|
|
|
|
export type SubmissionResultStatus = 'success' | 'mismatch' | 'duplicate' | 'error';
|
|
|
|
interface RequestSubmissionSuccessProps {
|
|
/** 'success' = matched & credit note; 'mismatch' = value mismatch; 'duplicate' = already submitted; 'error' = request received / processing */
|
|
status: SubmissionResultStatus;
|
|
requestId: string;
|
|
creditNoteNumber?: string | null;
|
|
/** Optional message (e.g. from API or error) */
|
|
message?: string | null;
|
|
onComplete: () => void;
|
|
onResubmit?: () => void;
|
|
/** When status is 'error' (request received), optional handler to open the request detail */
|
|
onViewRequest?: () => void;
|
|
}
|
|
|
|
export function RequestSubmissionSuccess({
|
|
status,
|
|
requestId,
|
|
creditNoteNumber,
|
|
message,
|
|
onComplete,
|
|
onResubmit,
|
|
onViewRequest,
|
|
}: RequestSubmissionSuccessProps) {
|
|
const isSuccess = status === 'success';
|
|
const isMismatch = status === 'mismatch';
|
|
const isMissing26AsMismatch = isMismatch && (message || '').toLowerCase().includes('26as') && (message || '').toLowerCase().includes('no 26as');
|
|
|
|
const onContactAdmin = async () => {
|
|
try {
|
|
await contactAdminForForm16Mismatch(requestId);
|
|
toast.success('Administrator notified');
|
|
} catch (e: any) {
|
|
const msg = e?.response?.data?.message || e?.message || 'Failed to notify administrator';
|
|
toast.error(String(msg));
|
|
}
|
|
};
|
|
|
|
const isDuplicate = status === 'duplicate';
|
|
const isError = status === 'error';
|
|
|
|
useEffect(() => {
|
|
if (!isSuccess) return;
|
|
const timer = setTimeout(onComplete, 5000);
|
|
return () => clearTimeout(timer);
|
|
}, [isSuccess, onComplete]);
|
|
|
|
const steps = [
|
|
{ label: 'Form 16A Uploaded', state: isDuplicate ? ('failed' as const) : ('completed' as const) },
|
|
{ label: 'Validation', state: isDuplicate ? ('failed' as const) : (isError ? ('pending' as const) : ('completed' as const)) },
|
|
{ label: '26AS Matching', state: isSuccess ? ('completed' as const) : (isMismatch || isDuplicate) ? ('failed' as const) : ('pending' as const) },
|
|
{ label: 'Credit Note', state: isSuccess ? ('completed' as const) : ('pending' as const) },
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-[calc(100vh-10rem)] flex items-center justify-center">
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.4 }}
|
|
className="w-full max-w-2xl"
|
|
>
|
|
<Card
|
|
className={
|
|
isSuccess
|
|
? 'border-teal-200 shadow-xl'
|
|
: isDuplicate
|
|
? 'border-red-300 shadow-xl bg-red-50/50'
|
|
: isMismatch
|
|
? 'border-amber-200 shadow-xl bg-amber-50/30'
|
|
: 'border-gray-200 shadow-xl bg-gray-50/30'
|
|
}
|
|
>
|
|
<CardContent className="pt-12 pb-10">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.15 }}
|
|
className="flex justify-center mb-6"
|
|
>
|
|
<StatusChip
|
|
variant={isSuccess ? 'success' : isDuplicate ? 'failed' : isError ? 'pending' : 'failed'}
|
|
label={
|
|
isSuccess
|
|
? 'Matched & Credit Note Generated'
|
|
: isDuplicate
|
|
? 'Duplicate Submission'
|
|
: isMismatch
|
|
? 'Value Mismatch'
|
|
: 'Request Received'
|
|
}
|
|
showIcon={true}
|
|
/>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ scale: 0 }}
|
|
animate={{ scale: 1 }}
|
|
transition={{ delay: 0.2, type: 'spring', stiffness: 200 }}
|
|
className="flex justify-center mb-6"
|
|
>
|
|
<div
|
|
className={`w-20 h-20 rounded-full flex items-center justify-center ${
|
|
isSuccess ? 'bg-teal-100' : isDuplicate ? 'bg-red-100' : isError ? 'bg-gray-100' : 'bg-amber-100'
|
|
}`}
|
|
>
|
|
{isSuccess ? (
|
|
<CheckCircle2 className="w-12 h-12 text-teal-600" />
|
|
) : isDuplicate ? (
|
|
<Ban className="w-12 h-12 text-red-600" />
|
|
) : isError ? (
|
|
<AlertCircle className="w-12 h-12 text-gray-500" />
|
|
) : (
|
|
<AlertCircle className="w-12 h-12 text-amber-600" />
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.3 }}
|
|
className="text-center mb-6"
|
|
>
|
|
{isSuccess ? (
|
|
<>
|
|
<h2 className="text-gray-900 mb-2">Request Submitted Successfully</h2>
|
|
<p className="text-gray-700 font-medium text-teal-800">
|
|
Details have matched and credit note is generated.
|
|
</p>
|
|
<p className="text-gray-600 text-sm mt-1">
|
|
Your Form 16A data matched with 26AS records. Credit note has been generated.
|
|
</p>
|
|
</>
|
|
) : isDuplicate ? (
|
|
<>
|
|
<h2 className="text-gray-900 mb-2 text-red-700 font-semibold">Duplicate Submission</h2>
|
|
<p className="text-gray-900 font-medium">
|
|
This Form 16A has already been submitted for the same quarter and financial year.
|
|
</p>
|
|
<p className="text-gray-700 text-sm mt-1">
|
|
A credit note may already have been issued. Please check your Closed Requests or Credit Notes.
|
|
</p>
|
|
{message && (
|
|
<div className="mt-3 p-3 bg-red-50 border-2 border-red-200 rounded-md">
|
|
<p className="text-sm font-semibold text-gray-900 mb-1">Duplicate submission — not allowed</p>
|
|
<p className="text-sm text-gray-900 whitespace-pre-wrap">{message}</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : isMismatch ? (
|
|
<>
|
|
<h2 className="text-gray-900 mb-2">Value Mismatch</h2>
|
|
<p className="text-gray-700 font-medium text-amber-800">
|
|
Resubmit the form carefully.
|
|
</p>
|
|
<p className="text-gray-600 text-sm mt-1">
|
|
Form 16A details did not match with 26AS data. Please verify the certificate and
|
|
resubmit with correct details.
|
|
</p>
|
|
{message && (
|
|
<div className="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-md">
|
|
<p className="text-sm font-semibold text-amber-900 mb-1">Validation Error:</p>
|
|
<p className="text-sm text-amber-800 whitespace-pre-wrap">{message}</p>
|
|
</div>
|
|
)}
|
|
{isMissing26AsMismatch && (
|
|
<div className="mt-3 p-3 bg-white border border-amber-200 rounded-md">
|
|
<p className="text-sm font-semibold text-gray-900 mb-1">
|
|
Contact administrator: FORM 26AS does not match FORM 16A.
|
|
</p>
|
|
<p className="text-sm text-gray-700">
|
|
If you have submitted the updated Form 16A but the latest 26AS is not uploaded for this quarter yet, please notify RE so they can upload/update 26AS.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
<h2 className="text-gray-900 mb-2">Request Submitted</h2>
|
|
<p className="text-gray-700 font-medium text-gray-800">
|
|
Your request has been received and is being processed.
|
|
</p>
|
|
<p className="text-gray-600 text-sm mt-1">
|
|
If there was an issue, you can try again or contact support.
|
|
</p>
|
|
{message && (
|
|
<p className="text-sm text-gray-600 mt-2 italic">{message}</p>
|
|
)}
|
|
</>
|
|
)}
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.4 }}
|
|
className={`rounded-lg p-4 mb-6 text-center ${
|
|
isSuccess ? 'bg-teal-50 border border-teal-200' : isDuplicate ? 'bg-red-50 border-2 border-red-200' : 'bg-white border border-amber-200'
|
|
}`}
|
|
>
|
|
<p className={`text-sm mb-1 ${isDuplicate ? 'text-red-700' : 'text-gray-600'}`}>Request ID</p>
|
|
<p className={`font-mono tracking-wide ${isDuplicate ? 'text-red-800 font-semibold' : 'text-gray-900'}`}>{requestId || '—'}</p>
|
|
{isSuccess && creditNoteNumber && (
|
|
<>
|
|
<p className="text-sm text-gray-600 mt-3 mb-1">Credit Note Number</p>
|
|
<p className="text-gray-900 font-mono font-medium text-teal-700">
|
|
{creditNoteNumber}
|
|
</p>
|
|
</>
|
|
)}
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.5 }}
|
|
className="mb-8"
|
|
>
|
|
<p className="text-sm text-gray-600 mb-4 text-center">Process flow</p>
|
|
<div className="flex items-center justify-center gap-0">
|
|
{steps.map((step, index) => (
|
|
<TimelineStep
|
|
key={index}
|
|
step={index + 1}
|
|
label={step.label}
|
|
state={step.state}
|
|
isLast={index === steps.length - 1}
|
|
/>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.6 }}
|
|
className="flex flex-col sm:flex-row items-center justify-center gap-3"
|
|
>
|
|
{isSuccess ? (
|
|
<>
|
|
<p className="text-sm text-gray-500 mb-2 sm:mb-0 sm:mr-2">
|
|
Redirecting to My Requests in a moment...
|
|
</p>
|
|
<Button onClick={onComplete} variant="outline" className="border-teal-300 text-teal-700">
|
|
Back to My Requests
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<>
|
|
{isError && onViewRequest && (
|
|
<Button onClick={onViewRequest} className="bg-teal-600 hover:bg-teal-700">
|
|
View Request
|
|
</Button>
|
|
)}
|
|
{onResubmit && (isMismatch || isDuplicate || isError) && (
|
|
<Button
|
|
onClick={onResubmit}
|
|
className={
|
|
isDuplicate
|
|
? 'bg-red-600 hover:bg-red-700'
|
|
: isMismatch
|
|
? 'bg-amber-600 hover:bg-amber-700'
|
|
: 'bg-gray-600 hover:bg-gray-700'
|
|
}
|
|
>
|
|
{isDuplicate ? 'Back to New Submission' : isMismatch ? 'Resubmit Form 16A' : 'Try Again'}
|
|
</Button>
|
|
)}
|
|
{isMissing26AsMismatch && (
|
|
<Button onClick={onContactAdmin} variant="outline" className="border-amber-300 text-amber-800">
|
|
Contact admin
|
|
</Button>
|
|
)}
|
|
<Button onClick={onComplete} variant="outline">
|
|
Back to My Requests
|
|
</Button>
|
|
</>
|
|
)}
|
|
</motion.div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|