templates disabled for the dealer
This commit is contained in:
parent
c1ec261a6d
commit
7d3b6a9da2
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } 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,14 +8,16 @@ 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;
|
||||||
@ -39,7 +41,8 @@ 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',
|
||||||
@ -55,14 +58,32 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,12 +105,13 @@ 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>
|
||||||
|
|
||||||
{/* Custom Close button */}
|
{/* Back arrow button - Top left */}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
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"
|
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"
|
||||||
|
aria-label="Go back"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5 text-gray-600" />
|
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Full Screen Content Container */}
|
{/* Full Screen Content Container */}
|
||||||
@ -117,6 +139,7 @@ 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
|
||||||
@ -124,14 +147,16 @@ 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={{ scale: 1.03 }}
|
whileHover={isDisabled ? {} : { scale: 1.03 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={isDisabled ? {} : { scale: 0.98 }}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className={`cursor-pointer h-full transition-all duration-300 border-2 ${
|
className={`h-full transition-all duration-300 border-2 ${
|
||||||
isSelected
|
isDisabled
|
||||||
? 'border-blue-500 shadow-xl bg-blue-50/50 ring-2 ring-blue-200'
|
? 'opacity-50 cursor-not-allowed border-gray-200'
|
||||||
: 'border-gray-200 hover:border-blue-300 hover:shadow-lg'
|
: isSelected
|
||||||
|
? '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)}
|
||||||
>
|
>
|
||||||
@ -157,6 +182,22 @@ 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">
|
||||||
@ -219,12 +260,12 @@ export function TemplateSelectionModal({ open, onClose, onSelectTemplate }: Temp
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleContinue}
|
onClick={handleContinue}
|
||||||
disabled={!selectedTemplate}
|
disabled={!selectedTemplate || isDealer || AVAILABLE_TEMPLATES.find(t => t.id === selectedTemplate)?.disabled}
|
||||||
size="lg"
|
size="lg"
|
||||||
className={`gap-2 px-8 ${
|
className={`gap-2 px-8 ${
|
||||||
selectedTemplate
|
selectedTemplate && !isDealer && !AVAILABLE_TEMPLATES.find(t => t.id === selectedTemplate)?.disabled
|
||||||
? 'bg-blue-600 hover:bg-blue-700'
|
? 'bg-blue-600 hover:bg-blue-700'
|
||||||
: 'bg-gray-400'
|
: 'bg-gray-400 cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Continue with Template
|
Continue with Template
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef, useEffect } 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,6 +25,7 @@ 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';
|
||||||
@ -79,6 +80,17 @@ 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: '',
|
||||||
@ -283,6 +295,11 @@ 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 || [];
|
||||||
@ -343,9 +360,44 @@ 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) {
|
||||||
onSubmit(claimData);
|
try {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -994,11 +1046,20 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!isStepValid()}
|
disabled={!isStepValid() || isSubmitting}
|
||||||
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"
|
||||||
>
|
>
|
||||||
<Check className="w-4 h-4" />
|
{isSubmitting ? (
|
||||||
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>
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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';
|
||||||
@ -44,6 +45,7 @@ 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);
|
||||||
@ -58,14 +60,19 @@ 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);
|
||||||
|
|
||||||
if (template.id === 'existing-template') {
|
// Note: For 'existing-template', the modal will open when Next is clicked (handled in nextStep)
|
||||||
setShowTemplateModal(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTemplateSelection = (templateId: string) => {
|
const handleTemplateSelection = (templateId: string) => {
|
||||||
if (onSubmit) {
|
// Navigate directly to the template-specific route when template is selected from modal
|
||||||
|
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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -74,6 +81,12 @@ 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' });
|
||||||
}
|
}
|
||||||
@ -142,6 +155,7 @@ export function useCreateRequestHandlers({
|
|||||||
setPreviewDocument(null);
|
setPreviewDocument(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showTemplateModal,
|
showTemplateModal,
|
||||||
setShowTemplateModal,
|
setShowTemplateModal,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user