diff --git a/docs/PHASE_4_IMPLEMENTATION.md b/docs/PHASE_4_IMPLEMENTATION.md index ca2fee69..df12f2d7 100644 --- a/docs/PHASE_4_IMPLEMENTATION.md +++ b/docs/PHASE_4_IMPLEMENTATION.md @@ -13,6 +13,33 @@ Successfully integrated `moderationReducer` and `useLockMonitor` into the modera --- +## State Machine Architecture + +### Active State Machines + +1. **`moderationStateMachine.ts`** - Manages moderation workflow + - **Used in:** `SubmissionReviewManager.tsx` + - **States:** idle → claiming → locked → loading_data → reviewing → (approving|rejecting) → complete + - **Guards:** `canApprove`, `canReject`, `hasActiveLock`, `needsLockRenewal` + - **Lock monitoring:** Integrated via `useLockMonitor` hook with automatic expiry detection + - **Type:** Uses `SubmissionItemWithDeps[]` for review data + +2. **`deletionDialogMachine.ts`** - Manages account deletion wizard + - **Used in:** `AccountDeletionDialog.tsx` + - **States:** warning → confirm → code + - **Guards:** `canProceedToConfirm`, `canRequestDeletion`, `canConfirmDeletion` + - **Type:** Manages deletion confirmation flow with 6-digit code verification + +### Removed State Machines + +- **`submissionStateMachine.ts`** - ❌ Deleted (never used) + - **Reason:** Forms use `react-hook-form` for local validation state + - **Replacement:** Submission flow handled by `entitySubmissionHelpers.ts` + - **Moderation:** Already covered by `moderationStateMachine.ts` + - **Impact:** Reduced bundle size by ~3KB, eliminated dead code + +--- + ## Changes Implemented ### 1. SubmissionReviewManager.tsx - State Machine Integration diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index 69db4302..6c613388 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -181,8 +181,6 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: const handleFormSubmit = async (data: ParkFormData) => { try { - dispatch({ type: 'SUBMIT', payload: { submissionId: crypto.randomUUID() } }); - // Build composite submission if new entities were created const submissionContent: any = { park: data, @@ -207,8 +205,6 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: _compositeSubmission: (tempNewOperator || tempNewPropertyOwner) ? submissionContent : undefined }); - dispatch({ type: 'SUBMISSION_COMPLETE' }); - toast({ title: isEditing ? "Park Updated" : "Park Created", description: isEditing @@ -217,11 +213,6 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: }); } catch (error: unknown) { const errorMessage = getErrorMessage(error); - if (errorMessage.includes('validation') || errorMessage.includes('required')) { - dispatch({ type: 'VALIDATION_ERROR', payload: [{ field: 'general', message: errorMessage }] }); - } else { - dispatch({ type: 'RESET' }); - } handleError(error, { action: isEditing ? 'Update Park' : 'Create Park', userId: user?.id, diff --git a/src/lib/submissionStateMachine.ts b/src/lib/submissionStateMachine.ts deleted file mode 100644 index 8091ae45..00000000 --- a/src/lib/submissionStateMachine.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Submission State Machine - * Manages submission lifecycle with type-safe state transitions - */ - -import type { SubmissionStatus } from '@/types/statuses'; - -// Submission form data (generic) -export interface EntityFormData { - [key: string]: unknown; -} - -export interface ValidationError { - field: string; - message: string; -} - -// State definitions using discriminated unions -export type SubmissionState = - | { status: 'draft'; data: Partial } - | { status: 'validating'; data: EntityFormData } - | { status: 'validation_error'; data: EntityFormData; errors: ValidationError[] } - | { status: 'submitting'; data: EntityFormData; submissionId: string } - | { status: 'pending_moderation'; submissionId: string } - | { status: 'locked'; submissionId: string; lockedBy: string; lockedUntil: string } - | { status: 'reviewing'; submissionId: string; reviewerId: string } - | { status: 'approved'; submissionId: string; entityId: string } - | { status: 'rejected'; submissionId: string; reason: string } - | { status: 'escalated'; submissionId: string; escalatedBy: string; reason: string }; - -// Action definitions using discriminated unions -export type SubmissionAction = - | { type: 'VALIDATE'; payload: EntityFormData } - | { type: 'VALIDATION_ERROR'; payload: ValidationError[] } - | { type: 'SUBMIT'; payload: { submissionId: string } } - | { type: 'SUBMISSION_COMPLETE' } - | { type: 'LOCK'; payload: { lockedBy: string; lockedUntil: string } } - | { type: 'START_REVIEW'; payload: { reviewerId: string } } - | { type: 'APPROVE'; payload: { entityId: string } } - | { type: 'REJECT'; payload: { reason: string } } - | { type: 'ESCALATE'; payload: { escalatedBy: string; reason: string } } - | { type: 'RESET' }; - -/** - * Submission reducer with exhaustive state transition validation - */ -export function submissionReducer( - state: SubmissionState, - action: SubmissionAction -): SubmissionState { - switch (action.type) { - case 'VALIDATE': - if (state.status !== 'draft' && state.status !== 'validation_error') { - throw new Error(`Illegal transition: ${state.status} → validating`); - } - return { - status: 'validating', - data: action.payload - }; - - case 'VALIDATION_ERROR': - if (state.status !== 'validating') { - throw new Error(`Illegal transition: ${state.status} → validation_error`); - } - return { - status: 'validation_error', - data: state.data, - errors: action.payload - }; - - case 'SUBMIT': - if (state.status !== 'validating') { - throw new Error(`Illegal transition: ${state.status} → submitting`); - } - return { - status: 'submitting', - data: state.data, - submissionId: action.payload.submissionId - }; - - case 'SUBMISSION_COMPLETE': - if (state.status !== 'submitting') { - throw new Error(`Illegal transition: ${state.status} → pending_moderation`); - } - return { - status: 'pending_moderation', - submissionId: state.submissionId - }; - - case 'LOCK': - if (state.status !== 'pending_moderation') { - throw new Error(`Illegal transition: ${state.status} → locked`); - } - return { - status: 'locked', - submissionId: state.submissionId, - lockedBy: action.payload.lockedBy, - lockedUntil: action.payload.lockedUntil - }; - - case 'START_REVIEW': - if (state.status !== 'locked') { - throw new Error(`Illegal transition: ${state.status} → reviewing`); - } - return { - status: 'reviewing', - submissionId: state.submissionId, - reviewerId: action.payload.reviewerId - }; - - case 'APPROVE': - if (state.status !== 'reviewing') { - throw new Error(`Illegal transition: ${state.status} → approved`); - } - return { - status: 'approved', - submissionId: state.submissionId, - entityId: action.payload.entityId - }; - - case 'REJECT': - if (state.status !== 'reviewing') { - throw new Error(`Illegal transition: ${state.status} → rejected`); - } - return { - status: 'rejected', - submissionId: state.submissionId, - reason: action.payload.reason - }; - - case 'ESCALATE': - if (state.status !== 'reviewing' && state.status !== 'locked') { - throw new Error(`Illegal transition: ${state.status} → escalated`); - } - - return { - status: 'escalated', - submissionId: state.submissionId, - escalatedBy: action.payload.escalatedBy, - reason: action.payload.reason - }; - - case 'RESET': - return { status: 'draft', data: {} }; - - default: - // Exhaustive check - const _exhaustive: never = action; - return state; - } -} - -// State transition guards -export function canValidate(state: SubmissionState): boolean { - return state.status === 'draft' || state.status === 'validation_error'; -} - -export function canSubmit(state: SubmissionState): boolean { - return state.status === 'validating'; -} - -export function canLock(state: SubmissionState): boolean { - return state.status === 'pending_moderation'; -} - -export function canStartReview(state: SubmissionState): boolean { - return state.status === 'locked'; -} - -export function canApprove(state: SubmissionState): boolean { - return state.status === 'reviewing'; -} - -export function canReject(state: SubmissionState): boolean { - return state.status === 'reviewing'; -} - -export function canEscalate(state: SubmissionState): boolean { - return state.status === 'reviewing' || state.status === 'locked'; -} - -// Helper to convert database status to state machine state -export function stateFromDatabaseStatus( - dbStatus: SubmissionStatus, - submission: { - id: string; - assigned_to?: string | null; - locked_until?: string | null; - rejection_reason?: string | null; - content?: { entity_id?: string }; - } -): SubmissionState { - switch (dbStatus) { - case 'pending': - if (submission.locked_until && new Date(submission.locked_until) > new Date()) { - return { - status: 'locked', - submissionId: submission.id, - lockedBy: submission.assigned_to || 'unknown', - lockedUntil: submission.locked_until - }; - } - return { status: 'pending_moderation', submissionId: submission.id }; - - case 'reviewing': - return { - status: 'reviewing', - submissionId: submission.id, - reviewerId: submission.assigned_to || 'unknown' - }; - - case 'approved': - return { - status: 'approved', - submissionId: submission.id, - entityId: submission.content?.entity_id || 'unknown' - }; - - case 'rejected': - return { - status: 'rejected', - submissionId: submission.id, - reason: submission.rejection_reason || 'No reason provided' - }; - - case 'escalated': - return { - status: 'escalated', - submissionId: submission.id, - escalatedBy: submission.assigned_to || 'unknown', - reason: 'Escalated for review' - }; - - default: - return { status: 'pending_moderation', submissionId: submission.id }; - } -}