laxman code merge

This commit is contained in:
Aaditya Jaiswal 2026-03-19 18:34:36 +05:30
commit a87c790a62
8 changed files with 85 additions and 22 deletions

View File

@ -1587,12 +1587,13 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
})()} })()}
<Textarea <Textarea
placeholder="Type your message... Use @username to mention someone" placeholder={effectiveIsSpectator ? "Spectators cannot send messages" : "Type your message... Use @username to mention someone"}
value={message} value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
className="min-h-[50px] sm:min-h-[60px] resize-none border-gray-200 focus:ring-blue-500 focus:border-blue-500 w-full text-sm" className="min-h-[50px] sm:min-h-[60px] resize-none border-gray-200 focus:ring-blue-500 focus:border-blue-500 w-full text-sm"
rows={2} rows={2}
disabled={effectiveIsSpectator}
/> />
{/* Emoji Picker Popup */} {/* Emoji Picker Popup */}
@ -1632,27 +1633,27 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-gray-500 h-8 w-8 p-0 hover:bg-blue-50 hover:text-blue-600 flex-shrink-0"
onClick={handleAttachmentClick} onClick={handleAttachmentClick}
title="Attach file" disabled={effectiveIsSpectator}
title={effectiveIsSpectator ? "Spectators cannot attach files" : "Attach file"}
> >
<Paperclip className="h-4 w-4" /> <Paperclip className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-gray-500 h-8 w-8 p-0 hover:bg-blue-50 hover:text-blue-600 flex-shrink-0"
onClick={() => setShowEmojiPicker(!showEmojiPicker)} onClick={() => setShowEmojiPicker(!showEmojiPicker)}
title="Add emoji" disabled={effectiveIsSpectator}
title={effectiveIsSpectator ? "Spectators cannot add emojis" : "Add emoji"}
> >
<Smile className="h-4 w-4" /> <Smile className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-gray-500 h-8 w-8 p-0 hover:bg-blue-50 hover:text-blue-600 flex-shrink-0"
onClick={() => setMessage(prev => prev + '@')} onClick={() => setMessage(prev => prev + '@')}
title="Mention someone" disabled={effectiveIsSpectator}
title={effectiveIsSpectator ? "Spectators cannot mention users" : "Mention someone"}
> >
<AtSign className="h-4 w-4" /> <AtSign className="h-4 w-4" />
</Button> </Button>
@ -1665,9 +1666,10 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
</span> </span>
<Button <Button
onClick={handleSendMessage} onClick={handleSendMessage}
disabled={!message.trim() && selectedFiles.length === 0} disabled={(!message.trim() && selectedFiles.length === 0) || effectiveIsSpectator}
className="bg-blue-600 hover:bg-blue-700 h-8 sm:h-9 px-3 sm:px-4 disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0" className="bg-blue-600 hover:bg-blue-700 h-8 sm:h-9 px-3 sm:px-4 disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
size="sm" size="sm"
title={effectiveIsSpectator ? "Spectators cannot send messages" : "Send"}
> >
<Send className="h-4 w-4 sm:mr-2" /> <Send className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Send</span> <span className="hidden sm:inline">Send</span>

View File

@ -588,6 +588,7 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq
triggerFileInput={triggerFileInput} triggerFileInput={triggerFileInput}
setPreviewDocument={setPreviewDocument} setPreviewDocument={setPreviewDocument}
downloadDocument={downloadDocument} downloadDocument={downloadDocument}
isSpectator={isSpectator}
/> />
</TabsContent> </TabsContent>

View File

@ -20,6 +20,7 @@ interface IOTabProps {
request: any; request: any;
apiRequest?: any; apiRequest?: any;
onRefresh?: () => void; onRefresh?: () => void;
isSpectator?: boolean;
} }
interface IOBlockedDetails { interface IOBlockedDetails {
@ -33,7 +34,7 @@ interface IOBlockedDetails {
status: 'blocked' | 'released' | 'failed' | 'pending'; status: 'blocked' | 'released' | 'failed' | 'pending';
} }
export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) { export function IOTab({ request, apiRequest, onRefresh, isSpectator = false }: IOTabProps) {
const { user } = useAuth(); const { user } = useAuth();
const requestId = apiRequest?.requestId || request?.requestId; const requestId = apiRequest?.requestId || request?.requestId;
@ -70,7 +71,11 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
const budgetTracking = apiRequest?.budgetTracking || request?.budgetTracking || {}; const budgetTracking = apiRequest?.budgetTracking || request?.budgetTracking || {};
const budgetStatus = budgetTracking?.budgetStatus || budgetTracking?.budget_status || ''; const budgetStatus = budgetTracking?.budgetStatus || budgetTracking?.budget_status || '';
const internalOrdersList = apiRequest?.internalOrders || apiRequest?.internal_orders || request?.internalOrders || []; const internalOrdersList = apiRequest?.internalOrders || apiRequest?.internal_orders || request?.internalOrders || [];
const isAdditionalBlockingNeeded = budgetStatus === 'PROPOSED' && internalOrdersList.length > 0; const totalBlocked = useMemo(() => {
return internalOrdersList.reduce((sum: number, io: any) => sum + Number(io.ioBlockedAmount || io.io_blocked_amount || 0), 0);
}, [internalOrdersList]);
const isAdditionalBlockingNeeded = budgetStatus === 'PROPOSED' && internalOrdersList.length > 0 && (estimatedBudget - totalBlocked) > 0.01;
const [ioNumber, setIoNumber] = useState(''); const [ioNumber, setIoNumber] = useState('');
const [fetchingAmount, setFetchingAmount] = useState(false); const [fetchingAmount, setFetchingAmount] = useState(false);
@ -331,12 +336,12 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
placeholder="Enter IO number (e.g., IO-2024-12345)" placeholder="Enter IO number (e.g., IO-2024-12345)"
value={ioNumber} value={ioNumber}
onChange={(e) => setIoNumber(e.target.value)} onChange={(e) => setIoNumber(e.target.value)}
disabled={fetchingAmount || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded)} disabled={fetchingAmount || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded) || isSpectator}
className="flex-1" className="flex-1"
/> />
<Button <Button
onClick={handleFetchAmount} onClick={handleFetchAmount}
disabled={!ioNumber.trim() || fetchingAmount || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded)} disabled={!ioNumber.trim() || fetchingAmount || (blockedIOs.length > 0 && !isAdditionalBlockingNeeded) || isSpectator}
className="bg-[#2d4a3e] hover:bg-[#1f3329]" className="bg-[#2d4a3e] hover:bg-[#1f3329]"
> >
<Download className="w-4 h-4 mr-2" /> <Download className="w-4 h-4 mr-2" />
@ -385,8 +390,16 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
min="0" min="0"
step="0.01" step="0.01"
value={amountToBlock} value={amountToBlock}
onChange={(e) => setAmountToBlock(e.target.value)} onChange={(e) => {
const val = e.target.value;
if (parseFloat(val) < 0) {
toast.error('Amount cannot be negative');
return;
}
setAmountToBlock(val);
}}
className="pl-8" className="pl-8"
disabled={isSpectator}
/> />
</div> </div>
{estimatedBudget > 0 && ( {estimatedBudget > 0 && (
@ -406,7 +419,8 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
!amountToBlock || !amountToBlock ||
parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) <= 0 ||
parseFloat(amountToBlock) > fetchedAmount || parseFloat(amountToBlock) > fetchedAmount ||
(estimatedBudget > 0 && Math.abs((blockedIOs.reduce((s, i) => s + i.blockedAmount, 0) + parseFloat(amountToBlock)) - estimatedBudget) > 0.01) (estimatedBudget > 0 && Math.abs((blockedIOs.reduce((s, i) => s + i.blockedAmount, 0) + parseFloat(amountToBlock)) - estimatedBudget) > 0.01) ||
isSpectator
} }
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]" className="w-full bg-[#2d4a3e] hover:bg-[#1f3329]"
> >

View File

@ -41,6 +41,7 @@ interface DealerClaimWorkflowTabProps {
maxFileSizeMB: number; maxFileSizeMB: number;
allowedFileTypes: string[]; allowedFileTypes: string[];
}; };
isSpectator?: boolean;
} }
interface WorkflowStep { interface WorkflowStep {
@ -178,7 +179,8 @@ export function DealerClaimWorkflowTab({
isInitiator, isInitiator,
onSkipApprover: _onSkipApprover, onSkipApprover: _onSkipApprover,
onRefresh, onRefresh,
documentPolicy documentPolicy,
isSpectator = false
}: DealerClaimWorkflowTabProps) { }: DealerClaimWorkflowTabProps) {
const [showProposalModal, setShowProposalModal] = useState(false); const [showProposalModal, setShowProposalModal] = useState(false);
const [showApprovalModal, setShowApprovalModal] = useState(false); const [showApprovalModal, setShowApprovalModal] = useState(false);
@ -2226,8 +2228,8 @@ export function DealerClaimWorkflowTab({
(stepLevel?.levelName && stepLevel.levelName.toLowerCase().includes('dealer')); (stepLevel?.levelName && stepLevel.levelName.toLowerCase().includes('dealer'));
const isUserAuthorized = isUserApproverForThisStep || (isDealerStep && isDealer); const isUserAuthorized = isUserApproverForThisStep || (isDealerStep && isDealer);
// Step must be active AND user must be authorized // Step must be active AND user must be authorized AND NOT a spectator
return isActive && isUserAuthorized; return isActive && isUserAuthorized && !isSpectator;
})() && ( })() && (
<div className="mt-4 flex gap-2"> <div className="mt-4 flex gap-2">
{/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */} {/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */}

View File

@ -273,8 +273,14 @@ export function DealerCompletionDocumentsModal({
if (item.id === id) { if (item.id === id) {
let updatedItem = { ...item, [field]: value }; let updatedItem = { ...item, [field]: value };
// Re-calculate GST if relevant fields change
if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate', 'quantity'].includes(field)) { if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate', 'quantity'].includes(field)) {
// Negative amount validation
const numValue = parseFloat(value);
if (!isNaN(numValue) && numValue < 0) {
toast.error('Value cannot be negative');
return item;
}
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount; const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity; const quantity = field === 'quantity' ? parseInt(value) || 1 : item.quantity;
@ -523,6 +529,13 @@ export function DealerCompletionDocumentsModal({
return; return;
} }
// Check for negative amounts
const hasNegativeAmounts = expenseItems.some(item => item.amount < 0 || item.quantity < 1);
if (hasNegativeAmounts) {
toast.error('Please ensure all amounts are non-negative and quantity is at least 1');
return;
}
// Filter valid expense items // Filter valid expense items
const validExpenses = expenseItems.filter( const validExpenses = expenseItems.filter(
(item) => item.description.trim() !== '' && item.amount > 0 (item) => item.description.trim() !== '' && item.amount > 0

View File

@ -437,8 +437,14 @@ export function DealerProposalSubmissionModal({
if (item.id === id) { if (item.id === id) {
let updatedItem = { ...item, [field]: value }; let updatedItem = { ...item, [field]: value };
// Re-calculate GST if relevant fields change
if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate'].includes(field)) { if (['amount', 'gstRate', 'cgstRate', 'sgstRate', 'utgstRate', 'igstRate'].includes(field)) {
// Negative amount validation
const numValue = parseFloat(value);
if (!isNaN(numValue) && numValue < 0) {
toast.error('Value cannot be negative');
return item;
}
const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount; const amount = field === 'amount' ? parseFloat(value) || 0 : item.amount;
const quantity = 1; // Quantity is now fixed to 1 const quantity = 1; // Quantity is now fixed to 1
@ -524,6 +530,18 @@ export function DealerProposalSubmissionModal({
return; return;
} }
// Check for negative amounts or invalid days
const hasNegativeAmounts = costItems.some(item => item.amount < 0);
if (hasNegativeAmounts) {
toast.error('Please ensure all amounts are non-negative');
return;
}
if (timelineMode === 'days' && (parseInt(numberOfDays) <= 0 || isNaN(parseInt(numberOfDays)))) {
toast.error('Please enter a valid number of days greater than 0');
return;
}
// Calculate final completion date if using days mode // Calculate final completion date if using days mode
let finalCompletionDate: string = expectedCompletionDate || ''; let finalCompletionDate: string = expectedCompletionDate || '';
if (timelineMode === 'days' && numberOfDays) { if (timelineMode === 'days' && numberOfDays) {

View File

@ -153,14 +153,22 @@ 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();
const isDepartmentLead = (request?.approvalFlow || []).some((level: any) =>
level.role === 'Department Lead Approval' &&
level.approverEmail?.toLowerCase() === currentUserEmail
);
// IO tab visibility for dealer claims // IO tab visibility for dealer claims
// Restricted: Hide from Dealers, show for internal roles (Initiator, Dept Lead, Finance, Admin) // Restricted: Hide from Dealers, show only for Initiator and Department Lead
const isDealer = (user as any)?.jobTitle === 'Dealer' || (user as any)?.designation === 'Dealer'; const isDealer = (user as any)?.jobTitle === 'Dealer' || (user as any)?.designation === 'Dealer';
const isClaimManagement = request?.workflowType === 'CLAIM_MANAGEMENT' || const isClaimManagement = request?.workflowType === 'CLAIM_MANAGEMENT' ||
apiRequest?.workflowType === 'CLAIM_MANAGEMENT' || apiRequest?.workflowType === 'CLAIM_MANAGEMENT' ||
request?.templateType === 'claim-management'; request?.templateType === 'claim-management';
const showIOTab = isClaimManagement && !isDealer;
// Requirement: IO tab visible only for Initiator and Department Lead Approver
const showIOTab = isClaimManagement && !isDealer && (isInitiator || isDepartmentLead);
const { const {
mergedMessages, mergedMessages,
@ -616,6 +624,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
request={request} request={request}
user={user} user={user}
isInitiator={isInitiator} isInitiator={isInitiator}
isSpectator={isSpectator}
onSkipApprover={(data) => { onSkipApprover={(data) => {
if (!data.levelId) { if (!data.levelId) {
alert('Level ID not available'); alert('Level ID not available');
@ -635,6 +644,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
request={request} request={request}
apiRequest={apiRequest} apiRequest={apiRequest}
onRefresh={refreshDetails} onRefresh={refreshDetails}
isSpectator={isSpectator}
/> />
</TabsContent> </TabsContent>
)} )}
@ -648,6 +658,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
triggerFileInput={triggerFileInput} triggerFileInput={triggerFileInput}
setPreviewDocument={setPreviewDocument} setPreviewDocument={setPreviewDocument}
downloadDocument={downloadDocument} downloadDocument={downloadDocument}
isSpectator={isSpectator}
/> />
</TabsContent> </TabsContent>

View File

@ -15,6 +15,7 @@ interface DocumentsTabProps {
triggerFileInput: () => void; triggerFileInput: () => void;
setPreviewDocument: (doc: any) => void; setPreviewDocument: (doc: any) => void;
downloadDocument: (documentId: string) => Promise<void>; downloadDocument: (documentId: string) => Promise<void>;
isSpectator?: boolean;
} }
export function DocumentsTab({ export function DocumentsTab({
@ -25,6 +26,7 @@ export function DocumentsTab({
triggerFileInput, triggerFileInput,
setPreviewDocument, setPreviewDocument,
downloadDocument, downloadDocument,
isSpectator = false,
}: DocumentsTabProps) { }: DocumentsTabProps) {
const isForm16 = (request?.templateType || request?.template_type || '').toString().toUpperCase() === 'FORM_16'; const isForm16 = (request?.templateType || request?.template_type || '').toString().toUpperCase() === 'FORM_16';
@ -105,7 +107,7 @@ export function DocumentsTab({
<Button <Button
size="sm" size="sm"
onClick={triggerFileInput} onClick={triggerFileInput}
disabled={uploadingDocument || request.status === 'closed'} disabled={uploadingDocument || request.status === 'closed' || isSpectator}
className="gap-1 sm:gap-2 h-8 sm:h-9 text-xs sm:text-sm shrink-0" className="gap-1 sm:gap-2 h-8 sm:h-9 text-xs sm:text-sm shrink-0"
data-testid="upload-document-btn" data-testid="upload-document-btn"
> >