block io move to request evaluation step custom shown as non-templatized for the user

This commit is contained in:
laxmanhalaki 2026-01-08 19:19:52 +05:30
parent d725e523b3
commit 4c3d7fd28b
16 changed files with 110 additions and 265 deletions

View File

@ -137,7 +137,7 @@ export function StandardClosedRequestsFilters({
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Templates</SelectItem>
<SelectItem value="CUSTOM">Custom</SelectItem>
<SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent>
</Select>

View File

@ -126,7 +126,7 @@ export function StandardRequestsFilters({
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Templates</SelectItem>
<SelectItem value="CUSTOM">Custom</SelectItem>
<SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent>
</Select>

View File

@ -10,7 +10,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { DollarSign, Download, CircleCheckBig, Target } from 'lucide-react';
import { toast } from 'sonner';
@ -31,7 +30,6 @@ interface IOBlockedDetails {
blockedDate: string;
blockedBy: string; // User who blocked
sapDocumentNumber: string;
ioRemark?: string; // IO remark
status: 'blocked' | 'released' | 'failed';
}
@ -42,7 +40,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
// Load existing IO data from apiRequest or request
const internalOrder = apiRequest?.internalOrder || apiRequest?.internal_order || null;
const existingIONumber = internalOrder?.ioNumber || internalOrder?.io_number || request?.ioNumber || '';
const existingIORemark = internalOrder?.ioRemark || internalOrder?.io_remark || '';
const existingBlockedAmount = internalOrder?.ioBlockedAmount || internalOrder?.io_blocked_amount || 0;
const existingAvailableBalance = internalOrder?.ioAvailableBalance || internalOrder?.io_available_balance || 0;
const existingRemainingBalance = internalOrder?.ioRemainingBalance || internalOrder?.io_remaining_balance || 0;
@ -51,15 +48,11 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
const organizer = internalOrder?.organizer || null;
const [ioNumber, setIoNumber] = useState(existingIONumber);
const [ioRemark, setIoRemark] = useState(existingIORemark);
const [fetchingAmount, setFetchingAmount] = useState(false);
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
const [amountToBlock, setAmountToBlock] = useState<string>('');
const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(null);
const [blockingBudget, setBlockingBudget] = useState(false);
const maxIoRemarkChars = 300;
const ioRemarkChars = ioRemark.length;
// Load existing IO block details from apiRequest
useEffect(() => {
@ -78,9 +71,8 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
organizer?.email ||
'Unknown User';
// Set IO number and remark from existing data
// Set IO number from existing data
setIoNumber(existingIONumber);
setIoRemark(existingIORemark);
// Only set blocked details if amount is blocked
if (existingBlockedAmount > 0) {
@ -112,7 +104,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
blockedDate: internalOrder.organizedAt || internalOrder.organized_at || new Date().toISOString(),
blockedBy: blockedByName,
sapDocumentNumber: sapDocNumber,
ioRemark: existingIORemark,
status: (internalOrder.status === 'BLOCKED' ? 'blocked' :
internalOrder.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed',
});
@ -123,7 +114,7 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
}
}
}
}, [internalOrder, existingIONumber, existingIORemark, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber, organizer]);
}, [internalOrder, existingIONumber, existingBlockedAmount, existingAvailableBalance, existingRemainingBalance, sapDocNumber, organizer]);
/**
* Fetch available budget from SAP
@ -166,50 +157,12 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
}
};
/**
* Save IO details (IO number and remark) without blocking budget
*/
const handleSaveIODetails = async () => {
if (!ioNumber.trim()) {
toast.error('Please enter an IO number');
return;
}
if (!ioRemark.trim()) {
toast.error('Please enter an IO remark');
return;
}
if (!requestId) {
toast.error('Request ID not found');
return;
}
setBlockingBudget(true);
try {
// Save only IO number and remark (no balance fields)
const payload = {
ioNumber: ioNumber.trim(),
ioRemark: ioRemark.trim(),
};
await updateIODetails(requestId, payload);
toast.success('IO details saved successfully');
// Refresh request details
onRefresh?.();
} catch (error: any) {
console.error('Failed to save IO details:', error);
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to save IO details';
toast.error(errorMessage);
} finally {
setBlockingBudget(false);
}
};
/**
* Block budget in SAP system
* This function:
* 1. Validates the IO number and amount
* 2. Calls SAP to block the budget
* 3. Saves IO number, blocked amount, and balance details to database
*/
const handleBlockBudget = async () => {
if (!ioNumber.trim() || fetchedAmount === null) {
@ -217,11 +170,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
return;
}
if (!ioRemark.trim()) {
toast.error('Please enter IO remark before blocking the amount');
return;
}
if (!requestId) {
toast.error('Request ID not found');
return;
@ -234,9 +182,9 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
return;
}
// Round to 2 decimal places to avoid floating point precision issues
// This ensures we send clean values like 240.00 instead of 239.9999999
const blockAmount = Math.round(blockAmountRaw * 100) / 100;
// Round to exactly 2 decimal places to avoid floating point precision issues
// Use parseFloat with toFixed to ensure exact 2 decimal precision
const blockAmount = parseFloat(blockAmountRaw.toFixed(2));
if (blockAmount > fetchedAmount) {
toast.error('Amount to block exceeds available IO budget');
@ -250,12 +198,14 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
// Call updateIODetails with blockedAmount to block budget in SAP and store in database
// This will store in internal_orders and claim_budget_tracking tables
// Note: Backend will recalculate remainingBalance from SAP's response, so we pass it for reference only
// Ensure all amounts are rounded to 2 decimal places for consistency
const roundedFetchedAmount = parseFloat(fetchedAmount.toFixed(2));
const calculatedRemaining = parseFloat((roundedFetchedAmount - blockAmount).toFixed(2));
const payload = {
ioNumber: ioNumber.trim(),
ioRemark: ioRemark.trim(),
ioAvailableBalance: fetchedAmount,
ioAvailableBalance: roundedFetchedAmount,
ioBlockedAmount: blockAmount,
ioRemainingBalance: fetchedAmount - blockAmount, // Calculated value (backend will use SAP's actual value)
ioRemainingBalance: calculatedRemaining, // Calculated value (backend will use SAP's actual value)
};
// Sending to backend
@ -301,8 +251,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
currentUser?.email ||
'Current User';
const savedIoRemark = updatedInternalOrder.ioRemark || updatedInternalOrder.io_remark || ioRemark.trim();
const blocked: IOBlockedDetails = {
ioNumber: updatedInternalOrder.ioNumber || updatedInternalOrder.io_number || ioNumber,
blockedAmount: savedBlockedAmount,
@ -311,7 +259,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
blockedDate: updatedInternalOrder.organizedAt || updatedInternalOrder.organized_at || new Date().toISOString(),
blockedBy: blockedByName,
sapDocumentNumber: updatedInternalOrder.sapDocumentNumber || updatedInternalOrder.sap_document_number || '',
ioRemark: savedIoRemark,
status: 'blocked',
};
@ -371,52 +318,13 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
</div>
</div>
{/* IO Remark Input */}
<div className="space-y-2">
<Label htmlFor="ioRemark" className="text-sm font-medium text-gray-900">
IO Remark <span className="text-red-500">*</span>
</Label>
<Textarea
id="ioRemark"
placeholder="Enter remarks about IO organization (required before blocking amount)"
value={ioRemark}
onChange={(e) => {
const value = e.target.value;
if (value.length <= maxIoRemarkChars) {
setIoRemark(value);
}
}}
rows={3}
disabled={!!blockedDetails}
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
/>
<div className="flex justify-between items-center">
<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>
{/* Save IO Details Button (shown when IO number is entered but amount not fetched) */}
{/* Instructions when IO number is entered but not fetched */}
{!fetchedAmount && !blockedDetails && ioNumber.trim() && (
<Button
onClick={handleSaveIODetails}
disabled={blockingBudget || !ioNumber.trim() || !ioRemark.trim()}
variant="outline"
className="w-full border-[#2d4a3e] text-[#2d4a3e] hover:bg-[#2d4a3e] hover:text-white"
>
{blockingBudget ? 'Saving...' : 'Save IO Details'}
</Button>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
<strong>Next Step:</strong> Click "Fetch Amount" to validate the IO number and get available balance from SAP.
</p>
</div>
)}
{/* Fetched Amount Display */}
@ -459,15 +367,12 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
{/* Block Button */}
<Button
onClick={handleBlockBudget}
disabled={blockingBudget || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount || !ioRemark.trim()}
disabled={blockingBudget || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount}
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]"
>
<Target className="w-4 h-4 mr-2" />
{blockingBudget ? 'Blocking in SAP...' : 'Block IO in SAP'}
</Button>
{!ioRemark.trim() && (
<p className="text-xs text-red-600 mt-1">Please enter IO remark before blocking the amount</p>
)}
</>
)}
</CardContent>
@ -508,12 +413,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">SAP Document Number</p>
<p className="text-sm font-semibold text-gray-900">{blockedDetails.sapDocumentNumber || 'N/A'}</p>
</div>
{blockedDetails.ioRemark && (
<div className="p-4">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">IO Remark</p>
<p className="text-sm font-medium text-gray-900 whitespace-pre-wrap">{blockedDetails.ioRemark}</p>
</div>
)}
<div className="p-4 bg-green-50">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Blocked Amount</p>
<p className="text-xl font-bold text-green-700">

View File

@ -47,7 +47,6 @@ interface WorkflowStep {
// Special fields for dealer claims
ioDetails?: {
ioNumber: string;
ioRemark: string;
organizedBy: string;
organizedAt: string;
blockedAmount?: number;
@ -422,19 +421,8 @@ export function DealerClaimWorkflowTab({
const internalOrder = request?.internalOrder || request?.internal_order;
if (internalOrder?.ioNumber || internalOrder?.io_number) {
// Try multiple field name variations for ioRemark
const ioRemarkValue =
internalOrder.ioRemark ||
internalOrder.io_remark ||
internalOrder.IORemark ||
internalOrder.IO_Remark ||
(internalOrder as any)?.ioRemark ||
(internalOrder as any)?.io_remark ||
'';
ioDetails = {
ioNumber: internalOrder.ioNumber || internalOrder.io_number || '',
ioRemark: (ioRemarkValue && typeof ioRemarkValue === 'string' && ioRemarkValue.trim()) ? ioRemarkValue.trim() : 'N/A',
blockedAmount: internalOrder.ioBlockedAmount || internalOrder.io_blocked_amount || 0,
availableBalance: internalOrder.ioAvailableBalance || internalOrder.io_available_balance || 0,
remainingBalance: internalOrder.ioRemainingBalance || internalOrder.io_remaining_balance || 0,
@ -818,7 +806,6 @@ export function DealerClaimWorkflowTab({
// Handle IO approval (Department Lead step - found dynamically)
const handleIOApproval = async (data: {
ioNumber: string;
ioRemark: string;
comments: string;
}) => {
try {
@ -846,17 +833,15 @@ export function DealerClaimWorkflowTab({
const levelId = step3Level.levelId || step3Level.level_id;
// First, update IO details using dealer claim API
// Only pass ioNumber and ioRemark - don't override existing balance values
// First, update IO details using dealer claim API (if needed)
// Only pass ioNumber - don't override existing balance values
// Balance values should already be stored when amount was blocked earlier
await updateIODetails(requestId, {
ioNumber: data.ioNumber,
ioRemark: data.ioRemark,
// Don't pass balance fields - let backend preserve existing values
});
// Approve Step 3 using real API
// IO remark is stored in claimDetails, so we just pass the comments
await approveLevel(requestId, levelId, data.comments);
// Activity is logged by backend approval service - no need to create work note
@ -1342,12 +1327,6 @@ export function DealerClaimWorkflowTab({
</span>
</div>
)}
<div className="pt-1.5 border-t border-blue-100">
<p className="text-xs text-gray-600 mb-1">IO Remark:</p>
<p className="text-sm text-gray-900">
{step.ioDetails.ioRemark || 'N/A'}
</p>
</div>
<div className="pt-1.5 border-t border-blue-100 text-xs text-gray-500">
Organised by {step.ioDetails.organizedBy || step.approver || 'N/A'} on{' '}
{step.ioDetails.organizedAt
@ -1424,6 +1403,8 @@ export function DealerClaimWorkflowTab({
</Button>
)}
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
{step.step === initiatorStepNumber && (isInitiator || isStep2Approver) && (
@ -1724,6 +1705,7 @@ export function DealerClaimWorkflowTab({
dealerName={dealerName}
activityName={activityName}
requestId={request?.id || request?.requestId}
request={request}
/>
{/* Dept Lead IO Approval Modal */}
@ -1735,7 +1717,6 @@ export function DealerClaimWorkflowTab({
requestTitle={request?.title}
requestId={request?.id || request?.requestId}
preFilledIONumber={request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number || undefined}
preFilledIORemark={request?.internalOrder?.ioRemark || request?.internalOrder?.io_remark || request?.internal_order?.ioRemark || request?.internal_order?.io_remark || undefined}
preFilledBlockedAmount={request?.internalOrder?.ioBlockedAmount || request?.internalOrder?.io_blocked_amount || request?.internal_order?.ioBlockedAmount || request?.internal_order?.io_blocked_amount || undefined}
preFilledRemainingBalance={request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance || undefined}
/>

View File

@ -28,7 +28,6 @@ interface DeptLeadIOApprovalModalProps {
onClose: () => void;
onApprove: (data: {
ioNumber: string;
ioRemark: string;
comments: string;
}) => Promise<void>;
onReject: (comments: string) => Promise<void>;
@ -36,7 +35,6 @@ interface DeptLeadIOApprovalModalProps {
requestId?: string;
// Pre-filled IO data from IO table
preFilledIONumber?: string;
preFilledIORemark?: string;
preFilledBlockedAmount?: number;
preFilledRemainingBalance?: number;
}
@ -49,12 +47,10 @@ export function DeptLeadIOApprovalModal({
requestTitle,
requestId: _requestId,
preFilledIONumber,
preFilledIORemark,
preFilledBlockedAmount,
preFilledRemainingBalance,
}: DeptLeadIOApprovalModalProps) {
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
const [ioRemark, setIoRemark] = useState('');
const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false);
@ -64,16 +60,12 @@ export function DeptLeadIOApprovalModal({
// Reset form when modal opens/closes
React.useEffect(() => {
if (isOpen) {
// Prefill IO remark from props if available
setIoRemark(preFilledIORemark || '');
setComments('');
setActionType('approve');
}
}, [isOpen, preFilledIORemark]);
}, [isOpen]);
const ioRemarkChars = ioRemark.length;
const commentsChars = comments.length;
const maxIoRemarkChars = 300;
const maxCommentsChars = 500;
// Validate form
@ -81,13 +73,12 @@ export function DeptLeadIOApprovalModal({
if (actionType === 'reject') {
return comments.trim().length > 0;
}
// For approve, need IO number (from table), IO remark, and comments
// For approve, need IO number (from table) and comments
return (
ioNumber.trim().length > 0 && // IO number must exist from IO table
ioRemark.trim().length > 0 &&
comments.trim().length > 0
);
}, [actionType, ioNumber, ioRemark, comments]);
}, [actionType, ioNumber, comments]);
const handleSubmit = async () => {
if (!isFormValid) {
@ -96,10 +87,6 @@ export function DeptLeadIOApprovalModal({
toast.error('IO number is required. Please block amount from IO tab first.');
return;
}
if (!ioRemark.trim()) {
toast.error('Please enter IO remark');
return;
}
}
if (!comments.trim()) {
toast.error('Please provide comments');
@ -114,7 +101,6 @@ export function DeptLeadIOApprovalModal({
if (actionType === 'approve') {
await onApprove({
ioNumber: ioNumber.trim(),
ioRemark: ioRemark.trim(),
comments: comments.trim(),
});
} else {
@ -133,7 +119,6 @@ export function DeptLeadIOApprovalModal({
const handleReset = () => {
setActionType('approve');
setIoRemark('');
setComments('');
};
@ -275,33 +260,6 @@ export function DeptLeadIOApprovalModal({
)}
</div>
{/* IO Remark - Read-only field (prefilled from IO tab, cannot be modified) */}
<div className="space-y-1">
<Label htmlFor="ioRemark" className="text-xs lg:text-sm font-semibold text-gray-900 flex items-center gap-2">
IO Remark <span className="text-red-500">*</span>
</Label>
<Textarea
id="ioRemark"
placeholder="Enter remarks about IO organization"
value={ioRemark || '—'}
disabled
readOnly
rows={3}
className="bg-gray-100 text-xs lg:text-sm min-h-[80px] lg:min-h-[100px] resize-none cursor-not-allowed"
/>
<div className="flex items-center justify-between text-xs">
{preFilledIORemark ? (
<span className="text-blue-600">
Loaded from IO tab
</span>
) : (
<span className="text-red-600">
IO remark not found. Please add remark in IO tab first.
</span>
)}
<span className="text-gray-600">{ioRemarkChars}/{maxIoRemarkChars}</span>
</div>
</div>
</div>
)}

View File

@ -64,6 +64,7 @@ interface InitiatorProposalApprovalModalProps {
dealerName?: string;
activityName?: string;
requestId?: string;
request?: any; // Request object to check IO blocking status
}
export function InitiatorProposalApprovalModal({
@ -75,10 +76,16 @@ export function InitiatorProposalApprovalModal({
dealerName = 'Dealer',
activityName = 'Activity',
requestId: _requestId, // Prefix with _ to indicate intentionally unused
request,
}: InitiatorProposalApprovalModalProps) {
const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false);
const [actionType, setActionType] = useState<'approve' | 'reject' | null>(null);
// Check if IO is blocked (IO blocking moved to Requestor Evaluation level)
const internalOrder = request?.internalOrder || request?.internal_order;
const ioBlockedAmount = internalOrder?.ioBlockedAmount || internalOrder?.io_blocked_amount || 0;
const isIOBlocked = ioBlockedAmount > 0;
const [previewDocument, setPreviewDocument] = useState<{
name: string;
url: string;
@ -545,45 +552,54 @@ export function InitiatorProposalApprovalModal({
</div>
</div>
<DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end px-6 pb-6 pt-3 lg:pt-4 flex-shrink-0 border-t bg-gray-50">
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-end px-6 pb-6 pt-3 lg:pt-4 flex-shrink-0 border-t bg-gray-50">
<Button
variant="outline"
onClick={handleClose}
disabled={submitting}
className="border-2"
className="border-2 w-full sm:w-auto"
>
Cancel
</Button>
<div className="flex gap-2">
<Button
onClick={handleReject}
disabled={!comments.trim() || submitting}
variant="destructive"
className="bg-red-600 hover:bg-red-700"
>
{submitting && actionType === 'reject' ? (
'Rejecting...'
) : (
<>
<XCircle className="w-4 h-4 mr-2" />
Reject (Cancel Request)
</>
)}
</Button>
<Button
onClick={handleApprove}
disabled={!comments.trim() || submitting}
className="bg-green-600 hover:bg-green-700 text-white"
>
{submitting && actionType === 'approve' ? (
'Approving...'
) : (
<>
<CheckCircle className="w-4 h-4 mr-2" />
Approve (Continue to Dept Lead)
</>
)}
</Button>
<div className="flex flex-col gap-2 w-full sm:w-auto">
<div className="flex flex-col sm:flex-row gap-2">
<Button
onClick={handleReject}
disabled={!comments.trim() || submitting}
variant="destructive"
className="bg-red-600 hover:bg-red-700 w-full sm:w-auto"
>
{submitting && actionType === 'reject' ? (
'Rejecting...'
) : (
<>
<XCircle className="w-4 h-4 mr-2" />
Reject (Cancel Request)
</>
)}
</Button>
<Button
onClick={handleApprove}
disabled={!comments.trim() || !isIOBlocked || submitting}
className="bg-green-600 hover:bg-green-700 text-white disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto"
title={!isIOBlocked ? 'Please block IO budget before approving' : ''}
>
{submitting && actionType === 'approve' ? (
'Approving...'
) : (
<>
<CheckCircle className="w-4 h-4 mr-2" />
Approve (Continue to Dept Lead)
</>
)}
</Button>
</div>
{/* Warning for IO not blocked - shown below Approve button */}
{!isIOBlocked && (
<p className="text-xs text-red-600 text-center sm:text-left">
Please block IO budget in the IO Tab before approving
</p>
)}
</div>
</DialogFooter>
</DialogContent>

View File

@ -153,34 +153,10 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
// Determine if user is department lead (find dynamically by levelName, not hardcoded step number)
const currentUserId = (user as any)?.userId || '';
const currentUserEmail = (user as any)?.email?.toLowerCase() || '';
// Use approvalFlow (transformed) or approvals (raw) - both have step/levelNumber
const approvalFlow = apiRequest?.approvalFlow || [];
const approvals = apiRequest?.approvals || [];
// Find Department Lead step dynamically by levelName (handles step shifts when approvers are added)
const deptLeadLevel = approvalFlow.find((level: any) => {
const levelName = (level.levelName || level.level_name || '').toLowerCase();
return levelName.includes('department lead');
}) || approvals.find((level: any) => {
const levelName = (level.levelName || level.level_name || '').toLowerCase();
return levelName.includes('department lead');
}) || approvalFlow.find((level: any) =>
(level.step || level.levelNumber || level.level_number) === 3
) || approvals.find((level: any) =>
(level.levelNumber || level.level_number) === 3
); // Fallback to step 3 for backwards compatibility
const deptLeadUserId = deptLeadLevel?.approverId || deptLeadLevel?.approver_id || deptLeadLevel?.approver?.userId;
const deptLeadEmail = (deptLeadLevel?.approverEmail || deptLeadLevel?.approver_email || deptLeadLevel?.approver?.email || '').toLowerCase().trim();
// User is department lead if they match the Department Lead approver (regardless of status or step number)
const isDeptLead = (deptLeadUserId && deptLeadUserId === currentUserId) ||
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail);
// IO tab visibility for dealer claims
// Show IO tab only for department lead (found dynamically, not hardcoded to step 3)
const showIOTab = isDeptLead;
// Show IO tab for initiator (Requestor Evaluation level) - initiator can now fetch and block IO
const showIOTab = isInitiator;
const {
mergedMessages,

View File

@ -67,7 +67,7 @@ export function ClosedRequestCard({ request, onViewRequest }: ClosedRequestCardP
const templateTypeUpper = templateType?.toUpperCase() || '';
// Direct mapping from templateType
let templateLabel = 'Custom';
let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
if (templateTypeUpper === 'DEALER CLAIM') {

View File

@ -135,7 +135,7 @@ export function ClosedRequestsFilters({
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Templates</SelectItem>
<SelectItem value="CUSTOM">Custom</SelectItem>
<SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent>
</Select>

View File

@ -8,7 +8,7 @@ import { Lightbulb, FileText } from 'lucide-react';
export const REQUEST_TEMPLATES: RequestTemplate[] = [
{
id: 'custom',
name: 'Custom Request',
name: 'Non-Templatized',
description:
'Create a custom request for unique business needs with full flexibility to define your own workflow and requirements',
category: 'General',

View File

@ -85,7 +85,7 @@ export function MyRequestsFilters({
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Templates</SelectItem>
<SelectItem value="CUSTOM">Custom</SelectItem>
<SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent>
</Select>

View File

@ -103,7 +103,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
const templateTypeUpper = templateType?.toUpperCase() || '';
// Direct mapping from templateType
let templateLabel = 'Custom';
let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
if (templateTypeUpper === 'DEALER CLAIM') {

View File

@ -393,7 +393,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const templateTypeUpper = templateType?.toUpperCase() || '';
// Direct mapping from templateType
let templateLabel = 'Custom';
let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
if (templateTypeUpper === 'DEALER CLAIM') {

View File

@ -80,12 +80,27 @@ export function RequestDetailHeader({
{/* Template Type Badge */}
{(() => {
const workflowType = request?.workflowType || request?.workflow_type;
const templateType = request?.templateType || request?.template_type;
const isClaimManagement = workflowType === 'CLAIM_MANAGEMENT' || templateType === 'claim-management';
const templateLabel = isClaimManagement ? 'Claim Management' : 'Custom';
const templateColor = isClaimManagement
? 'bg-blue-100 !text-blue-700 border-blue-200'
: 'bg-purple-100 !text-purple-600 border-purple-200';
const templateType = request?.templateType || request?.template_type || '';
const templateTypeUpper = templateType?.toUpperCase() || '';
// Check for dealer claim - support multiple formats
const isDealerClaim =
workflowType === 'CLAIM_MANAGEMENT' ||
workflowType === 'DEALER_CLAIM' ||
templateType === 'claim-management' ||
templateTypeUpper === 'DEALER CLAIM' ||
templateTypeUpper === 'DEALER_CLAIM';
// Direct mapping from templateType
let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
if (isDealerClaim) {
templateLabel = 'Dealer Claim';
templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
} else if (templateTypeUpper === 'TEMPLATE') {
templateLabel = 'Template';
}
return (
<Badge

View File

@ -563,7 +563,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Templates</SelectItem>
<SelectItem value="CUSTOM">Custom</SelectItem>
<SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent>
</Select>

View File

@ -102,7 +102,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
const templateTypeUpper = templateType?.toUpperCase() || '';
// Direct mapping from templateType
let templateLabel = 'Custom';
let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
if (templateTypeUpper === 'DEALER CLAIM') {