dealer access to the create request disabled

This commit is contained in:
laxmanhalaki 2025-12-31 20:11:31 +05:30
parent 7d3b6a9da2
commit 7893b52183
7 changed files with 155 additions and 128 deletions

View File

@ -19,6 +19,7 @@ import { ReLogo } from '@/assets';
import notificationApi, { Notification } from '@/services/notificationApi';
import { getSocket, joinUserRoom } from '@/utils/socket';
import { formatDistanceToNow } from 'date-fns';
import { TokenManager } from '@/utils/tokenManager';
interface PageLayoutProps {
children: React.ReactNode;
@ -36,6 +37,17 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
const [notificationsOpen, setNotificationsOpen] = useState(false);
const { user } = useAuth();
// Check if user is a Dealer
const isDealer = useMemo(() => {
try {
const userData = TokenManager.getUserData();
return userData?.jobTitle === 'Dealer';
} catch (error) {
console.error('[PageLayout] Error checking dealer status:', error);
return false;
}
}, []);
// Get user initials for avatar
const getUserInitials = () => {
try {
@ -63,16 +75,19 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
{ id: 'requests', label: 'All Requests', icon: List },
];
// Add remaining menu items
// Add remaining menu items (exclude "My Requests" for dealers)
if (!isDealer) {
items.push({ id: 'my-requests', label: 'My Requests', icon: User });
}
items.push(
{ id: 'my-requests', label: 'My Requests', icon: User },
{ id: 'open-requests', label: 'Open Requests', icon: FileText },
{ id: 'closed-requests', label: 'Closed Requests', icon: CheckCircle },
{ id: 'shared-summaries', label: 'Shared Summary', icon: Share2 }
);
return items;
}, []);
}, [isDealer]);
const toggleSidebar = () => {
setSidebarOpen(!sidebarOpen);
@ -256,16 +271,18 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
</div>
{/* Quick Action in Sidebar - Right below menu items */}
<div className="mt-6 pt-6 border-t border-gray-800 px-3">
<Button
onClick={onNewRequest}
className="w-full bg-re-green hover:bg-re-green/90 text-white text-sm font-medium"
size="sm"
>
<Plus className="w-4 h-4 mr-2" />
Raise New Request
</Button>
</div>
{!isDealer && (
<div className="mt-6 pt-6 border-t border-gray-800 px-3">
<Button
onClick={onNewRequest}
className="w-full bg-re-green hover:bg-re-green/90 text-white text-sm font-medium"
size="sm"
>
<Plus className="w-4 h-4 mr-2" />
Raise New Request
</Button>
</div>
)}
</div>
</div>
</aside>
@ -294,14 +311,16 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
</div>
<div className="flex items-center gap-4 shrink-0">
<Button
onClick={onNewRequest}
className="bg-re-green hover:bg-re-green/90 text-white gap-2 hidden md:flex text-sm"
size="sm"
>
<Plus className="w-4 h-4" />
New Request
</Button>
{!isDealer && (
<Button
onClick={onNewRequest}
className="bg-re-green hover:bg-re-green/90 text-white gap-2 hidden md:flex text-sm"
size="sm"
>
<Plus className="w-4 h-4" />
New Request
</Button>
)}
<DropdownMenu open={notificationsOpen} onOpenChange={setNotificationsOpen}>
<DropdownMenuTrigger asChild>

View File

@ -1,9 +1,8 @@
import { motion, AnimatePresence } from 'framer-motion';
import { motion } from 'framer-motion';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { Check, Clock, Users, Info, Flame, Target, TrendingUp } from 'lucide-react';
import { Check, Clock, Users, Flame, Target, TrendingUp } from 'lucide-react';
import { RequestTemplate } from '@/hooks/useCreateRequestForm';
interface TemplateSelectionStepProps {
@ -142,63 +141,6 @@ export function TemplateSelectionStep({
</motion.div>
))}
</div>
{/* Template Details Card */}
<AnimatePresence>
{selectedTemplate && (
<motion.div
initial={{ opacity: 0, y: 20, height: 0 }}
animate={{ opacity: 1, y: 0, height: 'auto' }}
exit={{ opacity: 0, y: -20, height: 0 }}
transition={{ duration: 0.3 }}
className="w-full max-w-6xl"
data-testid="template-details-card"
>
<Card className="bg-gradient-to-br from-blue-50 to-indigo-50 border-2 border-blue-200">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-blue-900" data-testid="template-details-title">
<Info className="w-5 h-5" />
{selectedTemplate.name} - Template Details
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-sla">
<Label className="text-blue-900 font-semibold">Suggested SLA</Label>
<p className="text-blue-700 mt-1">{selectedTemplate.suggestedSLA} days</p>
</div>
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-priority">
<Label className="text-blue-900 font-semibold">Priority Level</Label>
<div className="flex items-center gap-1 mt-1">
{getPriorityIcon(selectedTemplate.priority)}
<span className="text-blue-700 capitalize">{selectedTemplate.priority}</span>
</div>
</div>
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-duration">
<Label className="text-blue-900 font-semibold">Estimated Duration</Label>
<p className="text-blue-700 mt-1">{selectedTemplate.estimatedTime}</p>
</div>
</div>
<div className="bg-white/60 p-3 rounded-lg" data-testid="template-details-approvers">
<Label className="text-blue-900 font-semibold">Common Approvers</Label>
<div className="flex flex-wrap gap-2 mt-2">
{selectedTemplate.commonApprovers.map((approver, index) => (
<Badge
key={`${selectedTemplate.id}-approver-${index}-${approver}`}
variant="outline"
className="border-blue-300 text-blue-700 bg-white"
data-testid={`template-details-approver-${index}`}
>
{approver}
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}

View File

@ -251,7 +251,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
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.`,
`Dealer "${verifiedDealer.dealerName || verifiedDealer.displayName}" (${verifiedDealer.dealerCode}) is not mapped to the system.`,
{ duration: 5000 }
);
// Clear the selection
@ -277,9 +277,9 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
setDealerSearchInput(verifiedDealer.dealerName || verifiedDealer.displayName);
setDealerSearchResults([]);
toast.success(`Dealer "${verifiedDealer.dealerName || verifiedDealer.displayName}" verified and logged in`);
toast.success(`Dealer "${verifiedDealer.dealerName || verifiedDealer.displayName}" verified and mapped to the System`);
} catch (error: any) {
const errorMessage = error.message || 'Failed to verify dealer login';
const errorMessage = 'Dealer is not mapped to the system'
toast.error(errorMessage, { duration: 5000 });
// Clear the selection
setDealerSearchInput('');
@ -537,18 +537,6 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
)}
</div>
</div>
<div className="mt-2 flex items-center gap-2 text-xs text-gray-500">
<span>Status:</span>
<div className="flex items-center gap-1">
<CheckCircle className="w-3 h-3 text-green-600" />
<span>Logged in</span>
</div>
<span className="mx-1"></span>
<div className="flex items-center gap-1">
<XCircle className="w-3 h-3 text-red-500" />
<span>Not logged in</span>
</div>
</div>
{formData.dealerCode && (
<div className="mt-2 space-y-1">
<p className="text-sm text-gray-600">

View File

@ -395,10 +395,18 @@ export function DealerClaimWorkflowTab({
return `Step ${stepNumber} approval required.`;
};
// Get backend currentLevel to determine which steps are active vs waiting
// This needs to be calculated before mapping steps so we can use it in status normalization
// Convert to number to ensure proper comparison
const backendCurrentLevel = request?.currentLevel || request?.current_level || request?.currentStep;
const currentLevelNumber = backendCurrentLevel !== undefined && backendCurrentLevel !== null
? Number(backendCurrentLevel)
: null;
// Transform approval flow to dealer claim workflow steps
const workflowSteps: WorkflowStep[] = approvalFlow.map((step: any, index: number) => {
// Get actual step number from levelNumber or step field
const actualStepNumber = step.levelNumber || step.level_number || step.step || index + 1;
// Get actual step number from levelNumber or step field - ensure it's a number
const actualStepNumber = Number(step.levelNumber || step.level_number || step.step || index + 1);
// Get levelName from the approval level if available
const levelName = step.levelName || step.level_name;
@ -467,26 +475,75 @@ export function DealerClaimWorkflowTab({
}
}
// Normalize status - handle "in-review" and other variations
// Check both step.status and approval.status (approval status is more accurate after approval)
const stepStatus = step.status || approval?.status || 'waiting';
let normalizedStatus = stepStatus.toLowerCase();
// Normalize status - ALWAYS check step position first, then use approval/step status
// This ensures future steps are always 'waiting' regardless of approval/step status
let normalizedStatus: string;
// Handle status variations
if (normalizedStatus === 'in-review' || normalizedStatus === 'in_review' || normalizedStatus === 'in review') {
normalizedStatus = 'in_progress';
}
// If approval exists and has a status, prefer that (it's more up-to-date)
if (approval?.status) {
const approvalStatus = approval.status.toLowerCase();
// Map backend status values to frontend status values
if (approvalStatus === 'approved') {
normalizedStatus = 'approved';
} else if (approvalStatus === 'rejected') {
normalizedStatus = 'rejected';
} else if (approvalStatus === 'pending' || approvalStatus === 'in_progress' || approvalStatus === 'in-progress') {
normalizedStatus = 'in_progress';
// First, check step position relative to currentLevel (this is the source of truth)
// Use currentLevelNumber which is guaranteed to be a number or null
if (currentLevelNumber !== null && currentLevelNumber > 0) {
if (actualStepNumber > currentLevelNumber) {
// Future step - MUST be waiting (ignore approval status and step.status)
normalizedStatus = 'waiting';
} else if (actualStepNumber < currentLevelNumber) {
// Past step - should be approved/rejected
// Use approval status if available, otherwise default to waiting
if (approval?.status) {
const approvalStatus = approval.status.toLowerCase();
if (approvalStatus === 'approved') {
normalizedStatus = 'approved';
} else if (approvalStatus === 'rejected') {
normalizedStatus = 'rejected';
} else {
normalizedStatus = 'waiting'; // Past step with unexpected status
}
} else {
normalizedStatus = 'waiting'; // Past step with no approval
}
} else {
// Current step - use approval status if available, otherwise use step.status
if (approval?.status) {
const approvalStatus = approval.status.toLowerCase();
if (approvalStatus === 'approved') {
normalizedStatus = 'approved';
} else if (approvalStatus === 'rejected') {
normalizedStatus = 'rejected';
} else if (approvalStatus === 'pending' || approvalStatus === 'in_progress' || approvalStatus === 'in-progress') {
normalizedStatus = 'in_progress';
} else {
normalizedStatus = 'in_progress'; // Default for current step
}
} else {
// No approval object - normalize step.status
const stepStatus = (step.status || 'pending').toLowerCase();
if (stepStatus === 'in-review' || stepStatus === 'in_review' || stepStatus === 'in review' || stepStatus === 'pending') {
normalizedStatus = 'in_progress';
} else {
normalizedStatus = stepStatus;
}
}
}
} else {
// No backend currentLevel - use approval status if available, otherwise step.status
if (approval?.status) {
const approvalStatus = approval.status.toLowerCase();
if (approvalStatus === 'approved') {
normalizedStatus = 'approved';
} else if (approvalStatus === 'rejected') {
normalizedStatus = 'rejected';
} else if (approvalStatus === 'pending' || approvalStatus === 'in_progress' || approvalStatus === 'in-progress') {
normalizedStatus = 'in_progress';
} else {
normalizedStatus = (step.status || 'waiting').toLowerCase();
}
} else {
// No approval and no currentLevel - normalize step.status
const stepStatus = (step.status || 'waiting').toLowerCase();
if (stepStatus === 'in-review' || stepStatus === 'in_review' || stepStatus === 'in review' || stepStatus === 'pending') {
normalizedStatus = 'in_progress';
} else {
normalizedStatus = stepStatus;
}
}
}
@ -522,10 +579,12 @@ export function DealerClaimWorkflowTab({
// IMPORTANT: Use the workflow's currentLevel from backend (most accurate)
// Fallback to finding first pending step if currentLevel not available
// Note: Status normalization already handled in workflowSteps mapping above
const backendCurrentLevel = request?.currentLevel || request?.current_level || request?.currentStep;
// backendCurrentLevel is already calculated above before the map function
// Find the step that matches backend's currentLevel
const activeStepFromBackend = workflowSteps.find(s => s.step === backendCurrentLevel);
const activeStepFromBackend = currentLevelNumber !== null
? workflowSteps.find(s => s.step === currentLevelNumber)
: null;
// If backend currentLevel exists and step is pending/in_progress, use it
// Otherwise, find first pending/in_progress step
@ -537,7 +596,7 @@ export function DealerClaimWorkflowTab({
return status === 'pending' || status === 'in_progress' || status === 'in-review' || status === 'in_review';
});
const currentStep = activeStep ? activeStep.step : (backendCurrentLevel || request?.currentStep || 1);
const currentStep = activeStep ? activeStep.step : (currentLevelNumber || request?.currentStep || 1);
// Check if current user is the dealer (for steps 1 and 5)
const userEmail = (user as any)?.email?.toLowerCase() || '';

View File

@ -89,7 +89,7 @@ export function Auth() {
) : (
<>
<LogIn className="mr-2 h-5 w-5" />
Login with OKTA
RE Employee Login
</>
)}
</Button>
@ -119,7 +119,7 @@ export function Auth() {
) : (
<>
<Shield className="mr-2 h-5 w-5" />
Login with Tanflow
Dealer Login
</>
)}
</Button>

View File

@ -4,6 +4,7 @@ import { type DateRange } from '@/services/dashboard.service';
import { useAuth, hasManagementAccess } from '@/contexts/AuthContext';
import { useAppSelector, useAppDispatch } from '@/redux/hooks';
import { setViewAsUser } from './redux/dashboardSlice';
import { TokenManager } from '@/utils/tokenManager';
// Custom Hooks
import { useDashboardFilters } from './hooks/useDashboardFilters';
@ -161,8 +162,19 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRange, customStartDate, customEndDate, viewAsUser]);
// Check if user is a Dealer
const isDealer = useMemo(() => {
try {
const userData = TokenManager.getUserData();
return userData?.jobTitle === 'Dealer';
} catch (error) {
console.error('[Dashboard] Error checking dealer status:', error);
return false;
}
}, []);
// Quick actions
const quickActions = useMemo(() => getQuickActions(isAdmin, onNewRequest, onNavigate), [isAdmin, onNewRequest, onNavigate]);
const quickActions = useMemo(() => getQuickActions(isAdmin, onNewRequest, onNavigate, isDealer), [isAdmin, onNewRequest, onNavigate, isDealer]);
// KPI click handler
const handleKPIClick = useCallback((filters: {

View File

@ -50,16 +50,23 @@ export interface QuickAction {
export function getQuickActions(
isAdmin: boolean,
onNewRequest?: () => void,
onNavigate?: (page: string) => void
onNavigate?: (page: string) => void,
isDealer?: boolean
): QuickAction[] {
const actions: QuickAction[] = [
{ label: 'New Request', icon: FileText, action: () => onNewRequest?.(), color: 'bg-emerald-600 hover:bg-emerald-700' },
const actions: QuickAction[] = [];
// Only add "New Request" action if user is not a dealer
if (!isDealer) {
actions.push({ label: 'New Request', icon: FileText, action: () => onNewRequest?.(), color: 'bg-emerald-600 hover:bg-emerald-700' });
}
actions.push(
{ label: 'View Pending', icon: Clock, action: () => onNavigate?.('open-requests'), color: 'bg-blue-600 hover:bg-blue-700' },
{ label: 'Settings', icon: Settings, action: () => onNavigate?.('settings'), color: 'bg-slate-600 hover:bg-slate-700' }
];
);
if (isAdmin) {
actions.splice(2, 0, { label: 'Reports', icon: Activity, action: () => onNavigate?.('detailed-reports'), color: 'bg-purple-600 hover:bg-purple-700' });
actions.splice(actions.length - 1, 0, { label: 'Reports', icon: Activity, action: () => onNavigate?.('detailed-reports'), color: 'bg-purple-600 hover:bg-purple-700' });
}
return actions;