Compare commits

..

No commits in common. "7d3b6a9da267f2d76fff7ab939c4d235c182d7e6" and "c6bd5a19ef50d64dd20c67c853ad9b87d4191d72" have entirely different histories.

4 changed files with 29 additions and 143 deletions

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '../ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogTitle } from '../ui/dialog';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
@ -8,16 +8,14 @@ import {
Receipt, Receipt,
Package, Package,
ArrowRight, ArrowRight,
ArrowLeft,
Clock, Clock,
CheckCircle, CheckCircle,
Target, Target,
X,
Sparkles, Sparkles,
Check, Check
AlertCircle
} from 'lucide-react'; } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { TokenManager } from '../../utils/tokenManager';
interface TemplateSelectionModalProps { interface TemplateSelectionModalProps {
open: boolean; open: boolean;
@ -41,8 +39,7 @@ const AVAILABLE_TEMPLATES = [
'Document verification', 'Document verification',
'E-invoice generation', 'E-invoice generation',
'Credit note issuance' 'Credit note issuance'
], ]
disabled: false
}, },
{ {
id: 'vendor-payment', id: 'vendor-payment',
@ -58,32 +55,14 @@ const AVAILABLE_TEMPLATES = [
'Invoice verification', 'Invoice verification',
'Multi-level approvals', 'Multi-level approvals',
'Payment scheduling' 'Payment scheduling'
], ]
disabled: true,
comingSoon: true
} }
]; ];
export function TemplateSelectionModal({ open, onClose, onSelectTemplate }: TemplateSelectionModalProps) { export function TemplateSelectionModal({ open, onClose, onSelectTemplate }: TemplateSelectionModalProps) {
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null); const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
const [isDealer, setIsDealer] = useState(false);
// Check if user is a Dealer
useEffect(() => {
const userData = TokenManager.getUserData();
setIsDealer(userData?.jobTitle === 'Dealer');
}, []);
const handleSelect = (templateId: string) => { const handleSelect = (templateId: string) => {
// Don't allow selection if user is a dealer
if (isDealer) {
return;
}
// Don't allow selection if template is disabled
const template = AVAILABLE_TEMPLATES.find(t => t.id === templateId);
if (template?.disabled) {
return;
}
setSelectedTemplate(templateId); setSelectedTemplate(templateId);
}; };
@ -105,13 +84,12 @@ export function TemplateSelectionModal({ open, onClose, onSelectTemplate }: Temp
Choose from pre-configured templates with predefined workflows and approval chains for faster processing. Choose from pre-configured templates with predefined workflows and approval chains for faster processing.
</DialogDescription> </DialogDescription>
{/* Back arrow button - Top left */} {/* Custom Close button */}
<button <button
onClick={onClose} onClick={onClose}
className="!flex absolute top-6 left-6 z-50 w-10 h-10 rounded-full bg-white shadow-lg hover:shadow-xl border border-gray-200 items-center justify-center transition-all hover:scale-110" className="absolute top-6 right-6 z-50 w-10 h-10 rounded-full bg-white shadow-lg hover:shadow-xl border border-gray-200 flex items-center justify-center transition-all hover:scale-110"
aria-label="Go back"
> >
<ArrowLeft className="w-5 h-5 text-gray-600" /> <X className="w-5 h-5 text-gray-600" />
</button> </button>
{/* Full Screen Content Container */} {/* Full Screen Content Container */}
@ -139,7 +117,6 @@ export function TemplateSelectionModal({ open, onClose, onSelectTemplate }: Temp
{AVAILABLE_TEMPLATES.map((template, index) => { {AVAILABLE_TEMPLATES.map((template, index) => {
const Icon = template.icon; const Icon = template.icon;
const isSelected = selectedTemplate === template.id; const isSelected = selectedTemplate === template.id;
const isDisabled = isDealer || template.disabled;
return ( return (
<motion.div <motion.div
@ -147,16 +124,14 @@ export function TemplateSelectionModal({ open, onClose, onSelectTemplate }: Temp
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }} transition={{ delay: index * 0.1 }}
whileHover={isDisabled ? {} : { scale: 1.03 }} whileHover={{ scale: 1.03 }}
whileTap={isDisabled ? {} : { scale: 0.98 }} whileTap={{ scale: 0.98 }}
> >
<Card <Card
className={`h-full transition-all duration-300 border-2 ${ className={`cursor-pointer h-full transition-all duration-300 border-2 ${
isDisabled isSelected
? 'opacity-50 cursor-not-allowed border-gray-200' ? 'border-blue-500 shadow-xl bg-blue-50/50 ring-2 ring-blue-200'
: isSelected : 'border-gray-200 hover:border-blue-300 hover:shadow-lg'
? 'cursor-pointer border-blue-500 shadow-xl bg-blue-50/50 ring-2 ring-blue-200'
: 'cursor-pointer border-gray-200 hover:border-blue-300 hover:shadow-lg'
}`} }`}
onClick={() => handleSelect(template.id)} onClick={() => handleSelect(template.id)}
> >
@ -182,22 +157,6 @@ export function TemplateSelectionModal({ open, onClose, onSelectTemplate }: Temp
<CardDescription className="text-sm leading-relaxed"> <CardDescription className="text-sm leading-relaxed">
{template.description} {template.description}
</CardDescription> </CardDescription>
{isDealer && (
<div className="mt-3 flex items-start gap-2 p-2 bg-amber-50 border border-amber-200 rounded-lg">
<AlertCircle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
<p className="text-xs text-amber-800">
Not accessible for Dealers
</p>
</div>
)}
{template.comingSoon && !isDealer && (
<div className="mt-3 flex items-start gap-2 p-2 bg-blue-50 border border-blue-200 rounded-lg">
<AlertCircle className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<p className="text-xs text-blue-800 font-semibold">
Coming Soon
</p>
</div>
)}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pt-0 space-y-4"> <CardContent className="pt-0 space-y-4">
@ -260,12 +219,12 @@ export function TemplateSelectionModal({ open, onClose, onSelectTemplate }: Temp
</Button> </Button>
<Button <Button
onClick={handleContinue} onClick={handleContinue}
disabled={!selectedTemplate || isDealer || AVAILABLE_TEMPLATES.find(t => t.id === selectedTemplate)?.disabled} disabled={!selectedTemplate}
size="lg" size="lg"
className={`gap-2 px-8 ${ className={`gap-2 px-8 ${
selectedTemplate && !isDealer && !AVAILABLE_TEMPLATES.find(t => t.id === selectedTemplate)?.disabled selectedTemplate
? 'bg-blue-600 hover:bg-blue-700' ? 'bg-blue-600 hover:bg-blue-700'
: 'bg-gray-400 cursor-not-allowed' : 'bg-gray-400'
}`} }`}
> >
Continue with Template Continue with Template

View File

@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@ -25,7 +25,6 @@ import {
FileText, FileText,
Users, Users,
XCircle, XCircle,
Loader2,
} from 'lucide-react'; } from 'lucide-react';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -80,17 +79,6 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
const [dealerSearchLoading, setDealerSearchLoading] = useState(false); const [dealerSearchLoading, setDealerSearchLoading] = useState(false);
const [dealerSearchInput, setDealerSearchInput] = useState(''); const [dealerSearchInput, setDealerSearchInput] = useState('');
const dealerSearchTimer = useRef<any>(null); const dealerSearchTimer = useRef<any>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const submitTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (submitTimeoutRef.current) {
clearTimeout(submitTimeoutRef.current);
}
};
}, []);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
activityName: '', activityName: '',
@ -295,11 +283,6 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
}; };
const handleSubmit = () => { const handleSubmit = () => {
// Prevent multiple submissions
if (isSubmitting) {
return;
}
// Approvers are already using integer levels with proper shifting // Approvers are already using integer levels with proper shifting
// Just sort them and prepare for submission // Just sort them and prepare for submission
const approvers = formData.approvers || []; const approvers = formData.approvers || [];
@ -360,44 +343,9 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
approvers: finalApprovers approvers: finalApprovers
}; };
// Set submitting state to prevent multiple clicks
setIsSubmitting(true);
// Clear any existing timeout
if (submitTimeoutRef.current) {
clearTimeout(submitTimeoutRef.current);
}
// Set a timeout as a fallback to reset loading state (30 seconds)
// In most cases, the parent component will navigate away on success,
// but this prevents the button from being stuck in loading state if there's an error
submitTimeoutRef.current = setTimeout(() => {
setIsSubmitting(false);
submitTimeoutRef.current = null;
}, 30000);
// Don't show toast here - let the parent component handle success/error after API call // Don't show toast here - let the parent component handle success/error after API call
if (onSubmit) { if (onSubmit) {
try { onSubmit(claimData);
onSubmit(claimData);
// Note: On success, the component will unmount when parent navigates away (timeout cleared in useEffect)
// On error, the timeout will reset the state after 30 seconds
} catch (error) {
// If onSubmit throws synchronously, reset state immediately
if (submitTimeoutRef.current) {
clearTimeout(submitTimeoutRef.current);
submitTimeoutRef.current = null;
}
setIsSubmitting(false);
console.error('Error submitting claim:', error);
}
} else {
// If no onSubmit handler, reset immediately
if (submitTimeoutRef.current) {
clearTimeout(submitTimeoutRef.current);
submitTimeoutRef.current = null;
}
setIsSubmitting(false);
} }
}; };
@ -1046,20 +994,11 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
) : ( ) : (
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={!isStepValid() || isSubmitting} disabled={!isStepValid()}
className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto order-1 sm:order-2" className="gap-2 bg-green-600 hover:bg-green-700 w-full sm:w-auto order-1 sm:order-2"
> >
{isSubmitting ? ( <Check className="w-4 h-4" />
<> Submit Claim Request
<Loader2 className="w-4 h-4 animate-spin" />
Submitting...
</>
) : (
<>
<Check className="w-4 h-4" />
Submit Claim Request
</>
)}
</Button> </Button>
)} )}
</div> </div>

View File

@ -19,6 +19,8 @@ import {
CheckCircle, CheckCircle,
XCircle, XCircle,
FileText, FileText,
User,
Calendar,
MessageSquare, MessageSquare,
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';

View File

@ -8,7 +8,6 @@
*/ */
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { RequestTemplate, FormData } from '@/hooks/useCreateRequestForm'; import { RequestTemplate, FormData } from '@/hooks/useCreateRequestForm';
import { PreviewDocument } from '../types/createRequest.types'; import { PreviewDocument } from '../types/createRequest.types';
import { getDocumentPreviewUrl } from '@/services/workflowApi'; import { getDocumentPreviewUrl } from '@/services/workflowApi';
@ -45,7 +44,6 @@ export function useCreateRequestHandlers({
openValidationModal, openValidationModal,
onSubmit, onSubmit,
}: UseHandlersOptions) { }: UseHandlersOptions) {
const navigate = useNavigate();
const [showTemplateModal, setShowTemplateModal] = useState(false); const [showTemplateModal, setShowTemplateModal] = useState(false);
const [previewDocument, setPreviewDocument] = const [previewDocument, setPreviewDocument] =
useState<PreviewDocument | null>(null); useState<PreviewDocument | null>(null);
@ -60,19 +58,14 @@ export function useCreateRequestHandlers({
const suggestedDate = new Date(); const suggestedDate = new Date();
suggestedDate.setDate(suggestedDate.getDate() + template.suggestedSLA); suggestedDate.setDate(suggestedDate.getDate() + template.suggestedSLA);
updateFormData('slaEndDate', suggestedDate); updateFormData('slaEndDate', suggestedDate);
// Note: For 'existing-template', the modal will open when Next is clicked (handled in nextStep) if (template.id === 'existing-template') {
setShowTemplateModal(true);
}
}; };
const handleTemplateSelection = (templateId: string) => { const handleTemplateSelection = (templateId: string) => {
// Navigate directly to the template-specific route when template is selected from modal if (onSubmit) {
if (templateId === 'claim-management') {
navigate('/claim-management');
} else if (templateId === 'vendor-payment') {
// Add vendor-payment route if it exists, otherwise fallback to onSubmit
navigate('/vendor-payment');
} else if (onSubmit) {
// Fallback to onSubmit for other template types
onSubmit({ templateType: templateId }); onSubmit({ templateType: templateId });
} }
}; };
@ -81,12 +74,6 @@ export function useCreateRequestHandlers({
const nextStep = async () => { const nextStep = async () => {
if (!isStepValid()) return; if (!isStepValid()) return;
// On step 1, if "existing-template" is selected, open the template selection modal
if (currentStep === 1 && _selectedTemplate?.id === 'existing-template') {
setShowTemplateModal(true);
return;
}
if (window.innerWidth < 640) { if (window.innerWidth < 640) {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
} }
@ -155,7 +142,6 @@ export function useCreateRequestHandlers({
setPreviewDocument(null); setPreviewDocument(null);
}; };
return { return {
showTemplateModal, showTemplateModal,
setShowTemplateModal, setShowTemplateModal,