dealer dropdwn addd wit io remark mandatory in io tab
This commit is contained in:
parent
12f8affd15
commit
058ab97600
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
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';
|
||||||
@ -24,11 +24,10 @@ import {
|
|||||||
Info,
|
Info,
|
||||||
FileText,
|
FileText,
|
||||||
Users,
|
Users,
|
||||||
AlertCircle,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getAllDealers as fetchDealersFromAPI, getDealerByCode, type DealerInfo } from '@/services/dealerApi';
|
import { getAllDealers as fetchDealersFromAPI, verifyDealerLogin, type DealerInfo } from '@/services/dealerApi';
|
||||||
import { ClaimApproverSelectionStep } from './ClaimApproverSelectionStep';
|
import { ClaimApproverSelectionStep } from './ClaimApproverSelectionStep';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
|
||||||
@ -77,7 +76,12 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
const [currentStep, setCurrentStep] = useState(1);
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
const [dealers, setDealers] = useState<DealerInfo[]>([]);
|
const [dealers, setDealers] = useState<DealerInfo[]>([]);
|
||||||
const [loadingDealers, setLoadingDealers] = useState(true);
|
const [loadingDealers, setLoadingDealers] = useState(true);
|
||||||
const [isDealerUser, setIsDealerUser] = useState(false);
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [verifyingDealer, setVerifyingDealer] = useState(false);
|
||||||
|
const [dealerSearchResults, setDealerSearchResults] = useState<DealerInfo[]>([]);
|
||||||
|
const [dealerSearchLoading, setDealerSearchLoading] = useState(false);
|
||||||
|
const [dealerSearchInput, setDealerSearchInput] = useState('');
|
||||||
|
const dealerSearchTimer = useRef<any>(null);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
activityName: '',
|
activityName: '',
|
||||||
@ -110,42 +114,12 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
|
|
||||||
const totalSteps = STEP_NAMES.length;
|
const totalSteps = STEP_NAMES.length;
|
||||||
|
|
||||||
// Check if user is a Dealer and prevent access
|
|
||||||
useEffect(() => {
|
|
||||||
const userDesignation = (user as any)?.designation?.toLowerCase() || '';
|
|
||||||
const isDealer = userDesignation === 'dealer' || userDesignation.includes('dealer');
|
|
||||||
|
|
||||||
if (isDealer) {
|
|
||||||
setIsDealerUser(true);
|
|
||||||
toast.error('Dealers are not allowed to create claim requests. Please contact your administrator.');
|
|
||||||
console.warn('Dealer user attempted to create claim request:', {
|
|
||||||
userId: (user as any)?.userId,
|
|
||||||
email: (user as any)?.email,
|
|
||||||
designation: (user as any)?.designation,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Redirect back after a short delay
|
|
||||||
setTimeout(() => {
|
|
||||||
if (onBack) {
|
|
||||||
onBack();
|
|
||||||
} else {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}, [user, navigate, onBack]);
|
|
||||||
|
|
||||||
// Fetch dealers from API on component mount
|
// Fetch dealers from API on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Don't fetch dealers if user is a Dealer
|
|
||||||
if (isDealerUser) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchDealers = async () => {
|
const fetchDealers = async () => {
|
||||||
setLoadingDealers(true);
|
setLoadingDealers(true);
|
||||||
try {
|
try {
|
||||||
const fetchedDealers = await fetchDealersFromAPI();
|
const fetchedDealers = await fetchDealersFromAPI(undefined, 10); // Limit to 10 records
|
||||||
setDealers(fetchedDealers);
|
setDealers(fetchedDealers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to load dealer list.');
|
toast.error('Failed to load dealer list.');
|
||||||
@ -155,7 +129,40 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchDealers();
|
fetchDealers();
|
||||||
}, [isDealerUser]);
|
}, []);
|
||||||
|
|
||||||
|
// Handle dealer search input with debouncing
|
||||||
|
const handleDealerSearchInputChange = (value: string) => {
|
||||||
|
setDealerSearchInput(value);
|
||||||
|
|
||||||
|
// Clear previous timer
|
||||||
|
if (dealerSearchTimer.current) {
|
||||||
|
clearTimeout(dealerSearchTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If input is empty, clear results
|
||||||
|
if (!value || value.trim().length < 2) {
|
||||||
|
setDealerSearchResults([]);
|
||||||
|
setDealerSearchLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set loading state
|
||||||
|
setDealerSearchLoading(true);
|
||||||
|
|
||||||
|
// Debounce search
|
||||||
|
dealerSearchTimer.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const results = await fetchDealersFromAPI(value, 10); // Limit to 10 results
|
||||||
|
setDealerSearchResults(results);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching dealers:', error);
|
||||||
|
setDealerSearchResults([]);
|
||||||
|
} finally {
|
||||||
|
setDealerSearchLoading(false);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
const updateFormData = (field: string, value: any) => {
|
const updateFormData = (field: string, value: any) => {
|
||||||
setFormData(prev => {
|
setFormData(prev => {
|
||||||
@ -244,26 +251,54 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDealerChange = async (dealerCode: string) => {
|
const handleDealerSelect = async (selectedDealer: DealerInfo) => {
|
||||||
const selectedDealer = dealers.find(d => d.dealerCode === dealerCode);
|
// Verify dealer is logged in
|
||||||
if (selectedDealer) {
|
setVerifyingDealer(true);
|
||||||
updateFormData('dealerCode', dealerCode);
|
try {
|
||||||
updateFormData('dealerName', selectedDealer.dealerName);
|
const verifiedDealer = await verifyDealerLogin(selectedDealer.dealerCode);
|
||||||
updateFormData('dealerEmail', selectedDealer.email || '');
|
|
||||||
updateFormData('dealerPhone', selectedDealer.phone || '');
|
if (!verifiedDealer.isLoggedIn) {
|
||||||
|
toast.error(
|
||||||
|
`Dealer "${verifiedDealer.dealerName || verifiedDealer.displayName}" (${verifiedDealer.dealerCode}) has not logged in to the system. Please ask them to log in first.`,
|
||||||
|
{ duration: 5000 }
|
||||||
|
);
|
||||||
|
// Clear the selection
|
||||||
|
setDealerSearchInput('');
|
||||||
|
setDealerSearchResults([]);
|
||||||
|
updateFormData('dealerCode', '');
|
||||||
|
updateFormData('dealerName', '');
|
||||||
|
updateFormData('dealerEmail', '');
|
||||||
|
updateFormData('dealerPhone', '');
|
||||||
|
updateFormData('dealerAddress', '');
|
||||||
|
setVerifyingDealer(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dealer is logged in, update form data
|
||||||
|
updateFormData('dealerCode', verifiedDealer.dealerCode);
|
||||||
|
updateFormData('dealerName', verifiedDealer.dealerName || verifiedDealer.displayName);
|
||||||
|
updateFormData('dealerEmail', verifiedDealer.email || '');
|
||||||
|
updateFormData('dealerPhone', verifiedDealer.phone || '');
|
||||||
updateFormData('dealerAddress', ''); // Address not available in API response
|
updateFormData('dealerAddress', ''); // Address not available in API response
|
||||||
|
|
||||||
// Try to fetch full dealer info from API
|
// Clear search input and results
|
||||||
try {
|
setDealerSearchInput(verifiedDealer.dealerName || verifiedDealer.displayName);
|
||||||
const fullDealerInfo = await getDealerByCode(dealerCode);
|
setDealerSearchResults([]);
|
||||||
if (fullDealerInfo) {
|
|
||||||
updateFormData('dealerEmail', fullDealerInfo.email || selectedDealer.email || '');
|
toast.success(`Dealer "${verifiedDealer.dealerName || verifiedDealer.displayName}" verified and logged in`);
|
||||||
updateFormData('dealerPhone', fullDealerInfo.phone || selectedDealer.phone || '');
|
} catch (error: any) {
|
||||||
}
|
const errorMessage = error.message || 'Failed to verify dealer login';
|
||||||
} catch (error) {
|
toast.error(errorMessage, { duration: 5000 });
|
||||||
// Ignore error, use basic info from list
|
// Clear the selection
|
||||||
console.debug('Could not fetch full dealer info:', error);
|
setDealerSearchInput('');
|
||||||
}
|
setDealerSearchResults([]);
|
||||||
|
updateFormData('dealerCode', '');
|
||||||
|
updateFormData('dealerName', '');
|
||||||
|
updateFormData('dealerEmail', '');
|
||||||
|
updateFormData('dealerPhone', '');
|
||||||
|
updateFormData('dealerAddress', '');
|
||||||
|
} finally {
|
||||||
|
setVerifyingDealer(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -386,38 +421,102 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
{/* Dealer Selection */}
|
{/* Dealer Selection */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-base font-semibold">Dealer Code / Dealer Name *</Label>
|
<Label className="text-base font-semibold">Dealer Code / Dealer Name *</Label>
|
||||||
<Select value={formData.dealerCode} onValueChange={handleDealerChange} disabled={loadingDealers}>
|
<div className="mt-2">
|
||||||
<SelectTrigger className="mt-2 !h-12 data-[size=default]:!h-12" id="dealer-select">
|
<div className="relative">
|
||||||
<SelectValue placeholder={loadingDealers ? "Loading dealers..." : "Select dealer"}>
|
<Input
|
||||||
{formData.dealerCode && (
|
placeholder="Type dealer code, name, or email to search..."
|
||||||
<div className="flex items-center gap-2">
|
value={formData.dealerCode ? `${formData.dealerName} (${formData.dealerCode})` : dealerSearchInput}
|
||||||
<span className="font-mono text-sm">{formData.dealerCode}</span>
|
onChange={(e) => {
|
||||||
<span className="text-gray-400">•</span>
|
if (formData.dealerCode) {
|
||||||
<span>{formData.dealerName}</span>
|
// If dealer is already selected, clear selection first
|
||||||
</div>
|
updateFormData('dealerCode', '');
|
||||||
)}
|
updateFormData('dealerName', '');
|
||||||
</SelectValue>
|
updateFormData('dealerEmail', '');
|
||||||
</SelectTrigger>
|
updateFormData('dealerPhone', '');
|
||||||
<SelectContent>
|
updateFormData('dealerAddress', '');
|
||||||
{dealers.length === 0 && !loadingDealers ? (
|
setDealerSearchInput(e.target.value);
|
||||||
<div className="p-2 text-sm text-gray-500">No dealers available</div>
|
} else {
|
||||||
) : (
|
handleDealerSearchInputChange(e.target.value);
|
||||||
dealers.map((dealer) => (
|
}
|
||||||
<SelectItem key={dealer.userId} value={dealer.dealerCode}>
|
}}
|
||||||
<div className="flex items-center gap-2">
|
onFocus={() => {
|
||||||
<span className="font-mono text-sm font-semibold">{dealer.dealerCode}</span>
|
// When input is focused, show search results if input has value
|
||||||
<span className="text-gray-400">•</span>
|
if (dealerSearchInput && dealerSearchInput.length >= 2) {
|
||||||
<span>{dealer.dealerName}</span>
|
handleDealerSearchInputChange(dealerSearchInput);
|
||||||
</div>
|
}
|
||||||
</SelectItem>
|
}}
|
||||||
))
|
className="h-12 border-2 border-gray-300 focus:border-blue-500"
|
||||||
|
disabled={verifyingDealer}
|
||||||
|
/>
|
||||||
|
{formData.dealerCode && (
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||||
|
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
|
||||||
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
|
Verified
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
{/* Search suggestions dropdown */}
|
||||||
</Select>
|
{(dealerSearchLoading || dealerSearchResults.length > 0) && !formData.dealerCode && (
|
||||||
|
<div className="absolute left-0 right-0 top-full mt-1 z-50 border rounded-md bg-white shadow-lg">
|
||||||
|
{dealerSearchLoading ? (
|
||||||
|
<div className="p-2 text-xs text-gray-500">Searching...</div>
|
||||||
|
) : (
|
||||||
|
<ul className="max-h-56 overflow-auto divide-y">
|
||||||
|
{dealerSearchResults.map((dealer) => (
|
||||||
|
<li
|
||||||
|
key={dealer.dealerId}
|
||||||
|
className="p-2 text-sm cursor-pointer hover:bg-gray-50"
|
||||||
|
onClick={() => handleDealerSelect(dealer)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
{dealer.dealerName || dealer.displayName}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-600">
|
||||||
|
<span className="font-mono">{dealer.dealerCode}</span>
|
||||||
|
{dealer.email && (
|
||||||
|
<>
|
||||||
|
<span className="mx-1">•</span>
|
||||||
|
<span>{dealer.email}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{dealer.city && dealer.state && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{dealer.city}, {dealer.state}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="ml-2">
|
||||||
|
{dealer.isLoggedIn ? (
|
||||||
|
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
|
||||||
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
|
Logged In
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="destructive" className="text-xs">Not Logged In</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{formData.dealerCode && (
|
{formData.dealerCode && (
|
||||||
<p className="text-sm text-gray-600 mt-2">
|
<div className="mt-2 space-y-1">
|
||||||
Selected: <span className="font-semibold">{formData.dealerName}</span> ({formData.dealerCode})
|
<p className="text-sm text-gray-600">
|
||||||
</p>
|
Selected: <span className="font-semibold">{formData.dealerName}</span> ({formData.dealerCode})
|
||||||
|
</p>
|
||||||
|
{formData.dealerEmail && (
|
||||||
|
<p className="text-xs text-gray-500">Email: {formData.dealerEmail}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -821,51 +920,6 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show access denied message if user is a Dealer
|
|
||||||
if (isDealerUser) {
|
|
||||||
return (
|
|
||||||
<div className="w-full bg-gradient-to-br from-gray-50 to-gray-100 py-4 sm:py-6 lg:py-8 px-3 sm:px-4 lg:px-6 overflow-y-auto">
|
|
||||||
<div className="max-w-6xl mx-auto pb-8">
|
|
||||||
<div className="mb-6 sm:mb-8">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={onBack || (() => navigate('/'))}
|
|
||||||
className="mb-3 sm:mb-4 gap-2 text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-3 h-3 sm:w-4 sm:h-4" />
|
|
||||||
<span className="hidden sm:inline">Back to Dashboard</span>
|
|
||||||
<span className="sm:hidden">Back</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="max-w-2xl mx-auto">
|
|
||||||
<CardContent className="p-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<AlertCircle className="w-8 h-8 text-red-600" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Access Denied</h2>
|
|
||||||
<p className="text-gray-600 mb-6">
|
|
||||||
Dealers are not allowed to create claim requests. Only internal employees can initiate claim requests.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 mb-6">
|
|
||||||
If you believe this is an error, please contact your administrator.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={onBack || (() => navigate('/'))}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-gradient-to-br from-gray-50 to-gray-100 py-4 sm:py-6 lg:py-8 px-3 sm:px-4 lg:px-6 overflow-y-auto">
|
<div className="w-full bg-gradient-to-br from-gray-50 to-gray-100 py-4 sm:py-6 lg:py-8 px-3 sm:px-4 lg:px-6 overflow-y-auto">
|
||||||
<div className="max-w-6xl mx-auto pb-8">
|
<div className="max-w-6xl mx-auto pb-8">
|
||||||
|
|||||||
@ -193,6 +193,11 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ioRemark.trim()) {
|
||||||
|
toast.error('Please enter IO remark before blocking the amount');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!requestId) {
|
if (!requestId) {
|
||||||
toast.error('Request ID not found');
|
toast.error('Request ID not found');
|
||||||
return;
|
return;
|
||||||
@ -348,10 +353,13 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="ioRemark" className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
<Label htmlFor="ioRemark" className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
||||||
IO Remark <span className="text-red-500">*</span>
|
IO Remark <span className="text-red-500">*</span>
|
||||||
|
{fetchedAmount !== null && !blockedDetails && (
|
||||||
|
<Badge variant="destructive" className="text-xs ml-2">Required before blocking</Badge>
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="ioRemark"
|
id="ioRemark"
|
||||||
placeholder="Enter remarks about IO organization"
|
placeholder="Enter remarks about IO organization (required before blocking amount)"
|
||||||
value={ioRemark}
|
value={ioRemark}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
@ -361,11 +369,22 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
}}
|
}}
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={!!blockedDetails}
|
disabled={!!blockedDetails}
|
||||||
className="bg-white text-sm min-h-[80px] resize-none"
|
className={`bg-white text-sm min-h-[80px] resize-none ${
|
||||||
|
fetchedAmount !== null && !ioRemark.trim() && !blockedDetails
|
||||||
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end text-xs text-gray-600">
|
<div className="flex justify-between items-center">
|
||||||
<span>{ioRemarkChars}/{maxIoRemarkChars}</span>
|
<div>
|
||||||
|
{fetchedAmount !== null && !ioRemark.trim() && !blockedDetails && (
|
||||||
|
<p className="text-xs text-red-600">IO remark is mandatory before blocking the amount</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-600">
|
||||||
|
<span>{ioRemarkChars}/{maxIoRemarkChars}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -421,12 +440,15 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
{/* Block Button */}
|
{/* Block Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleBlockBudget}
|
onClick={handleBlockBudget}
|
||||||
disabled={blockingBudget || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount}
|
disabled={blockingBudget || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount || !ioRemark.trim()}
|
||||||
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]"
|
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]"
|
||||||
>
|
>
|
||||||
<Target className="w-4 h-4 mr-2" />
|
<Target className="w-4 h-4 mr-2" />
|
||||||
{blockingBudget ? 'Blocking in SAP...' : 'Block IO in SAP'}
|
{blockingBudget ? 'Blocking in SAP...' : 'Block IO in SAP'}
|
||||||
</Button>
|
</Button>
|
||||||
|
{!ioRemark.trim() && (
|
||||||
|
<p className="text-xs text-red-600 mt-1">Please enter IO remark before blocking the amount</p>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export function CreditNoteSAPModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="sm:max-w-lg max-w-4xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-lg lg:max-w-[1000px] max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="font-semibold flex items-center gap-2 text-2xl">
|
<DialogTitle className="font-semibold flex items-center gap-2 text-2xl">
|
||||||
<Receipt className="w-6 h-6 text-[--re-green]" />
|
<Receipt className="w-6 h-6 text-[--re-green]" />
|
||||||
|
|||||||
@ -13,25 +13,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Large screens - use more width */
|
/* Tablet and small desktop */
|
||||||
|
@media (min-width: 641px) and (max-width: 1023px) {
|
||||||
|
.dms-push-modal {
|
||||||
|
width: 90vw !important;
|
||||||
|
max-width: 90vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large screens - fixed max-width for better readability */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.dms-push-modal {
|
.dms-push-modal {
|
||||||
width: 85vw !important;
|
width: 90vw !important;
|
||||||
max-width: 85vw !important;
|
max-width: 1000px !important;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.dms-push-modal {
|
|
||||||
width: 80vw !important;
|
|
||||||
max-width: 80vw !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Extra large screens */
|
||||||
@media (min-width: 1536px) {
|
@media (min-width: 1536px) {
|
||||||
.dms-push-modal {
|
.dms-push-modal {
|
||||||
width: 75vw !important;
|
width: 90vw !important;
|
||||||
max-width: 75vw !important;
|
max-width: 1000px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,25 +13,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Large screens - use more width */
|
/* Tablet and small desktop */
|
||||||
|
@media (min-width: 641px) and (max-width: 1023px) {
|
||||||
|
.dealer-completion-documents-modal {
|
||||||
|
width: 90vw !important;
|
||||||
|
max-width: 90vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large screens - fixed max-width for better readability */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.dealer-completion-documents-modal {
|
.dealer-completion-documents-modal {
|
||||||
width: 85vw !important;
|
width: 90vw !important;
|
||||||
max-width: 85vw !important;
|
max-width: 1000px !important;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.dealer-completion-documents-modal {
|
|
||||||
width: 80vw !important;
|
|
||||||
max-width: 80vw !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Extra large screens */
|
||||||
@media (min-width: 1536px) {
|
@media (min-width: 1536px) {
|
||||||
.dealer-completion-documents-modal {
|
.dealer-completion-documents-modal {
|
||||||
width: 75vw !important;
|
width: 90vw !important;
|
||||||
max-width: 75vw !important;
|
max-width: 1000px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,25 +13,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Large screens - use more width */
|
/* Tablet and small desktop */
|
||||||
|
@media (min-width: 641px) and (max-width: 1023px) {
|
||||||
|
.dealer-proposal-modal {
|
||||||
|
width: 90vw !important;
|
||||||
|
max-width: 90vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large screens - fixed max-width for better readability */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.dealer-proposal-modal {
|
.dealer-proposal-modal {
|
||||||
width: 85vw !important;
|
width: 90vw !important;
|
||||||
max-width: 85vw !important;
|
max-width: 1000px !important;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.dealer-proposal-modal {
|
|
||||||
width: 80vw !important;
|
|
||||||
max-width: 80vw !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Extra large screens */
|
||||||
@media (min-width: 1536px) {
|
@media (min-width: 1536px) {
|
||||||
.dealer-proposal-modal {
|
.dealer-proposal-modal {
|
||||||
width: 75vw !important;
|
width: 90vw !important;
|
||||||
max-width: 75vw !important;
|
max-width: 1000px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,25 +13,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Large screens - use more width */
|
/* Tablet and small desktop */
|
||||||
|
@media (min-width: 641px) and (max-width: 1023px) {
|
||||||
|
.dept-lead-io-modal {
|
||||||
|
width: 90vw !important;
|
||||||
|
max-width: 90vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Large screens - fixed max-width for better readability */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.dept-lead-io-modal {
|
.dept-lead-io-modal {
|
||||||
width: 85vw !important;
|
width: 90vw !important;
|
||||||
max-width: 85vw !important;
|
max-width: 1000px !important;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.dept-lead-io-modal {
|
|
||||||
width: 80vw !important;
|
|
||||||
max-width: 80vw !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Extra large screens */
|
||||||
@media (min-width: 1536px) {
|
@media (min-width: 1536px) {
|
||||||
.dept-lead-io-modal {
|
.dept-lead-io-modal {
|
||||||
width: 75vw !important;
|
width: 90vw !important;
|
||||||
max-width: 75vw !important;
|
max-width: 1000px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -92,7 +92,7 @@ export function EditClaimAmountModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
<DialogContent className="sm:max-w-[500px] lg:max-w-[800px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<DollarSign className="w-5 h-5 text-green-600" />
|
<DollarSign className="w-5 h-5 text-green-600" />
|
||||||
|
|||||||
@ -53,7 +53,7 @@ This is an automated message.`;
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="sm:max-w-2xl max-w-2xl">
|
<DialogContent className="sm:max-w-2xl lg:max-w-[1000px] max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@ -6,22 +6,40 @@
|
|||||||
import apiClient from './authApi';
|
import apiClient from './authApi';
|
||||||
|
|
||||||
export interface DealerInfo {
|
export interface DealerInfo {
|
||||||
userId: string;
|
dealerId: string;
|
||||||
email: string;
|
userId?: string | null; // User ID if dealer is logged in
|
||||||
dealerCode: string;
|
email: string; // domain_id from dealers table
|
||||||
dealerName: string;
|
dealerCode: string; // dlrcode from dealers table
|
||||||
displayName: string;
|
dealerName: string; // dealership from dealers table
|
||||||
phone?: string;
|
displayName: string; // dealer_principal_name from dealers table
|
||||||
|
phone?: string; // dp_contact_number from dealers table
|
||||||
department?: string;
|
department?: string;
|
||||||
designation?: string;
|
designation?: string;
|
||||||
|
isLoggedIn: boolean; // Whether dealer's domain_id exists in users table
|
||||||
|
salesCode?: string | null;
|
||||||
|
serviceCode?: string | null;
|
||||||
|
gearCode?: string | null;
|
||||||
|
gmaCode?: string | null;
|
||||||
|
region?: string | null;
|
||||||
|
state?: string | null;
|
||||||
|
district?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
dealerPrincipalName?: string | null;
|
||||||
|
dealerPrincipalEmailId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all dealers
|
* Get all dealers (with optional search)
|
||||||
|
* @param searchTerm - Optional search term to filter dealers
|
||||||
|
* @param limit - Maximum number of records to return (default: 10)
|
||||||
*/
|
*/
|
||||||
export async function getAllDealers(): Promise<DealerInfo[]> {
|
export async function getAllDealers(searchTerm?: string, limit: number = 10): Promise<DealerInfo[]> {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get('/dealers');
|
const params: any = { limit };
|
||||||
|
if (searchTerm) {
|
||||||
|
params.q = searchTerm;
|
||||||
|
}
|
||||||
|
const res = await apiClient.get('/dealers', { params });
|
||||||
return res.data?.data || res.data || [];
|
return res.data?.data || res.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[DealerAPI] Error fetching dealers:', error);
|
console.error('[DealerAPI] Error fetching dealers:', error);
|
||||||
@ -57,11 +75,13 @@ export async function getDealerByEmail(email: string): Promise<DealerInfo | null
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search dealers
|
* Search dealers
|
||||||
|
* @param searchTerm - Search term to filter dealers
|
||||||
|
* @param limit - Maximum number of records to return (default: 10)
|
||||||
*/
|
*/
|
||||||
export async function searchDealers(searchTerm: string): Promise<DealerInfo[]> {
|
export async function searchDealers(searchTerm: string, limit: number = 10): Promise<DealerInfo[]> {
|
||||||
try {
|
try {
|
||||||
const res = await apiClient.get('/dealers/search', {
|
const res = await apiClient.get('/dealers/search', {
|
||||||
params: { q: searchTerm },
|
params: { q: searchTerm, limit },
|
||||||
});
|
});
|
||||||
return res.data?.data || res.data || [];
|
return res.data?.data || res.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -70,3 +90,18 @@ export async function searchDealers(searchTerm: string): Promise<DealerInfo[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify dealer is logged in to the system
|
||||||
|
* Throws error if dealer is not logged in
|
||||||
|
*/
|
||||||
|
export async function verifyDealerLogin(dealerCode: string): Promise<DealerInfo> {
|
||||||
|
try {
|
||||||
|
const res = await apiClient.get(`/dealers/verify/${dealerCode}`);
|
||||||
|
return res.data?.data || res.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || 'Dealer verification failed';
|
||||||
|
console.error('[DealerAPI] Error verifying dealer login:', errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -368,6 +368,36 @@ export function getDocumentPreviewUrl(documentId: string): string {
|
|||||||
return `${baseURL}/api/v1/workflows/documents/${documentId}/preview`;
|
return `${baseURL}/api/v1/workflows/documents/${documentId}/preview`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract filename from Content-Disposition header
|
||||||
|
* Handles both formats: filename="name" and filename*=UTF-8''encoded
|
||||||
|
*/
|
||||||
|
function extractFilenameFromContentDisposition(contentDisposition: string | null): string {
|
||||||
|
if (!contentDisposition) {
|
||||||
|
return 'download';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract from filename* first (RFC 5987 encoded) - preferred for non-ASCII
|
||||||
|
const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
|
||||||
|
if (filenameStarMatch) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(filenameStarMatch[1]);
|
||||||
|
} catch {
|
||||||
|
// If decoding fails, fall back to regular filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to regular filename (for ASCII-only filenames)
|
||||||
|
const filenameMatch = contentDisposition.match(/filename="?([^";]+)"?/);
|
||||||
|
if (filenameMatch) {
|
||||||
|
// Remove quotes and extract only the filename part (before any semicolon)
|
||||||
|
const extracted = filenameMatch[1].replace(/^"|"$/g, '').split(';')[0].trim();
|
||||||
|
return extracted || 'download';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'download';
|
||||||
|
}
|
||||||
|
|
||||||
export async function downloadDocument(documentId: string): Promise<void> {
|
export async function downloadDocument(documentId: string): Promise<void> {
|
||||||
const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
|
const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
|
||||||
const downloadUrl = `${baseURL}/api/v1/workflows/documents/${documentId}/download`;
|
const downloadUrl = `${baseURL}/api/v1/workflows/documents/${documentId}/download`;
|
||||||
@ -398,8 +428,7 @@ export async function downloadDocument(documentId: string): Promise<void> {
|
|||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
const contentDisposition = response.headers.get('Content-Disposition');
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
const extractedFilename = contentDisposition?.split('filename=')[1]?.replace(/"/g, '');
|
const filename = extractFilenameFromContentDisposition(contentDisposition);
|
||||||
const filename: string = extractedFilename || 'download';
|
|
||||||
|
|
||||||
const downloadLink = document.createElement('a');
|
const downloadLink = document.createElement('a');
|
||||||
downloadLink.href = url;
|
downloadLink.href = url;
|
||||||
@ -445,8 +474,7 @@ export async function downloadWorkNoteAttachment(attachmentId: string): Promise<
|
|||||||
|
|
||||||
// Get filename from Content-Disposition header or use default
|
// Get filename from Content-Disposition header or use default
|
||||||
const contentDisposition = response.headers.get('Content-Disposition');
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
const extractedFilename = contentDisposition?.split('filename=')[1]?.replace(/"/g, '');
|
const filename = extractFilenameFromContentDisposition(contentDisposition);
|
||||||
const filename: string = extractedFilename || 'download';
|
|
||||||
|
|
||||||
const downloadLink = document.createElement('a');
|
const downloadLink = document.createElement('a');
|
||||||
downloadLink.href = url;
|
downloadLink.href = url;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user