multi level iteration partially implemented
This commit is contained in:
parent
fc46f32282
commit
a3a142d603
@ -47,6 +47,10 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
// Get organizer user object from association (organizer) or fallback to organizedBy UUID
|
// Get organizer user object from association (organizer) or fallback to organizedBy UUID
|
||||||
const organizer = internalOrder?.organizer || null;
|
const organizer = internalOrder?.organizer || null;
|
||||||
|
|
||||||
|
// Get estimated budget from proposal details
|
||||||
|
const proposalDetails = apiRequest?.proposalDetails || {};
|
||||||
|
const estimatedBudget = Number(proposalDetails?.totalEstimatedBudget || proposalDetails?.total_estimated_budget || 0);
|
||||||
|
|
||||||
const [ioNumber, setIoNumber] = useState(existingIONumber);
|
const [ioNumber, setIoNumber] = useState(existingIONumber);
|
||||||
const [fetchingAmount, setFetchingAmount] = useState(false);
|
const [fetchingAmount, setFetchingAmount] = useState(false);
|
||||||
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
const [fetchedAmount, setFetchedAmount] = useState<number | null>(null);
|
||||||
@ -139,8 +143,12 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
|
|
||||||
if (ioData.isValid && ioData.availableBalance > 0) {
|
if (ioData.isValid && ioData.availableBalance > 0) {
|
||||||
setFetchedAmount(ioData.availableBalance);
|
setFetchedAmount(ioData.availableBalance);
|
||||||
// Pre-fill amount to block with available balance
|
// Pre-fill amount to block with estimated budget (if available), otherwise use available balance
|
||||||
setAmountToBlock(String(ioData.availableBalance));
|
if (estimatedBudget > 0) {
|
||||||
|
setAmountToBlock(String(estimatedBudget));
|
||||||
|
} else {
|
||||||
|
setAmountToBlock(String(ioData.availableBalance));
|
||||||
|
}
|
||||||
toast.success(`IO fetched from SAP. Available balance: ₹${ioData.availableBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
toast.success(`IO fetched from SAP. Available balance: ₹${ioData.availableBalance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Invalid IO number or no available balance found');
|
toast.error('Invalid IO number or no available balance found');
|
||||||
@ -191,6 +199,15 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate that amount to block must exactly match estimated budget
|
||||||
|
if (estimatedBudget > 0) {
|
||||||
|
const roundedEstimatedBudget = parseFloat(estimatedBudget.toFixed(2));
|
||||||
|
if (Math.abs(blockAmount - roundedEstimatedBudget) > 0.01) {
|
||||||
|
toast.error(`Amount to block must be exactly equal to the estimated budget (₹${roundedEstimatedBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Blocking budget
|
// Blocking budget
|
||||||
|
|
||||||
setBlockingBudget(true);
|
setBlockingBudget(true);
|
||||||
@ -362,12 +379,25 @@ export function IOTab({ request, apiRequest, onRefresh }: IOTabProps) {
|
|||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{estimatedBudget > 0 && (
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-amber-800">
|
||||||
|
<strong>Required:</strong> Amount must be exactly equal to the estimated budget: <strong>₹{estimatedBudget.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Block Button */}
|
{/* Block Button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleBlockBudget}
|
onClick={handleBlockBudget}
|
||||||
disabled={blockingBudget || !amountToBlock || parseFloat(amountToBlock) <= 0 || parseFloat(amountToBlock) > fetchedAmount}
|
disabled={
|
||||||
|
blockingBudget ||
|
||||||
|
!amountToBlock ||
|
||||||
|
parseFloat(amountToBlock) <= 0 ||
|
||||||
|
parseFloat(amountToBlock) > fetchedAmount ||
|
||||||
|
(estimatedBudget > 0 && Math.abs(parseFloat(amountToBlock) - estimatedBudget) > 0.01)
|
||||||
|
}
|
||||||
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" />
|
||||||
|
|||||||
@ -10,20 +10,23 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle } from 'lucide-react';
|
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon, XCircle, History, ChevronDown, ChevronUp, RefreshCw, RotateCw } from 'lucide-react';
|
||||||
import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
|
import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
|
||||||
import { formatHoursMinutes } from '@/utils/slaTracker';
|
import { formatHoursMinutes } from '@/utils/slaTracker';
|
||||||
import { AdditionalApproverReviewModal } from './modals';
|
import {
|
||||||
import { DealerProposalSubmissionModal } from './modals';
|
AdditionalApproverReviewModal,
|
||||||
import { InitiatorProposalApprovalModal } from './modals';
|
DealerProposalSubmissionModal,
|
||||||
import { DeptLeadIOApprovalModal } from './modals';
|
InitiatorProposalApprovalModal,
|
||||||
import { DealerCompletionDocumentsModal } from './modals';
|
DeptLeadIOApprovalModal,
|
||||||
import { CreditNoteSAPModal } from './modals';
|
DealerCompletionDocumentsModal,
|
||||||
import { EmailNotificationTemplateModal } from './modals';
|
CreditNoteSAPModal,
|
||||||
import { DMSPushModal } from './modals';
|
EmailNotificationTemplateModal,
|
||||||
|
DMSPushModal
|
||||||
|
// InitiatorActionModal - Removed, using direct buttons instead
|
||||||
|
} from './modals';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { submitProposal, updateIODetails, submitCompletion, updateEInvoice, sendCreditNoteToDealer } from '@/services/dealerClaimApi';
|
import { submitProposal, updateIODetails, submitCompletion, updateEInvoice, sendCreditNoteToDealer } from '@/services/dealerClaimApi';
|
||||||
import { getWorkflowDetails, approveLevel, rejectLevel } from '@/services/workflowApi';
|
import { getWorkflowDetails, approveLevel, rejectLevel, handleInitiatorAction, getWorkflowHistory } from '@/services/workflowApi';
|
||||||
import { uploadDocument } from '@/services/documentApi';
|
import { uploadDocument } from '@/services/documentApi';
|
||||||
|
|
||||||
interface DealerClaimWorkflowTabProps {
|
interface DealerClaimWorkflowTabProps {
|
||||||
@ -61,6 +64,10 @@ interface WorkflowStep {
|
|||||||
};
|
};
|
||||||
einvoiceUrl?: string;
|
einvoiceUrl?: string;
|
||||||
emailTemplateUrl?: string;
|
emailTemplateUrl?: string;
|
||||||
|
versionHistory?: {
|
||||||
|
current: any;
|
||||||
|
previous: any;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -168,6 +175,10 @@ export function DealerClaimWorkflowTab({
|
|||||||
const [selectedStepForEmail, setSelectedStepForEmail] = useState<{ stepNumber: number; stepName: string } | null>(null);
|
const [selectedStepForEmail, setSelectedStepForEmail] = useState<{ stepNumber: number; stepName: string } | null>(null);
|
||||||
const [showAdditionalApproverReviewModal, setShowAdditionalApproverReviewModal] = useState(false);
|
const [showAdditionalApproverReviewModal, setShowAdditionalApproverReviewModal] = useState(false);
|
||||||
const [selectedLevelForReview, setSelectedLevelForReview] = useState<{ levelId: string; levelName: string; approverName: string } | null>(null);
|
const [selectedLevelForReview, setSelectedLevelForReview] = useState<{ levelId: string; levelName: string; approverName: string } | null>(null);
|
||||||
|
// Removed - Initiator actions are now direct buttons in step card, no modal needed
|
||||||
|
const [versionHistory, setVersionHistory] = useState<any[]>([]);
|
||||||
|
const [showHistory, setShowHistory] = useState(false);
|
||||||
|
const [expandedVersionSteps, setExpandedVersionSteps] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
// Load approval flows from real API
|
// Load approval flows from real API
|
||||||
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
const [approvalFlow, setApprovalFlow] = useState<any[]>([]);
|
||||||
@ -285,8 +296,24 @@ export function DealerClaimWorkflowTab({
|
|||||||
// Small delay to ensure backend has fully processed the approval
|
// Small delay to ensure backend has fully processed the approval
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
onRefresh?.();
|
onRefresh?.();
|
||||||
|
loadVersionHistory();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadVersionHistory = async () => {
|
||||||
|
if (request?.id || request?.requestId) {
|
||||||
|
try {
|
||||||
|
const history = await getWorkflowHistory(request.id || request.requestId);
|
||||||
|
setVersionHistory(history);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load version history:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadVersionHistory();
|
||||||
|
}, [request?.id, request?.requestId, refreshTrigger]);
|
||||||
|
|
||||||
// Step title and description mapping based on actual step number (not array index)
|
// Step title and description mapping based on actual step number (not array index)
|
||||||
// This handles cases where approvers are added between steps
|
// This handles cases where approvers are added between steps
|
||||||
const getStepTitle = (stepNumber: number, levelName?: string, approverName?: string): string => {
|
const getStepTitle = (stepNumber: number, levelName?: string, approverName?: string): string => {
|
||||||
@ -394,6 +421,38 @@ export function DealerClaimWorkflowTab({
|
|||||||
return `Step ${stepNumber} approval required.`;
|
return `Step ${stepNumber} approval required.`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to get version history for a specific approval level
|
||||||
|
// Uses levelName to match history (consistent and doesn't change with step shifts or additional approvers)
|
||||||
|
const getStepVersionHistory = (levelName: string) => {
|
||||||
|
if (!versionHistory || versionHistory.length === 0 || !levelName) return { current: null, previous: null };
|
||||||
|
|
||||||
|
// Filter history by levelName (most reliable and consistent)
|
||||||
|
// Level names are consistent: "Dealer Proposal Submission", "Department Lead Approval", etc.
|
||||||
|
// Additional approvers have unique names like "Additional Approver - John Doe"
|
||||||
|
const stepVersions = versionHistory.filter((history: any) => {
|
||||||
|
// Primary match: by levelName (consistent identifier)
|
||||||
|
if (history.levelName && history.levelName.trim() === levelName.trim()) {
|
||||||
|
return true; // Include all snapshot types for this level
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: match by levelName from snapshotData (for approval snapshots)
|
||||||
|
if (history.snapshotData?.levelName && history.snapshotData.levelName.trim() === levelName.trim()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by version descending to get most recent first
|
||||||
|
const sortedVersions = [...stepVersions].sort((a, b) => b.version - a.version);
|
||||||
|
|
||||||
|
const current = sortedVersions.length > 0 ? sortedVersions[0] : null;
|
||||||
|
const previous = sortedVersions.length > 1 ? sortedVersions[1] : null;
|
||||||
|
|
||||||
|
return { current, previous };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Get backend currentLevel to determine which steps are active vs waiting
|
// Get backend currentLevel to determine which steps are active vs waiting
|
||||||
// This needs to be calculated before mapping steps so we can use it in status normalization
|
// This needs to be calculated before mapping steps so we can use it in status normalization
|
||||||
// Convert to number to ensure proper comparison
|
// Convert to number to ensure proper comparison
|
||||||
@ -402,6 +461,23 @@ export function DealerClaimWorkflowTab({
|
|||||||
? Number(backendCurrentLevel)
|
? Number(backendCurrentLevel)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Check request status - if rejected or closed, no steps should be active
|
||||||
|
const requestStatus = (request?.status || '').toUpperCase();
|
||||||
|
const isRequestRejected = requestStatus === 'REJECTED';
|
||||||
|
const isRequestClosed = requestStatus === 'CLOSED';
|
||||||
|
const isRequestActive = !isRequestRejected && !isRequestClosed &&
|
||||||
|
(requestStatus === 'PENDING' || requestStatus === 'IN_PROGRESS' || requestStatus === 'IN-PROGRESS');
|
||||||
|
|
||||||
|
// Find the rejected step level (if any) - all steps after rejection should be inactive
|
||||||
|
const rejectedStepLevel = approvalFlow.find((level: any) => {
|
||||||
|
const levelId = level.levelId || level.level_id;
|
||||||
|
const approval = request?.approvals?.find((a: any) => a.levelId === levelId);
|
||||||
|
return approval?.status?.toLowerCase() === 'rejected';
|
||||||
|
});
|
||||||
|
const rejectedStepNumber = rejectedStepLevel
|
||||||
|
? Number(rejectedStepLevel.levelNumber || rejectedStepLevel.level_number || rejectedStepLevel.step || 0)
|
||||||
|
: null;
|
||||||
|
|
||||||
// Transform approval flow to dealer claim workflow steps
|
// Transform approval flow to dealer claim workflow steps
|
||||||
const workflowSteps: WorkflowStep[] = approvalFlow.map((step: any, index: number) => {
|
const workflowSteps: WorkflowStep[] = approvalFlow.map((step: any, index: number) => {
|
||||||
// Get actual step number from levelNumber or step field - ensure it's a number
|
// Get actual step number from levelNumber or step field - ensure it's a number
|
||||||
@ -463,13 +539,44 @@ export function DealerClaimWorkflowTab({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize status - ALWAYS check step position first, then use approval/step status
|
// Normalize status - CRITICAL: Check request status first, then step position
|
||||||
// This ensures future steps are always 'waiting' regardless of approval/step status
|
// If request is rejected/closed, no steps should be active after rejection point
|
||||||
let normalizedStatus: string;
|
let normalizedStatus: string;
|
||||||
|
|
||||||
// First, check step position relative to currentLevel (this is the source of truth)
|
// FIRST: Check if request is rejected/closed - if so, handle accordingly
|
||||||
// Use currentLevelNumber which is guaranteed to be a number or null
|
if (isRequestRejected || isRequestClosed) {
|
||||||
if (currentLevelNumber !== null && currentLevelNumber > 0) {
|
// If this step was rejected, mark it as rejected
|
||||||
|
if (approval?.status?.toLowerCase() === 'rejected') {
|
||||||
|
normalizedStatus = 'rejected';
|
||||||
|
}
|
||||||
|
// If there's a rejected step and this step comes after it, mark as waiting (inactive)
|
||||||
|
else if (rejectedStepNumber !== null && actualStepNumber > rejectedStepNumber) {
|
||||||
|
normalizedStatus = 'waiting';
|
||||||
|
}
|
||||||
|
// If this step was approved before rejection, keep it as approved
|
||||||
|
else if (approval?.status?.toLowerCase() === 'approved') {
|
||||||
|
normalizedStatus = 'approved';
|
||||||
|
}
|
||||||
|
// For steps before rejection that weren't explicitly approved/rejected
|
||||||
|
else if (rejectedStepNumber !== null && actualStepNumber < rejectedStepNumber) {
|
||||||
|
// Check if step was approved
|
||||||
|
if (approval?.status?.toLowerCase() === 'approved') {
|
||||||
|
normalizedStatus = 'approved';
|
||||||
|
} else {
|
||||||
|
normalizedStatus = 'waiting';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no rejected step found but request is rejected, check current level
|
||||||
|
else if (currentLevelNumber !== null && actualStepNumber > currentLevelNumber) {
|
||||||
|
normalizedStatus = 'waiting';
|
||||||
|
}
|
||||||
|
// Default: mark as waiting (inactive) for rejected/closed requests
|
||||||
|
else {
|
||||||
|
normalizedStatus = 'waiting';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SECOND: Request is active - check step position relative to currentLevel
|
||||||
|
else if (isRequestActive && currentLevelNumber !== null && currentLevelNumber > 0) {
|
||||||
if (actualStepNumber > currentLevelNumber) {
|
if (actualStepNumber > currentLevelNumber) {
|
||||||
// Future step - MUST be waiting (ignore approval status and step.status)
|
// Future step - MUST be waiting (ignore approval status and step.status)
|
||||||
normalizedStatus = 'waiting';
|
normalizedStatus = 'waiting';
|
||||||
@ -511,8 +618,9 @@ export function DealerClaimWorkflowTab({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// No backend currentLevel - use approval status if available, otherwise step.status
|
// THIRD: No backend currentLevel or request status unclear - use approval status
|
||||||
|
else {
|
||||||
if (approval?.status) {
|
if (approval?.status) {
|
||||||
const approvalStatus = approval.status.toLowerCase();
|
const approvalStatus = approval.status.toLowerCase();
|
||||||
if (approvalStatus === 'approved') {
|
if (approvalStatus === 'approved') {
|
||||||
@ -546,6 +654,13 @@ export function DealerClaimWorkflowTab({
|
|||||||
|
|
||||||
const approverName = step.approver || step.approverName || 'Unknown';
|
const approverName = step.approver || step.approverName || 'Unknown';
|
||||||
|
|
||||||
|
// Get levelName for this step (consistent identifier, doesn't change with step shifts)
|
||||||
|
// Level names are consistent: "Dealer Proposal Submission", "Department Lead Approval", etc.
|
||||||
|
// Additional approvers have unique names like "Additional Approver - John Doe"
|
||||||
|
|
||||||
|
// Get version history for this step using levelName (consistent and reliable)
|
||||||
|
const stepVersionHistory = levelName ? getStepVersionHistory(levelName) : { current: null, previous: null };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: actualStepNumber,
|
step: actualStepNumber,
|
||||||
title: getStepTitle(actualStepNumber, levelName, approverName),
|
title: getStepTitle(actualStepNumber, levelName, approverName),
|
||||||
@ -560,6 +675,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
dmsDetails,
|
dmsDetails,
|
||||||
einvoiceUrl: actualStepNumber === 7 ? (approval as any)?.einvoiceUrl : undefined,
|
einvoiceUrl: actualStepNumber === 7 ? (approval as any)?.einvoiceUrl : undefined,
|
||||||
emailTemplateUrl: (approval as any)?.emailTemplateUrl || undefined,
|
emailTemplateUrl: (approval as any)?.emailTemplateUrl || undefined,
|
||||||
|
versionHistory: stepVersionHistory, // Add version history to step
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -569,22 +685,42 @@ export function DealerClaimWorkflowTab({
|
|||||||
// Note: Status normalization already handled in workflowSteps mapping above
|
// Note: Status normalization already handled in workflowSteps mapping above
|
||||||
// backendCurrentLevel is already calculated above before the map function
|
// backendCurrentLevel is already calculated above before the map function
|
||||||
|
|
||||||
// Find the step that matches backend's currentLevel
|
// CRITICAL: If request is rejected or closed, no step should be active
|
||||||
const activeStepFromBackend = currentLevelNumber !== null
|
let activeStep = null;
|
||||||
? workflowSteps.find(s => s.step === currentLevelNumber)
|
let currentStep = 1;
|
||||||
: null;
|
|
||||||
|
|
||||||
// If backend currentLevel exists and step is pending/in_progress, use it
|
if (isRequestRejected || isRequestClosed) {
|
||||||
// Otherwise, find first pending/in_progress step
|
// Request is rejected/closed - no active step
|
||||||
const activeStep = activeStepFromBackend &&
|
// Find the rejected step to show as the "current" step for display purposes
|
||||||
(activeStepFromBackend.status === 'pending' || activeStepFromBackend.status === 'in_progress')
|
if (rejectedStepNumber !== null) {
|
||||||
? activeStepFromBackend
|
currentStep = rejectedStepNumber;
|
||||||
: workflowSteps.find(s => {
|
} else if (currentLevelNumber !== null) {
|
||||||
const status = s.status?.toLowerCase() || '';
|
currentStep = currentLevelNumber;
|
||||||
return status === 'pending' || status === 'in_progress' || status === 'in-review' || status === 'in_review';
|
} else {
|
||||||
});
|
currentStep = request?.currentStep || 1;
|
||||||
|
}
|
||||||
|
} else if (isRequestActive) {
|
||||||
|
// Request is active - find the active step
|
||||||
|
// Find the step that matches backend's currentLevel
|
||||||
|
const activeStepFromBackend = currentLevelNumber !== null
|
||||||
|
? workflowSteps.find(s => s.step === currentLevelNumber)
|
||||||
|
: null;
|
||||||
|
|
||||||
const currentStep = activeStep ? activeStep.step : (currentLevelNumber || request?.currentStep || 1);
|
// If backend currentLevel exists and step is pending/in_progress, use it
|
||||||
|
// Otherwise, find first pending/in_progress step
|
||||||
|
activeStep = activeStepFromBackend &&
|
||||||
|
(activeStepFromBackend.status === 'pending' || activeStepFromBackend.status === 'in_progress')
|
||||||
|
? activeStepFromBackend
|
||||||
|
: workflowSteps.find(s => {
|
||||||
|
const status = s.status?.toLowerCase() || '';
|
||||||
|
return status === 'pending' || status === 'in_progress' || status === 'in-review' || status === 'in_review';
|
||||||
|
});
|
||||||
|
|
||||||
|
currentStep = activeStep ? activeStep.step : (currentLevelNumber || request?.currentStep || 1);
|
||||||
|
} else {
|
||||||
|
// Request status unclear - use backend currentLevel or default
|
||||||
|
currentStep = currentLevelNumber || request?.currentStep || 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if current user is the dealer (for steps 1 and 5)
|
// Check if current user is the dealer (for steps 1 and 5)
|
||||||
const userEmail = (user as any)?.email?.toLowerCase() || '';
|
const userEmail = (user as any)?.email?.toLowerCase() || '';
|
||||||
@ -888,6 +1024,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
totalClosedExpenses: data.totalClosedExpenses,
|
totalClosedExpenses: data.totalClosedExpenses,
|
||||||
completionDocuments: data.completionDocuments,
|
completionDocuments: data.completionDocuments,
|
||||||
activityPhotos: data.activityPhotos,
|
activityPhotos: data.activityPhotos,
|
||||||
|
completionDescription: data.completionDescription,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Upload supporting documents if provided
|
// Upload supporting documents if provided
|
||||||
@ -1174,18 +1311,48 @@ export function DealerClaimWorkflowTab({
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{/* Returned to Initiator Banner */}
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const isReturnedToInitiator = request?.status === 'rejected' &&
|
||||||
|
!(request?.closureDate || request?.closure_date) && isInitiator;
|
||||||
|
if (!isReturnedToInitiator) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 p-4 bg-amber-50 border-l-4 border-amber-500 rounded-lg flex items-start gap-4 shadow-sm">
|
||||||
|
<div className="p-2 bg-amber-100 rounded-full">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-bold text-amber-900 mb-1">Action Required: Request Returned</h3>
|
||||||
|
<p className="text-xs text-amber-800 leading-relaxed mb-3">
|
||||||
|
This request has been returned to you by the department head.
|
||||||
|
{isInitiator ?
|
||||||
|
'You can choose to resubmit, discuss with the dealer, request a revision, or cancel the request.' :
|
||||||
|
'The initiator needs to take action to proceed.'}
|
||||||
|
</p>
|
||||||
|
{/* Actions are now in the step card itself - no need for separate button */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{workflowSteps.map((step, index) => {
|
{workflowSteps.map((step, index) => {
|
||||||
// Step is active if:
|
// Step is active if:
|
||||||
// 1. It's pending or in_progress
|
// 1. Request is active (not rejected/closed)
|
||||||
// 2. AND it matches currentStep (from backend or calculated)
|
// 2. It's pending or in_progress
|
||||||
// 3. AND it's the actual current step (not a future step that happens to be pending)
|
// 3. AND it matches currentStep (from backend or calculated)
|
||||||
|
// 4. AND it's the actual current step (not a future step that happens to be pending)
|
||||||
const stepStatus = step.status?.toLowerCase() || '';
|
const stepStatus = step.status?.toLowerCase() || '';
|
||||||
const isPendingOrInProgress = stepStatus === 'pending' || stepStatus === 'in_progress';
|
const isPendingOrInProgress = stepStatus === 'in_progress';
|
||||||
const matchesCurrentStep = step.step === currentStep;
|
const matchesCurrentStep = step.step === currentStep;
|
||||||
|
|
||||||
// Step is active only if it matches the current step AND is pending/in_progress
|
// Step is active only if:
|
||||||
const isActive = isPendingOrInProgress && matchesCurrentStep;
|
// - Request is active (not rejected/closed)
|
||||||
|
// - AND it matches the current step
|
||||||
|
// - AND is pending/in_progress
|
||||||
|
const isActive = isRequestActive && isPendingOrInProgress && matchesCurrentStep;
|
||||||
const isCompleted = step.status === 'approved';
|
const isCompleted = step.status === 'approved';
|
||||||
|
|
||||||
// Find approval data for this step to get SLA information
|
// Find approval data for this step to get SLA information
|
||||||
@ -1272,6 +1439,275 @@ export function DealerClaimWorkflowTab({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Version History Section */}
|
||||||
|
{step.versionHistory && (step.versionHistory.current || step.versionHistory.previous) && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-between text-xs text-amber-700 hover:text-amber-800 hover:bg-amber-50"
|
||||||
|
onClick={() => {
|
||||||
|
const newExpanded = new Set(expandedVersionSteps);
|
||||||
|
if (newExpanded.has(step.step)) {
|
||||||
|
newExpanded.delete(step.step);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(step.step);
|
||||||
|
}
|
||||||
|
setExpandedVersionSteps(newExpanded);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<History className="w-3.5 h-3.5" />
|
||||||
|
<span className="font-medium">Version History</span>
|
||||||
|
{step.versionHistory.current && (
|
||||||
|
<Badge className="bg-amber-100 text-amber-800 text-[10px] px-1.5 py-0">
|
||||||
|
v{step.versionHistory.current.version}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.previous && (
|
||||||
|
<Badge className="bg-gray-100 text-gray-600 text-[10px] px-1.5 py-0">
|
||||||
|
v{step.versionHistory.previous.version}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{expandedVersionSteps.has(step.step) ? (
|
||||||
|
<ChevronUp className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{expandedVersionSteps.has(step.step) && (
|
||||||
|
<div className="mt-2 space-y-3 p-3 bg-amber-50/50 rounded-lg border border-amber-200">
|
||||||
|
{/* Current Version */}
|
||||||
|
{step.versionHistory.current && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge className="bg-amber-500 text-white text-[10px] px-2 py-0.5">
|
||||||
|
Current: v{step.versionHistory.current.version}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-[10px] text-amber-700 font-medium">
|
||||||
|
{formatDateSafe(step.versionHistory.current.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-700 font-medium">
|
||||||
|
{step.versionHistory.current.changeReason || 'Version Update'}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-blue-100 flex items-center justify-center">
|
||||||
|
<span className="text-[8px] font-bold text-blue-600">
|
||||||
|
{step.versionHistory.current.changer?.displayName?.charAt(0) || 'U'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-gray-600">
|
||||||
|
By {step.versionHistory.current.changer?.displayName || step.versionHistory.current.changer?.email || 'Unknown User'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Show snapshot data if available - JSONB structure */}
|
||||||
|
{step.versionHistory.current.snapshotType === 'PROPOSAL' && step.versionHistory.current.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-amber-200">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-700 mb-1">Proposal Snapshot:</p>
|
||||||
|
{step.versionHistory.current.snapshotData.documentUrl && (
|
||||||
|
<p className="text-[10px] text-blue-600 mb-1">
|
||||||
|
<a href={step.versionHistory.current.snapshotData.documentUrl} target="_blank" rel="noopener noreferrer" className="underline">
|
||||||
|
View Document
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
Budget: ₹{Number(step.versionHistory.current.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</p>
|
||||||
|
{step.versionHistory.current.snapshotData.comments && (
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1">
|
||||||
|
Comments: {step.versionHistory.current.snapshotData.comments.substring(0, 100)}
|
||||||
|
{step.versionHistory.current.snapshotData.comments.length > 100 ? '...' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.current.snapshotData.costItems && step.versionHistory.current.snapshotData.costItems.length > 0 && (
|
||||||
|
<p className="text-[10px] text-gray-500 mt-1">
|
||||||
|
{step.versionHistory.current.snapshotData.costItems.length} cost item(s)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.current.snapshotType === 'INTERNAL_ORDER' && step.versionHistory.current.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-amber-200">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-700 mb-1">IO Block Snapshot:</p>
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
IO Number: {step.versionHistory.current.snapshotData.ioNumber || 'N/A'}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
Blocked Amount: ₹{Number(step.versionHistory.current.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</p>
|
||||||
|
{step.versionHistory.current.snapshotData.sapDocumentNumber && (
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
SAP Doc: {step.versionHistory.current.snapshotData.sapDocumentNumber}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.current.snapshotType === 'COMPLETION' && step.versionHistory.current.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-amber-200">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-700 mb-1">Completion Snapshot:</p>
|
||||||
|
{step.versionHistory.current.snapshotData.documentUrl && (
|
||||||
|
<p className="text-[10px] text-blue-600 mb-1">
|
||||||
|
<a href={step.versionHistory.current.snapshotData.documentUrl} target="_blank" rel="noopener noreferrer" className="underline">
|
||||||
|
View Document
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
Total Expenses: ₹{Number(step.versionHistory.current.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</p>
|
||||||
|
{step.versionHistory.current.snapshotData.comments && (
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1">
|
||||||
|
Comments: {step.versionHistory.current.snapshotData.comments.substring(0, 100)}
|
||||||
|
{step.versionHistory.current.snapshotData.comments.length > 100 ? '...' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.current.snapshotData.expenses && step.versionHistory.current.snapshotData.expenses.length > 0 && (
|
||||||
|
<p className="text-[10px] text-gray-500 mt-1">
|
||||||
|
{step.versionHistory.current.snapshotData.expenses.length} expense item(s)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.current.snapshotType === 'APPROVE' && step.versionHistory.current.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-amber-200">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-700 mb-1">
|
||||||
|
{step.versionHistory.current.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'} Snapshot:
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
By: {step.versionHistory.current.snapshotData.approverName || step.versionHistory.current.snapshotData.approverEmail || 'Unknown'}
|
||||||
|
</p>
|
||||||
|
{step.versionHistory.current.snapshotData.comments && (
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1">
|
||||||
|
Comments: {step.versionHistory.current.snapshotData.comments.substring(0, 100)}
|
||||||
|
{step.versionHistory.current.snapshotData.comments.length > 100 ? '...' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.current.snapshotData.rejectionReason && (
|
||||||
|
<p className="text-[10px] text-red-600 mt-1">
|
||||||
|
Rejection Reason: {step.versionHistory.current.snapshotData.rejectionReason.substring(0, 100)}
|
||||||
|
{step.versionHistory.current.snapshotData.rejectionReason.length > 100 ? '...' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Previous Version */}
|
||||||
|
{step.versionHistory.previous && (
|
||||||
|
<div className="space-y-2 pt-2 border-t border-amber-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge className="bg-gray-400 text-white text-[10px] px-2 py-0.5">
|
||||||
|
Previous: v{step.versionHistory.previous.version}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-[10px] text-gray-600 font-medium">
|
||||||
|
{formatDateSafe(step.versionHistory.previous.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-700 font-medium">
|
||||||
|
{step.versionHistory.previous.changeReason || 'Version Update'}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||||
|
<span className="text-[8px] font-bold text-gray-600">
|
||||||
|
{step.versionHistory.previous.changer?.displayName?.charAt(0) || 'U'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-gray-600">
|
||||||
|
By {step.versionHistory.previous.changer?.displayName || step.versionHistory.previous.changer?.email || 'Unknown User'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Show snapshot data if available - JSONB structure */}
|
||||||
|
{step.versionHistory.previous.snapshotType === 'PROPOSAL' && step.versionHistory.previous.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-gray-200">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-700 mb-1">Proposal Snapshot:</p>
|
||||||
|
{step.versionHistory.previous.snapshotData.documentUrl && (
|
||||||
|
<p className="text-[10px] text-blue-600 mb-1">
|
||||||
|
<a href={step.versionHistory.previous.snapshotData.documentUrl} target="_blank" rel="noopener noreferrer" className="underline">
|
||||||
|
View Document
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
Budget: ₹{Number(step.versionHistory.previous.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</p>
|
||||||
|
{step.versionHistory.previous.snapshotData.comments && (
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1">
|
||||||
|
Comments: {step.versionHistory.previous.snapshotData.comments.substring(0, 100)}
|
||||||
|
{step.versionHistory.previous.snapshotData.comments.length > 100 ? '...' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.previous.snapshotType === 'INTERNAL_ORDER' && step.versionHistory.previous.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-gray-200">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-700 mb-1">IO Block Snapshot:</p>
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
IO Number: {step.versionHistory.previous.snapshotData.ioNumber || 'N/A'}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
Blocked Amount: ₹{Number(step.versionHistory.previous.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.previous.snapshotType === 'COMPLETION' && step.versionHistory.previous.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-gray-200">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-700 mb-1">Completion Snapshot:</p>
|
||||||
|
{step.versionHistory.previous.snapshotData.documentUrl && (
|
||||||
|
<p className="text-[10px] text-blue-600 mb-1">
|
||||||
|
<a href={step.versionHistory.previous.snapshotData.documentUrl} target="_blank" rel="noopener noreferrer" className="underline">
|
||||||
|
View Document
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
Total Expenses: ₹{Number(step.versionHistory.previous.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
|
</p>
|
||||||
|
{step.versionHistory.previous.snapshotData.comments && (
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1">
|
||||||
|
Comments: {step.versionHistory.previous.snapshotData.comments.substring(0, 100)}
|
||||||
|
{step.versionHistory.previous.snapshotData.comments.length > 100 ? '...' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.previous.snapshotType === 'APPROVE' && step.versionHistory.previous.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-gray-200">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-700 mb-1">
|
||||||
|
{step.versionHistory.previous.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'} Snapshot:
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-gray-600">
|
||||||
|
By: {step.versionHistory.previous.snapshotData.approverName || step.versionHistory.previous.snapshotData.approverEmail || 'Unknown'}
|
||||||
|
</p>
|
||||||
|
{step.versionHistory.previous.snapshotData.comments && (
|
||||||
|
<p className="text-[10px] text-gray-600 mt-1">
|
||||||
|
Comments: {step.versionHistory.previous.snapshotData.comments.substring(0, 100)}
|
||||||
|
{step.versionHistory.previous.snapshotData.comments.length > 100 ? '...' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{step.versionHistory.previous.snapshotData.rejectionReason && (
|
||||||
|
<p className="text-[10px] text-red-600 mt-1">
|
||||||
|
Rejection Reason: {step.versionHistory.previous.snapshotData.rejectionReason.substring(0, 100)}
|
||||||
|
{step.versionHistory.previous.snapshotData.rejectionReason.length > 100 ? '...' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Active Approver - SLA Time Tracking (Only show for current active step) */}
|
{/* Active Approver - SLA Time Tracking (Only show for current active step) */}
|
||||||
{isActive && approval?.sla && (
|
{isActive && approval?.sla && (
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
@ -1494,6 +1930,56 @@ export function DealerClaimWorkflowTab({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Initiator Action Step: Show action buttons (REVISE, REOPEN) - Direct actions, no modal */}
|
||||||
|
{(() => {
|
||||||
|
// Find the step level from approvalFlow
|
||||||
|
const stepLevelForInitiator = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step);
|
||||||
|
const stepLevelName = (stepLevelForInitiator?.levelName || step.title || '').toLowerCase();
|
||||||
|
const isInitiatorActionStep = stepLevelName.includes('initiator action');
|
||||||
|
const isUserInitiator = isInitiator || (userEmail === initiatorEmail);
|
||||||
|
|
||||||
|
if (!isInitiatorActionStep || !isUserInitiator) return null;
|
||||||
|
|
||||||
|
const handleDirectAction = async (action: 'REVISE' | 'REOPEN') => {
|
||||||
|
try {
|
||||||
|
if (!request?.id && !request?.requestId) {
|
||||||
|
throw new Error('Request ID not found');
|
||||||
|
}
|
||||||
|
const requestId = request.requestId || request.id;
|
||||||
|
|
||||||
|
// Call action directly without modal - comments are optional
|
||||||
|
await handleInitiatorAction(requestId, action, { reason: '' });
|
||||||
|
toast.success(`Action "${action === 'REVISE' ? 'Revision Requested' : 'Request Reopened'}" performed successfully`);
|
||||||
|
handleRefresh();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to perform initiator action:', error);
|
||||||
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to perform action';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-orange-500 text-orange-600 hover:bg-orange-50"
|
||||||
|
onClick={() => handleDirectAction('REVISE')}
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
Request Re-quotation
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-blue-500 text-blue-600 hover:bg-blue-50"
|
||||||
|
onClick={() => handleDirectAction('REOPEN')}
|
||||||
|
>
|
||||||
|
<RotateCw className="w-4 h-4 mr-2" />
|
||||||
|
Reopen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Department Lead Step: Approve and Organise IO - Find dynamically by levelName */}
|
{/* Department Lead Step: Approve and Organise IO - Find dynamically by levelName */}
|
||||||
{(() => {
|
{(() => {
|
||||||
// Find Department Lead step dynamically (handles step shifts)
|
// Find Department Lead step dynamically (handles step shifts)
|
||||||
@ -1529,7 +2015,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
disabled={!hasIONumber}
|
disabled={!hasIONumber}
|
||||||
>
|
>
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
Approve and Organise IO
|
Review and Approve
|
||||||
</Button>
|
</Button>
|
||||||
{!hasIONumber && (
|
{!hasIONumber && (
|
||||||
<p className="text-xs text-amber-600">
|
<p className="text-xs text-amber-600">
|
||||||
@ -1969,6 +2455,148 @@ export function DealerClaimWorkflowTab({
|
|||||||
approverName={selectedLevelForReview.approverName}
|
approverName={selectedLevelForReview.approverName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Initiator Action Modal - Removed, actions are now direct buttons in step card */}
|
||||||
|
|
||||||
|
{/* Version History Section */}
|
||||||
|
{versionHistory && versionHistory.length > 0 && (
|
||||||
|
<Card className="mt-6 border-amber-100 bg-amber-50/30">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-sm font-bold flex items-center gap-2 text-amber-900">
|
||||||
|
<Activity className="w-4 h-4" />
|
||||||
|
Revision History & Audit Trail
|
||||||
|
</CardTitle>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowHistory(!showHistory)}
|
||||||
|
className="text-amber-700 hover:text-amber-800 hover:bg-amber-100"
|
||||||
|
>
|
||||||
|
{showHistory ? 'Hide History' : 'View History'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<CardDescription className="text-xs text-amber-700">
|
||||||
|
Records of all revisions and actions taken on this request
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
{showHistory && (
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{versionHistory.map((item, idx) => (
|
||||||
|
<div key={item.historyId || idx} className="relative pl-6 pb-4 border-l-2 border-amber-200 last:border-0 last:pb-0">
|
||||||
|
<div className="absolute left-[-9px] top-0 w-4 h-4 rounded-full bg-amber-500 border-2 border-white shadow-sm" />
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-bold text-amber-900">
|
||||||
|
Version {item.version}
|
||||||
|
</span>
|
||||||
|
{item.snapshotType && (
|
||||||
|
<Badge className="bg-blue-100 text-blue-700 text-[9px] px-1.5 py-0">
|
||||||
|
{item.snapshotType}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{item.levelNumber && (
|
||||||
|
<Badge className="bg-gray-100 text-gray-600 text-[9px] px-1.5 py-0">
|
||||||
|
Step {item.levelNumber}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-amber-600 font-medium bg-amber-100 px-2 py-0.5 rounded-full">
|
||||||
|
{formatDateSafe(item.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-medium text-gray-800">
|
||||||
|
{item.changeReason || 'Version Update'}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1.5 mt-1">
|
||||||
|
<div className="w-4 h-4 rounded-full bg-blue-100 flex items-center justify-center">
|
||||||
|
<span className="text-[8px] font-bold text-blue-600">
|
||||||
|
{item.changer?.displayName?.charAt(0) || 'U'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
By {item.changer?.displayName || item.changer?.email || 'Unknown User'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Show snapshot details based on type - JSONB structure */}
|
||||||
|
{item.snapshotType === 'PROPOSAL' && item.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-amber-200 text-[10px]">
|
||||||
|
<p className="font-semibold text-gray-700 mb-1">Proposal:</p>
|
||||||
|
{item.snapshotData.documentUrl && (
|
||||||
|
<p className="text-blue-600 mb-1">
|
||||||
|
<a href={item.snapshotData.documentUrl} target="_blank" rel="noopener noreferrer" className="underline">
|
||||||
|
View Document
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-gray-600">Budget: ₹{Number(item.snapshotData.totalBudget || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</p>
|
||||||
|
{item.snapshotData.comments && (
|
||||||
|
<p className="text-gray-600 mt-1">Comments: {item.snapshotData.comments.substring(0, 80)}{item.snapshotData.comments.length > 80 ? '...' : ''}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.snapshotType === 'COMPLETION' && item.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-amber-200 text-[10px]">
|
||||||
|
<p className="font-semibold text-gray-700 mb-1">Completion:</p>
|
||||||
|
{item.snapshotData.documentUrl && (
|
||||||
|
<p className="text-blue-600 mb-1">
|
||||||
|
<a href={item.snapshotData.documentUrl} target="_blank" rel="noopener noreferrer" className="underline">
|
||||||
|
View Document
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-gray-600">Total Expenses: ₹{Number(item.snapshotData.totalExpenses || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</p>
|
||||||
|
{item.snapshotData.comments && (
|
||||||
|
<p className="text-gray-600 mt-1">Comments: {item.snapshotData.comments.substring(0, 80)}{item.snapshotData.comments.length > 80 ? '...' : ''}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.snapshotType === 'INTERNAL_ORDER' && item.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-amber-200 text-[10px]">
|
||||||
|
<p className="font-semibold text-gray-700 mb-1">IO Block:</p>
|
||||||
|
<p className="text-gray-600">IO Number: {item.snapshotData.ioNumber || 'N/A'}</p>
|
||||||
|
<p className="text-gray-600">Blocked: ₹{Number(item.snapshotData.blockedAmount || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</p>
|
||||||
|
{item.snapshotData.sapDocumentNumber && (
|
||||||
|
<p className="text-gray-600">SAP Doc: {item.snapshotData.sapDocumentNumber}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.snapshotType === 'APPROVE' && item.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-amber-200 text-[10px]">
|
||||||
|
<p className="font-semibold text-gray-700 mb-1">
|
||||||
|
{item.snapshotData.action === 'APPROVE' ? 'Approval' : 'Rejection'}:
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600">By: {item.snapshotData.approverName || item.snapshotData.approverEmail || 'Unknown'}</p>
|
||||||
|
{item.snapshotData.levelName && (
|
||||||
|
<p className="text-gray-600">Level: {item.snapshotData.levelName}</p>
|
||||||
|
)}
|
||||||
|
{item.snapshotData.comments && (
|
||||||
|
<p className="text-gray-600 mt-1">Comments: {item.snapshotData.comments.substring(0, 80)}{item.snapshotData.comments.length > 80 ? '...' : ''}</p>
|
||||||
|
)}
|
||||||
|
{item.snapshotData.rejectionReason && (
|
||||||
|
<p className="text-red-600 mt-1">Rejection Reason: {item.snapshotData.rejectionReason.substring(0, 80)}{item.snapshotData.rejectionReason.length > 80 ? '...' : ''}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.snapshotType === 'WORKFLOW' && item.snapshotData && (
|
||||||
|
<div className="mt-2 p-2 bg-white rounded border border-amber-200 text-[10px]">
|
||||||
|
<p className="font-semibold text-gray-700 mb-1">Workflow:</p>
|
||||||
|
<p className="text-gray-600">Status: {item.snapshotData.status || 'N/A'}</p>
|
||||||
|
{item.snapshotData.currentLevel && (
|
||||||
|
<p className="text-gray-600">Current Level: {item.snapshotData.currentLevel}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,7 +139,7 @@ export function DeptLeadIOApprovalModal({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<DialogTitle className="font-semibold text-lg lg:text-xl">
|
<DialogTitle className="font-semibold text-lg lg:text-xl">
|
||||||
Approve and Organise IO
|
Review and Approve
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-xs lg:text-sm mt-1">
|
<DialogDescription className="text-xs lg:text-sm mt-1">
|
||||||
Review IO details and provide your approval comments
|
Review IO details and provide your approval comments
|
||||||
|
|||||||
@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* InitiatorActionModal Component
|
||||||
|
* Modal for Initiator to take action on a returned/rejected request
|
||||||
|
* Actions: Reopen, Request Revised Quotation, Cancel
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
RefreshCw,
|
||||||
|
MessageSquare,
|
||||||
|
FileEdit,
|
||||||
|
XOctagon,
|
||||||
|
AlertTriangle,
|
||||||
|
Loader2
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface InitiatorActionModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onAction: (action: 'REOPEN' | 'REVISE' | 'CANCEL', comments: string) => Promise<void>;
|
||||||
|
requestTitle?: string;
|
||||||
|
requestId?: string;
|
||||||
|
defaultAction?: 'REOPEN' | 'REVISE' | 'CANCEL';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InitiatorActionModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onAction,
|
||||||
|
requestTitle = 'Request',
|
||||||
|
requestId: _requestId,
|
||||||
|
defaultAction,
|
||||||
|
}: InitiatorActionModalProps) {
|
||||||
|
const [comments, setComments] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [selectedAction, setSelectedAction] = useState<'REOPEN' | 'REVISE' | 'CANCEL' | null>(defaultAction || null);
|
||||||
|
|
||||||
|
// Update selectedAction when defaultAction changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultAction) {
|
||||||
|
setSelectedAction(defaultAction);
|
||||||
|
}
|
||||||
|
}, [defaultAction]);
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
id: 'REOPEN',
|
||||||
|
label: 'Reopen & Resubmit',
|
||||||
|
description: 'Resubmit the request to the department head for approval.',
|
||||||
|
icon: <RefreshCw className="w-5 h-5 text-blue-600" />,
|
||||||
|
color: 'blue',
|
||||||
|
variant: 'default' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'REVISE',
|
||||||
|
label: 'Request Revised Quotation',
|
||||||
|
description: 'Ask dealer to submit a new proposal/quotation.',
|
||||||
|
icon: <FileEdit className="w-5 h-5 text-amber-600" />,
|
||||||
|
color: 'amber',
|
||||||
|
variant: 'default' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CANCEL',
|
||||||
|
label: 'Cancel Request',
|
||||||
|
description: 'Permanently close and cancel this request.',
|
||||||
|
icon: <XOctagon className="w-5 h-5 text-red-600" />,
|
||||||
|
color: 'red',
|
||||||
|
variant: 'destructive' as const
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleActionClick = (actionId: any) => {
|
||||||
|
setSelectedAction(actionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!selectedAction) {
|
||||||
|
toast.error('Please select an action');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!comments.trim()) {
|
||||||
|
toast.error('Please provide a reason or comments for this action');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
await onAction(selectedAction, comments);
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to perform initiator action:', error);
|
||||||
|
const errorMessage = error?.response?.data?.message || error?.message || 'Action failed. Please try again.';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setComments('');
|
||||||
|
setSelectedAction(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!submitting) {
|
||||||
|
handleReset();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl">Action Required: {requestTitle}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
This request has been returned to you. Please select how you would like to proceed.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="py-4 space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{actions.map((action) => (
|
||||||
|
<div
|
||||||
|
key={action.id}
|
||||||
|
onClick={() => handleActionClick(action.id)}
|
||||||
|
className={`
|
||||||
|
cursor-pointer p-4 border-2 rounded-xl transition-all duration-200
|
||||||
|
${selectedAction === action.id
|
||||||
|
? `border-${action.color}-600 bg-${action.color}-50 shadow-sm`
|
||||||
|
: 'border-gray-100 hover:border-gray-200 hover:bg-gray-50'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className={`p-2 rounded-lg bg-white border border-gray-100`}>
|
||||||
|
{action.icon}
|
||||||
|
</div>
|
||||||
|
<h4 className="font-bold text-sm text-gray-900">{action.label}</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 leading-relaxed">
|
||||||
|
{action.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
<MessageSquare className="w-4 h-4 text-gray-500" />
|
||||||
|
Comments / Reason
|
||||||
|
</h3>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Provide a detailed reason for your decision..."
|
||||||
|
value={comments}
|
||||||
|
onChange={(e) => setComments(e.target.value)}
|
||||||
|
className="min-h-[120px] text-sm resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedAction === 'CANCEL' && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-100 rounded-lg flex items-start gap-3">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="text-xs text-red-800">
|
||||||
|
<p className="font-bold mb-1">Warning: Irreversible Action</p>
|
||||||
|
<p>Cancelling this request will permanently close it. This action cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="border-t pt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!selectedAction || !comments.trim() || submitting}
|
||||||
|
className={`
|
||||||
|
min-w-[120px]
|
||||||
|
${selectedAction === 'CANCEL' ? 'bg-red-600 hover:bg-red-700' : 'bg-purple-600 hover:bg-purple-700'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Confirm Action'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -14,3 +14,4 @@ export { DMSPushModal } from './DMSPushModal';
|
|||||||
export { EditClaimAmountModal } from './EditClaimAmountModal';
|
export { EditClaimAmountModal } from './EditClaimAmountModal';
|
||||||
export { EmailNotificationTemplateModal } from './EmailNotificationTemplateModal';
|
export { EmailNotificationTemplateModal } from './EmailNotificationTemplateModal';
|
||||||
export { InitiatorProposalApprovalModal } from './InitiatorProposalApprovalModal';
|
export { InitiatorProposalApprovalModal } from './InitiatorProposalApprovalModal';
|
||||||
|
export { InitiatorActionModal } from './InitiatorActionModal';
|
||||||
|
|||||||
@ -143,6 +143,7 @@ export async function submitCompletion(
|
|||||||
totalClosedExpenses?: number;
|
totalClosedExpenses?: number;
|
||||||
completionDocuments?: File[];
|
completionDocuments?: File[];
|
||||||
activityPhotos?: File[];
|
activityPhotos?: File[];
|
||||||
|
completionDescription?: string;
|
||||||
}
|
}
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
@ -162,6 +163,10 @@ export async function submitCompletion(
|
|||||||
formData.append('totalClosedExpenses', completionData.totalClosedExpenses.toString());
|
formData.append('totalClosedExpenses', completionData.totalClosedExpenses.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (completionData.completionDescription) {
|
||||||
|
formData.append('completionDescription', completionData.completionDescription);
|
||||||
|
}
|
||||||
|
|
||||||
if (completionData.completionDocuments) {
|
if (completionData.completionDocuments) {
|
||||||
completionData.completionDocuments.forEach((file) => {
|
completionData.completionDocuments.forEach((file) => {
|
||||||
formData.append('completionDocuments', file);
|
formData.append('completionDocuments', file);
|
||||||
|
|||||||
@ -504,6 +504,7 @@ export default {
|
|||||||
getWorkflowDetails,
|
getWorkflowDetails,
|
||||||
getWorkNotes,
|
getWorkNotes,
|
||||||
createWorkNoteMultipart,
|
createWorkNoteMultipart,
|
||||||
|
handleInitiatorAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function submitWorkflow(requestId: string) {
|
export async function submitWorkflow(requestId: string) {
|
||||||
@ -582,7 +583,26 @@ export async function updateBreachReason(levelId: string, breachReason: string):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also export in default for convenience
|
/**
|
||||||
// Note: keeping separate named export above for tree-shaking
|
* Handle initiator action on returned (rejected without closure) requests
|
||||||
|
* POST /api/v1/workflows/:id/initiator-action
|
||||||
|
*/
|
||||||
|
export async function handleInitiatorAction(
|
||||||
|
requestId: string,
|
||||||
|
action: 'REOPEN' | 'DISCUSS' | 'REVISE' | 'CANCEL',
|
||||||
|
data?: any
|
||||||
|
): Promise<any> {
|
||||||
|
const response = await apiClient.post(`/workflows/${requestId}/initiator-action`, {
|
||||||
|
action,
|
||||||
|
...data
|
||||||
|
});
|
||||||
|
return response.data?.data || response.data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get version history for a workflow request
|
||||||
|
* GET /api/v1/workflows/:id/history
|
||||||
|
*/
|
||||||
|
export async function getWorkflowHistory(requestId: string): Promise<any[]> {
|
||||||
|
const response = await apiClient.get(`/workflows/${requestId}/history`);
|
||||||
|
return response.data?.data || [];
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user