mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:31:12 -05:00
Fix state machine cleanup
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<EntityFormData> }
|
||||
| { 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user