Re_Figma_Code/src/pages/CreateRequest/CreateRequest.tsx

384 lines
12 KiB
TypeScript

/**
* Main CreateRequest component - Clean composition only
*
* This component orchestrates the CreateRequest flow by:
* - Composing layout structure
* - Wiring up hooks and components
* - Handling top-level callbacks
*
* All business logic, API calls, and utilities are extracted to:
* - hooks/ - Business logic and state management
* - services/ - API operations
* - utils/ - Pure utility functions
* - components/ - UI components
*/
import { useState, useRef, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { TemplateSelectionModal } from '@/components/modals/TemplateSelectionModal';
import { FilePreview } from '@/components/common/FilePreview';
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
import { downloadDocument } from '@/services/workflowApi';
// Custom Hooks
import { useCreateRequestForm } from '@/hooks/useCreateRequestForm';
import { useWizardNavigation } from '@/hooks/useWizardNavigation';
import { useRequestModals } from './hooks/useRequestModals';
import { useCreateRequestSubmission } from './hooks/useCreateRequestSubmission';
import { useCreateRequestHandlers } from './hooks/useCreateRequestHandlers';
// Constants
import { REQUEST_TEMPLATES } from './constants/requestTemplates';
// Components
import { WizardStepper } from '@/components/workflow/CreateRequest/WizardStepper';
import { WizardFooter } from '@/components/workflow/CreateRequest/WizardFooter';
import { TemplateSelectionStep } from '@/components/workflow/CreateRequest/TemplateSelectionStep';
import { BasicInformationStep } from '@/components/workflow/CreateRequest/BasicInformationStep';
import { ApprovalWorkflowStep } from '@/components/workflow/CreateRequest/ApprovalWorkflowStep';
import { ParticipantsStep } from '@/components/workflow/CreateRequest/ParticipantsStep';
import { DocumentsStep } from '@/components/workflow/CreateRequest/DocumentsStep';
import { ReviewSubmitStep } from '@/components/workflow/CreateRequest/ReviewSubmitStep';
import { CreateRequestHeader } from './components/CreateRequestHeader';
import { CreateRequestContent } from './components/CreateRequestContent';
import { ValidationErrorModal } from './components/modals/ValidationErrorModal';
import { DocumentErrorModal } from './components/modals/DocumentErrorModal';
interface CreateRequestProps {
onBack?: () => void;
onSubmit?: (requestData: any) => void;
requestId?: string;
isEditMode?: boolean;
}
export function CreateRequest({
onBack,
onSubmit,
requestId: propRequestId,
isEditMode = false,
}: CreateRequestProps) {
const params = useParams<{ requestId: string }>();
const navigate = useNavigate();
const editRequestId = params.requestId || propRequestId || '';
const isEditing = isEditMode && !!editRequestId;
const { user } = useAuth();
// Form and state management hooks
const {
formData,
updateFormData,
selectedTemplate,
setSelectedTemplate,
loadingDraft,
systemPolicy,
documentPolicy,
existingDocuments,
setExistingDocuments,
} = useCreateRequestForm(isEditing, editRequestId, REQUEST_TEMPLATES);
const {
currentStep,
totalSteps,
stepNames,
isStepValid,
nextStep: wizardNextStep,
prevStep: wizardPrevStep,
} = useWizardNavigation(isEditing, selectedTemplate, formData);
// Document management state
const [documents, setDocuments] = useState<File[]>([]);
const [documentsToDelete, setDocumentsToDelete] = useState<string[]>([]);
const fileInputRef = useRef<HTMLInputElement>(null);
// Modal management
const {
validationModal,
policyViolationModal,
documentErrorModal,
openValidationModal,
closeValidationModal,
closePolicyViolationModal,
openDocumentErrorModal,
closeDocumentErrorModal,
} = useRequestModals();
// Submission logic
const { submitting, savingDraft, handleSubmit, handleSaveDraft } =
useCreateRequestSubmission({
formData,
selectedTemplate,
documents,
documentsToDelete,
user: user!,
isEditing,
editRequestId,
onSubmit,
});
// Event handlers
const {
showTemplateModal,
setShowTemplateModal,
previewDocument,
selectTemplate,
handleTemplateSelection,
nextStep,
prevStep,
handlePreviewDocument,
closePreview,
} = useCreateRequestHandlers({
selectedTemplate,
setSelectedTemplate,
updateFormData,
formData,
currentStep,
isStepValid,
wizardNextStep,
wizardPrevStep,
user: user!,
openValidationModal,
onSubmit,
});
// Handle back button:
// - Steps 1, 3, or 4: Navigate back to previous screen (browser history)
// - Other steps: Go to previous step in wizard
const handleBackButton = useCallback(() => {
if (currentStep === 1 || currentStep === 3 || currentStep === 4) {
// On steps 1, 3, or 4, navigate back to previous screen using browser history
if (onBack) {
onBack();
} else {
// Use window.history.back() as fallback for more reliable navigation
if (window.history.length > 1) {
window.history.back();
} else {
// If no history, navigate to dashboard
navigate('/dashboard', { replace: true });
}
}
} else {
// On other steps (2, 5, 6), go to previous step in wizard
prevStep();
}
}, [currentStep, onBack, navigate, prevStep]);
// Sync documents from formData only on initial mount (when loading draft)
const isInitialMount = useRef(true);
const documentsSyncedRef = useRef(false);
useEffect(() => {
// Only sync from formData on initial mount or when loading a draft
if (isInitialMount.current && formData.documents && formData.documents.length > 0 && !documentsSyncedRef.current) {
setDocuments(formData.documents);
documentsSyncedRef.current = true;
}
isInitialMount.current = false;
}, [formData.documents]);
// Update formData.documents when documents change (one-way sync from local state to formData)
// Use a ref to prevent circular updates
const isUpdatingFromFormData = useRef(false);
const prevDocumentsRef = useRef(documents);
useEffect(() => {
// Skip if we're currently syncing from formData
if (isUpdatingFromFormData.current) {
isUpdatingFromFormData.current = false;
prevDocumentsRef.current = documents;
return;
}
// Only update if documents actually changed
if (prevDocumentsRef.current !== documents) {
updateFormData('documents', documents);
prevDocumentsRef.current = documents;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [documents]);
// Render step content
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<TemplateSelectionStep
templates={REQUEST_TEMPLATES}
selectedTemplate={selectedTemplate}
onSelectTemplate={selectTemplate}
/>
);
case 2:
return (
<BasicInformationStep
formData={formData}
selectedTemplate={selectedTemplate}
updateFormData={updateFormData}
/>
);
case 3:
return (
<ApprovalWorkflowStep
formData={formData}
updateFormData={updateFormData}
onValidationError={(error) =>
openValidationModal(
error.type as 'error' | 'self-assign' | 'not-found',
error.email,
error.message
)
}
/>
);
case 4:
return (
<ParticipantsStep
formData={formData}
updateFormData={updateFormData}
onValidationError={(error) =>
openValidationModal(
error.type as 'error' | 'self-assign' | 'not-found',
error.email,
error.message
)
}
initiatorEmail={(user as any)?.email || ''}
/>
);
case 5:
return (
<DocumentsStep
documentPolicy={documentPolicy}
isEditing={isEditing}
documents={documents}
existingDocuments={existingDocuments}
documentsToDelete={documentsToDelete}
onDocumentsChange={setDocuments}
onExistingDocumentsChange={setExistingDocuments}
onDocumentsToDeleteChange={setDocumentsToDelete}
onPreviewDocument={handlePreviewDocument}
onDocumentErrors={(errors) => openDocumentErrorModal(errors)}
fileInputRef={fileInputRef}
/>
);
case 6:
return (
<ReviewSubmitStep
formData={formData}
selectedTemplate={selectedTemplate}
/>
);
default:
return null;
}
};
// Loading state
if (loadingDraft) {
return (
<div
className="min-h-screen bg-gray-50 flex items-center justify-center"
data-testid="create-request-loading"
>
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading draft...</p>
</div>
</div>
);
}
// Main render
return (
<div
className="h-full flex flex-col bg-gradient-to-br from-gray-50 to-white"
data-testid="create-request"
>
<CreateRequestHeader
isEditing={isEditing}
currentStep={currentStep}
totalSteps={totalSteps}
stepNames={stepNames}
onBack={handleBackButton}
/>
<WizardStepper
currentStep={currentStep}
totalSteps={totalSteps}
stepNames={stepNames}
/>
<CreateRequestContent>{renderStepContent()}</CreateRequestContent>
<WizardFooter
currentStep={currentStep}
totalSteps={totalSteps}
isStepValid={isStepValid()}
onPrev={prevStep}
onNext={nextStep}
onSubmit={handleSubmit}
onSaveDraft={handleSaveDraft}
submitting={submitting}
savingDraft={savingDraft}
loadingDraft={loadingDraft}
isEditing={isEditing}
/>
{/* Modals */}
<TemplateSelectionModal
open={showTemplateModal}
onClose={() => setShowTemplateModal(false)}
onSelectTemplate={handleTemplateSelection}
/>
{previewDocument && (
<FilePreview
fileName={previewDocument.fileName}
fileType={previewDocument.fileType}
fileUrl={previewDocument.fileUrl}
fileSize={previewDocument.fileSize}
open={!!previewDocument}
onClose={closePreview}
onDownload={async () => {
if (previewDocument.file) {
const link = document.createElement('a');
link.href = previewDocument.fileUrl;
link.download = previewDocument.fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else if (previewDocument.documentId) {
await downloadDocument(previewDocument.documentId);
}
}}
attachmentId={previewDocument.documentId}
/>
)}
<ValidationErrorModal
modal={validationModal}
onClose={closeValidationModal}
/>
<DocumentErrorModal
modal={documentErrorModal}
documentPolicy={documentPolicy}
onClose={closeDocumentErrorModal}
/>
<PolicyViolationModal
open={policyViolationModal.open}
onClose={closePolicyViolationModal}
violations={policyViolationModal.violations}
policyDetails={{
maxApprovalLevels: systemPolicy.maxApprovalLevels,
maxParticipants: systemPolicy.maxParticipants,
allowSpectators: systemPolicy.allowSpectators,
maxSpectators: systemPolicy.maxSpectators,
}}
/>
</div>
);
}