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> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Templates</SelectItem> <SelectItem value="all">All Templates</SelectItem>
<SelectItem value="CUSTOM">Custom</SelectItem> <SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem> <SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>

View File

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

View File

@ -10,7 +10,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { DollarSign, Download, CircleCheckBig, Target } from 'lucide-react'; import { DollarSign, Download, CircleCheckBig, Target } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -31,7 +30,6 @@ interface IOBlockedDetails {
blockedDate: string; blockedDate: string;
blockedBy: string; // User who blocked blockedBy: string; // User who blocked
sapDocumentNumber: string; sapDocumentNumber: string;
ioRemark?: string; // IO remark
status: 'blocked' | 'released' | 'failed'; status: 'blocked' | 'released' | 'failed';
} }
@ -42,7 +40,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
// Load existing IO data from apiRequest or request // Load existing IO data from apiRequest or request
const internalOrder = apiRequest?.internalOrder || apiRequest?.internal_order || null; const internalOrder = apiRequest?.internalOrder || apiRequest?.internal_order || null;
const existingIONumber = internalOrder?.ioNumber || internalOrder?.io_number || request?.ioNumber || ''; 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 existingBlockedAmount = internalOrder?.ioBlockedAmount || internalOrder?.io_blocked_amount || 0;
const existingAvailableBalance = internalOrder?.ioAvailableBalance || internalOrder?.io_available_balance || 0; const existingAvailableBalance = internalOrder?.ioAvailableBalance || internalOrder?.io_available_balance || 0;
const existingRemainingBalance = internalOrder?.ioRemainingBalance || internalOrder?.io_remaining_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 organizer = internalOrder?.organizer || null;
const [ioNumber, setIoNumber] = useState(existingIONumber); const [ioNumber, setIoNumber] = useState(existingIONumber);
const [ioRemark, setIoRemark] = useState(existingIORemark);
const [fetchingAmount, setFetchingAmount] = useState(false); const [fetchingAmount, setFetchingAmount] = useState(false);
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null); const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
const [amountToBlock, setAmountToBlock] = useState<string>(''); const [amountToBlock, setAmountToBlock] = useState<string>('');
const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(null); const [blockedDetails, setBlockedDetails] = useState<IOBlockedDetails | null>(null);
const [blockingBudget, setBlockingBudget] = useState(false); const [blockingBudget, setBlockingBudget] = useState(false);
const maxIoRemarkChars = 300;
const ioRemarkChars = ioRemark.length;
// Load existing IO block details from apiRequest // Load existing IO block details from apiRequest
useEffect(() => { useEffect(() => {
@ -78,9 +71,8 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
organizer?.email || organizer?.email ||
'Unknown User'; 'Unknown User';
// Set IO number and remark from existing data // Set IO number from existing data
setIoNumber(existingIONumber); setIoNumber(existingIONumber);
setIoRemark(existingIORemark);
// Only set blocked details if amount is blocked // Only set blocked details if amount is blocked
if (existingBlockedAmount > 0) { if (existingBlockedAmount > 0) {
@ -112,7 +104,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
blockedDate: internalOrder.organizedAt || internalOrder.organized_at || new Date().toISOString(), blockedDate: internalOrder.organizedAt || internalOrder.organized_at || new Date().toISOString(),
blockedBy: blockedByName, blockedBy: blockedByName,
sapDocumentNumber: sapDocNumber, sapDocumentNumber: sapDocNumber,
ioRemark: existingIORemark,
status: (internalOrder.status === 'BLOCKED' ? 'blocked' : status: (internalOrder.status === 'BLOCKED' ? 'blocked' :
internalOrder.status === 'RELEASED' ? 'released' : 'blocked') as 'blocked' | 'released' | 'failed', 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 * 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 * 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 () => { const handleBlockBudget = async () => {
if (!ioNumber.trim() || fetchedAmount === null) { if (!ioNumber.trim() || fetchedAmount === null) {
@ -217,11 +170,6 @@ 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;
@ -234,9 +182,9 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
return; return;
} }
// Round to 2 decimal places to avoid floating point precision issues // Round to exactly 2 decimal places to avoid floating point precision issues
// This ensures we send clean values like 240.00 instead of 239.9999999 // Use parseFloat with toFixed to ensure exact 2 decimal precision
const blockAmount = Math.round(blockAmountRaw * 100) / 100; const blockAmount = parseFloat(blockAmountRaw.toFixed(2));
if (blockAmount > fetchedAmount) { if (blockAmount > fetchedAmount) {
toast.error('Amount to block exceeds available IO budget'); 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 // Call updateIODetails with blockedAmount to block budget in SAP and store in database
// This will store in internal_orders and claim_budget_tracking tables // 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 // 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 = { const payload = {
ioNumber: ioNumber.trim(), ioNumber: ioNumber.trim(),
ioRemark: ioRemark.trim(), ioAvailableBalance: roundedFetchedAmount,
ioAvailableBalance: fetchedAmount,
ioBlockedAmount: blockAmount, 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 // Sending to backend
@ -301,8 +251,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
currentUser?.email || currentUser?.email ||
'Current User'; 'Current User';
const savedIoRemark = updatedInternalOrder.ioRemark || updatedInternalOrder.io_remark || ioRemark.trim();
const blocked: IOBlockedDetails = { const blocked: IOBlockedDetails = {
ioNumber: updatedInternalOrder.ioNumber || updatedInternalOrder.io_number || ioNumber, ioNumber: updatedInternalOrder.ioNumber || updatedInternalOrder.io_number || ioNumber,
blockedAmount: savedBlockedAmount, blockedAmount: savedBlockedAmount,
@ -311,7 +259,6 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
blockedDate: updatedInternalOrder.organizedAt || updatedInternalOrder.organized_at || new Date().toISOString(), blockedDate: updatedInternalOrder.organizedAt || updatedInternalOrder.organized_at || new Date().toISOString(),
blockedBy: blockedByName, blockedBy: blockedByName,
sapDocumentNumber: updatedInternalOrder.sapDocumentNumber || updatedInternalOrder.sap_document_number || '', sapDocumentNumber: updatedInternalOrder.sapDocumentNumber || updatedInternalOrder.sap_document_number || '',
ioRemark: savedIoRemark,
status: 'blocked', status: 'blocked',
}; };
@ -371,52 +318,13 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
</div> </div>
</div> </div>
{/* IO Remark Input */} {/* Instructions when IO number is entered but not fetched */}
<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) */}
{!fetchedAmount && !blockedDetails && ioNumber.trim() && ( {!fetchedAmount && !blockedDetails && ioNumber.trim() && (
<Button <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
onClick={handleSaveIODetails} <p className="text-sm text-blue-800">
disabled={blockingBudget || !ioNumber.trim() || !ioRemark.trim()} <strong>Next Step:</strong> Click "Fetch Amount" to validate the IO number and get available balance from SAP.
variant="outline" </p>
className="w-full border-[#2d4a3e] text-[#2d4a3e] hover:bg-[#2d4a3e] hover:text-white" </div>
>
{blockingBudget ? 'Saving...' : 'Save IO Details'}
</Button>
)} )}
{/* Fetched Amount Display */} {/* Fetched Amount Display */}
@ -459,15 +367,12 @@ 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 || !ioRemark.trim()} disabled={blockingBudget || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount}
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>
@ -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-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> <p className="text-sm font-semibold text-gray-900">{blockedDetails.sapDocumentNumber || 'N/A'}</p>
</div> </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"> <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-xs text-gray-500 uppercase tracking-wide mb-1">Blocked Amount</p>
<p className="text-xl font-bold text-green-700"> <p className="text-xl font-bold text-green-700">

View File

@ -47,7 +47,6 @@ interface WorkflowStep {
// Special fields for dealer claims // Special fields for dealer claims
ioDetails?: { ioDetails?: {
ioNumber: string; ioNumber: string;
ioRemark: string;
organizedBy: string; organizedBy: string;
organizedAt: string; organizedAt: string;
blockedAmount?: number; blockedAmount?: number;
@ -422,19 +421,8 @@ export function DealerClaimWorkflowTab({
const internalOrder = request?.internalOrder || request?.internal_order; const internalOrder = request?.internalOrder || request?.internal_order;
if (internalOrder?.ioNumber || internalOrder?.io_number) { 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 = { ioDetails = {
ioNumber: internalOrder.ioNumber || internalOrder.io_number || '', 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, blockedAmount: internalOrder.ioBlockedAmount || internalOrder.io_blocked_amount || 0,
availableBalance: internalOrder.ioAvailableBalance || internalOrder.io_available_balance || 0, availableBalance: internalOrder.ioAvailableBalance || internalOrder.io_available_balance || 0,
remainingBalance: internalOrder.ioRemainingBalance || internalOrder.io_remaining_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) // Handle IO approval (Department Lead step - found dynamically)
const handleIOApproval = async (data: { const handleIOApproval = async (data: {
ioNumber: string; ioNumber: string;
ioRemark: string;
comments: string; comments: string;
}) => { }) => {
try { try {
@ -846,17 +833,15 @@ export function DealerClaimWorkflowTab({
const levelId = step3Level.levelId || step3Level.level_id; const levelId = step3Level.levelId || step3Level.level_id;
// First, update IO details using dealer claim API // First, update IO details using dealer claim API (if needed)
// Only pass ioNumber and ioRemark - don't override existing balance values // Only pass ioNumber - don't override existing balance values
// Balance values should already be stored when amount was blocked earlier // Balance values should already be stored when amount was blocked earlier
await updateIODetails(requestId, { await updateIODetails(requestId, {
ioNumber: data.ioNumber, ioNumber: data.ioNumber,
ioRemark: data.ioRemark,
// Don't pass balance fields - let backend preserve existing values // Don't pass balance fields - let backend preserve existing values
}); });
// Approve Step 3 using real API // Approve Step 3 using real API
// IO remark is stored in claimDetails, so we just pass the comments
await approveLevel(requestId, levelId, data.comments); await approveLevel(requestId, levelId, data.comments);
// Activity is logged by backend approval service - no need to create work note // Activity is logged by backend approval service - no need to create work note
@ -1342,12 +1327,6 @@ export function DealerClaimWorkflowTab({
</span> </span>
</div> </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"> <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{' '} Organised by {step.ioDetails.organizedBy || step.approver || 'N/A'} on{' '}
{step.ioDetails.organizedAt {step.ioDetails.organizedAt
@ -1424,6 +1403,8 @@ export function DealerClaimWorkflowTab({
</Button> </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 */} {/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */} {/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
{step.step === initiatorStepNumber && (isInitiator || isStep2Approver) && ( {step.step === initiatorStepNumber && (isInitiator || isStep2Approver) && (
@ -1724,6 +1705,7 @@ export function DealerClaimWorkflowTab({
dealerName={dealerName} dealerName={dealerName}
activityName={activityName} activityName={activityName}
requestId={request?.id || request?.requestId} requestId={request?.id || request?.requestId}
request={request}
/> />
{/* Dept Lead IO Approval Modal */} {/* Dept Lead IO Approval Modal */}
@ -1735,7 +1717,6 @@ export function DealerClaimWorkflowTab({
requestTitle={request?.title} requestTitle={request?.title}
requestId={request?.id || request?.requestId} requestId={request?.id || request?.requestId}
preFilledIONumber={request?.internalOrder?.ioNumber || request?.internalOrder?.io_number || request?.internal_order?.ioNumber || request?.internal_order?.io_number || undefined} 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} 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} 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; onClose: () => void;
onApprove: (data: { onApprove: (data: {
ioNumber: string; ioNumber: string;
ioRemark: string;
comments: string; comments: string;
}) => Promise<void>; }) => Promise<void>;
onReject: (comments: string) => Promise<void>; onReject: (comments: string) => Promise<void>;
@ -36,7 +35,6 @@ interface DeptLeadIOApprovalModalProps {
requestId?: string; requestId?: string;
// Pre-filled IO data from IO table // Pre-filled IO data from IO table
preFilledIONumber?: string; preFilledIONumber?: string;
preFilledIORemark?: string;
preFilledBlockedAmount?: number; preFilledBlockedAmount?: number;
preFilledRemainingBalance?: number; preFilledRemainingBalance?: number;
} }
@ -49,12 +47,10 @@ export function DeptLeadIOApprovalModal({
requestTitle, requestTitle,
requestId: _requestId, requestId: _requestId,
preFilledIONumber, preFilledIONumber,
preFilledIORemark,
preFilledBlockedAmount, preFilledBlockedAmount,
preFilledRemainingBalance, preFilledRemainingBalance,
}: DeptLeadIOApprovalModalProps) { }: DeptLeadIOApprovalModalProps) {
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve'); const [actionType, setActionType] = useState<'approve' | 'reject'>('approve');
const [ioRemark, setIoRemark] = useState('');
const [comments, setComments] = useState(''); const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@ -64,16 +60,12 @@ export function DeptLeadIOApprovalModal({
// Reset form when modal opens/closes // Reset form when modal opens/closes
React.useEffect(() => { React.useEffect(() => {
if (isOpen) { if (isOpen) {
// Prefill IO remark from props if available
setIoRemark(preFilledIORemark || '');
setComments(''); setComments('');
setActionType('approve'); setActionType('approve');
} }
}, [isOpen, preFilledIORemark]); }, [isOpen]);
const ioRemarkChars = ioRemark.length;
const commentsChars = comments.length; const commentsChars = comments.length;
const maxIoRemarkChars = 300;
const maxCommentsChars = 500; const maxCommentsChars = 500;
// Validate form // Validate form
@ -81,13 +73,12 @@ export function DeptLeadIOApprovalModal({
if (actionType === 'reject') { if (actionType === 'reject') {
return comments.trim().length > 0; 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 ( return (
ioNumber.trim().length > 0 && // IO number must exist from IO table ioNumber.trim().length > 0 && // IO number must exist from IO table
ioRemark.trim().length > 0 &&
comments.trim().length > 0 comments.trim().length > 0
); );
}, [actionType, ioNumber, ioRemark, comments]); }, [actionType, ioNumber, comments]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!isFormValid) { if (!isFormValid) {
@ -96,10 +87,6 @@ export function DeptLeadIOApprovalModal({
toast.error('IO number is required. Please block amount from IO tab first.'); toast.error('IO number is required. Please block amount from IO tab first.');
return; return;
} }
if (!ioRemark.trim()) {
toast.error('Please enter IO remark');
return;
}
} }
if (!comments.trim()) { if (!comments.trim()) {
toast.error('Please provide comments'); toast.error('Please provide comments');
@ -114,7 +101,6 @@ export function DeptLeadIOApprovalModal({
if (actionType === 'approve') { if (actionType === 'approve') {
await onApprove({ await onApprove({
ioNumber: ioNumber.trim(), ioNumber: ioNumber.trim(),
ioRemark: ioRemark.trim(),
comments: comments.trim(), comments: comments.trim(),
}); });
} else { } else {
@ -133,7 +119,6 @@ export function DeptLeadIOApprovalModal({
const handleReset = () => { const handleReset = () => {
setActionType('approve'); setActionType('approve');
setIoRemark('');
setComments(''); setComments('');
}; };
@ -275,33 +260,6 @@ export function DeptLeadIOApprovalModal({
)} )}
</div> </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> </div>
)} )}

View File

@ -64,6 +64,7 @@ interface InitiatorProposalApprovalModalProps {
dealerName?: string; dealerName?: string;
activityName?: string; activityName?: string;
requestId?: string; requestId?: string;
request?: any; // Request object to check IO blocking status
} }
export function InitiatorProposalApprovalModal({ export function InitiatorProposalApprovalModal({
@ -75,10 +76,16 @@ export function InitiatorProposalApprovalModal({
dealerName = 'Dealer', dealerName = 'Dealer',
activityName = 'Activity', activityName = 'Activity',
requestId: _requestId, // Prefix with _ to indicate intentionally unused requestId: _requestId, // Prefix with _ to indicate intentionally unused
request,
}: InitiatorProposalApprovalModalProps) { }: InitiatorProposalApprovalModalProps) {
const [comments, setComments] = useState(''); const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [actionType, setActionType] = useState<'approve' | 'reject' | null>(null); 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<{ const [previewDocument, setPreviewDocument] = useState<{
name: string; name: string;
url: string; url: string;
@ -545,45 +552,54 @@ export function InitiatorProposalApprovalModal({
</div> </div>
</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 <Button
variant="outline" variant="outline"
onClick={handleClose} onClick={handleClose}
disabled={submitting} disabled={submitting}
className="border-2" className="border-2 w-full sm:w-auto"
> >
Cancel Cancel
</Button> </Button>
<div className="flex gap-2"> <div className="flex flex-col gap-2 w-full sm:w-auto">
<Button <div className="flex flex-col sm:flex-row gap-2">
onClick={handleReject} <Button
disabled={!comments.trim() || submitting} onClick={handleReject}
variant="destructive" disabled={!comments.trim() || submitting}
className="bg-red-600 hover:bg-red-700" variant="destructive"
> className="bg-red-600 hover:bg-red-700 w-full sm:w-auto"
{submitting && actionType === 'reject' ? ( >
'Rejecting...' {submitting && actionType === 'reject' ? (
) : ( 'Rejecting...'
<> ) : (
<XCircle className="w-4 h-4 mr-2" /> <>
Reject (Cancel Request) <XCircle className="w-4 h-4 mr-2" />
</> Reject (Cancel Request)
)} </>
</Button> )}
<Button </Button>
onClick={handleApprove} <Button
disabled={!comments.trim() || submitting} onClick={handleApprove}
className="bg-green-600 hover:bg-green-700 text-white" 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"
{submitting && actionType === 'approve' ? ( title={!isIOBlocked ? 'Please block IO budget before approving' : ''}
'Approving...' >
) : ( {submitting && actionType === 'approve' ? (
<> 'Approving...'
<CheckCircle className="w-4 h-4 mr-2" /> ) : (
Approve (Continue to Dept Lead) <>
</> <CheckCircle className="w-4 h-4 mr-2" />
)} Approve (Continue to Dept Lead)
</Button> </>
)}
</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> </div>
</DialogFooter> </DialogFooter>
</DialogContent> </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) // Determine if user is department lead (find dynamically by levelName, not hardcoded step number)
const currentUserId = (user as any)?.userId || ''; 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 // IO tab visibility for dealer claims
// Show IO tab only for department lead (found dynamically, not hardcoded to step 3) // Show IO tab for initiator (Requestor Evaluation level) - initiator can now fetch and block IO
const showIOTab = isDeptLead; const showIOTab = isInitiator;
const { const {
mergedMessages, mergedMessages,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -80,12 +80,27 @@ export function RequestDetailHeader({
{/* Template Type Badge */} {/* Template Type Badge */}
{(() => { {(() => {
const workflowType = request?.workflowType || request?.workflow_type; const workflowType = request?.workflowType || request?.workflow_type;
const templateType = request?.templateType || request?.template_type; const templateType = request?.templateType || request?.template_type || '';
const isClaimManagement = workflowType === 'CLAIM_MANAGEMENT' || templateType === 'claim-management'; const templateTypeUpper = templateType?.toUpperCase() || '';
const templateLabel = isClaimManagement ? 'Claim Management' : 'Custom';
const templateColor = isClaimManagement // Check for dealer claim - support multiple formats
? 'bg-blue-100 !text-blue-700 border-blue-200' const isDealerClaim =
: 'bg-purple-100 !text-purple-600 border-purple-200'; 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 ( return (
<Badge <Badge

View File

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

View File

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