block io move to request evaluation step custom shown as non-templatized for the user
This commit is contained in:
parent
d725e523b3
commit
4c3d7fd28b
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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') {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user