mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:51:14 -05:00
feat: Implement comprehensive validation error handling
This commit is contained in:
@@ -247,28 +247,68 @@ export function SubmissionReviewManager({
|
||||
}
|
||||
|
||||
// Run validation on all selected items
|
||||
const validationResultsMap = await validateMultipleItems(
|
||||
selectedItems.map(item => ({
|
||||
item_type: item.item_type,
|
||||
item_data: item.item_data,
|
||||
id: item.id
|
||||
}))
|
||||
);
|
||||
let validationResultsMap: Map<string, any>;
|
||||
|
||||
setValidationResults(validationResultsMap);
|
||||
try {
|
||||
validationResultsMap = await validateMultipleItems(
|
||||
selectedItems.map(item => ({
|
||||
item_type: item.item_type,
|
||||
item_data: item.item_data,
|
||||
id: item.id
|
||||
}))
|
||||
);
|
||||
|
||||
// Check for blocking errors
|
||||
const itemsWithBlockingErrors = selectedItems.filter(item => {
|
||||
const result = validationResultsMap.get(item.id);
|
||||
return result && result.blockingErrors.length > 0;
|
||||
});
|
||||
setValidationResults(validationResultsMap);
|
||||
|
||||
// CRITICAL: Blocking errors can NEVER be bypassed, regardless of warnings
|
||||
if (itemsWithBlockingErrors.length > 0) {
|
||||
setHasBlockingErrors(true);
|
||||
setShowValidationBlockerDialog(true);
|
||||
dispatch({ type: 'ERROR', payload: { error: 'Validation failed' } });
|
||||
return; // Block approval
|
||||
// Check for blocking errors
|
||||
const itemsWithBlockingErrors = selectedItems.filter(item => {
|
||||
const result = validationResultsMap.get(item.id);
|
||||
return result && result.blockingErrors.length > 0;
|
||||
});
|
||||
|
||||
// CRITICAL: Blocking errors can NEVER be bypassed, regardless of warnings
|
||||
if (itemsWithBlockingErrors.length > 0) {
|
||||
// Log which items have blocking errors
|
||||
itemsWithBlockingErrors.forEach(item => {
|
||||
const result = validationResultsMap.get(item.id);
|
||||
logger.error('Blocking validation errors prevent approval', {
|
||||
submissionId,
|
||||
itemId: item.id,
|
||||
itemType: item.item_type,
|
||||
errors: result?.blockingErrors
|
||||
});
|
||||
});
|
||||
|
||||
setHasBlockingErrors(true);
|
||||
setShowValidationBlockerDialog(true);
|
||||
dispatch({ type: 'ERROR', payload: { error: 'Validation failed' } });
|
||||
return; // Block approval
|
||||
}
|
||||
} catch (error) {
|
||||
// Validation itself failed (network error, bug, etc.)
|
||||
const errorId = handleError(error, {
|
||||
action: 'Validation System Error',
|
||||
userId: user?.id,
|
||||
metadata: {
|
||||
submissionId,
|
||||
selectedItemCount: selectedItems.length,
|
||||
itemTypes: selectedItems.map(i => i.item_type)
|
||||
}
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Validation System Error',
|
||||
description: (
|
||||
<div className="space-y-2">
|
||||
<p>Unable to validate submission. Please try again.</p>
|
||||
<p className="text-xs font-mono">Ref: {errorId.slice(0, 8)}</p>
|
||||
</div>
|
||||
),
|
||||
variant: 'destructive'
|
||||
});
|
||||
|
||||
dispatch({ type: 'ERROR', payload: { error: 'Validation system error' } });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for warnings
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { AlertCircle, ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -9,6 +10,9 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { ValidationError } from '@/lib/entityValidationSchemas';
|
||||
|
||||
interface ValidationBlockerDialogProps {
|
||||
@@ -24,9 +28,11 @@ export function ValidationBlockerDialog({
|
||||
blockingErrors,
|
||||
itemNames,
|
||||
}: ValidationBlockerDialogProps) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onClose}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogContent className="max-w-2xl">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="flex items-center gap-2 text-destructive">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
@@ -34,29 +40,52 @@ export function ValidationBlockerDialog({
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
The following items have blocking validation errors that MUST be fixed before approval.
|
||||
These items cannot be approved until the errors are resolved. Please edit or reject them.
|
||||
Edit the items to fix the errors, or reject them.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<div className="space-y-3 my-4">
|
||||
{itemNames.map((name, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="font-medium text-sm">{name}</div>
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="space-y-1">
|
||||
{blockingErrors
|
||||
.filter((_, i) => i === index || itemNames.length === 1)
|
||||
.map((error, errIndex) => (
|
||||
{itemNames.map((name, index) => {
|
||||
const itemErrors = blockingErrors.filter((_, i) =>
|
||||
itemNames.length === 1 || i === index
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="font-medium text-sm flex items-center justify-between">
|
||||
<span>{name}</span>
|
||||
<Badge variant="destructive">
|
||||
{itemErrors.length} error{itemErrors.length > 1 ? 's' : ''}
|
||||
</Badge>
|
||||
</div>
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="space-y-1">
|
||||
{itemErrors.map((error, errIndex) => (
|
||||
<div key={errIndex} className="text-sm">
|
||||
• <span className="font-medium">{error.field}:</span> {error.message}
|
||||
</div>
|
||||
))}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
))}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Collapsible open={showDetails} onOpenChange={setShowDetails}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="w-full">
|
||||
{showDetails ? 'Hide' : 'Show'} Technical Details
|
||||
<ChevronDown className={`ml-2 h-4 w-4 transition-transform ${showDetails ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-2">
|
||||
<div className="bg-muted p-3 rounded text-xs font-mono max-h-60 overflow-auto">
|
||||
<pre>{JSON.stringify(blockingErrors, null, 2)}</pre>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={onClose}>
|
||||
Close
|
||||
|
||||
@@ -171,42 +171,93 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
||||
});
|
||||
|
||||
// Run validation on all items
|
||||
const validationResults = await validateMultipleItems(itemsWithData);
|
||||
try {
|
||||
const validationResults = await validateMultipleItems(itemsWithData);
|
||||
|
||||
// Check for blocking errors
|
||||
const itemsWithBlockingErrors = itemsWithData.filter(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
return result && result.blockingErrors.length > 0;
|
||||
});
|
||||
|
||||
// CRITICAL: Block approval if any item has blocking errors
|
||||
if (itemsWithBlockingErrors.length > 0) {
|
||||
const errorDetails = itemsWithBlockingErrors.map(item => {
|
||||
// Check for blocking errors
|
||||
const itemsWithBlockingErrors = itemsWithData.filter(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
return `${item.item_type}: ${result?.blockingErrors[0]?.message || 'Unknown error'}`;
|
||||
}).join(', ');
|
||||
|
||||
toast({
|
||||
title: 'Cannot Approve - Validation Errors',
|
||||
description: `${itemsWithBlockingErrors.length} item(s) have blocking errors that must be fixed first. ${errorDetails}`,
|
||||
variant: 'destructive',
|
||||
return result && result.blockingErrors.length > 0;
|
||||
});
|
||||
|
||||
// Return early - do NOT proceed with approval
|
||||
return;
|
||||
}
|
||||
// CRITICAL: Block approval if any item has blocking errors
|
||||
if (itemsWithBlockingErrors.length > 0) {
|
||||
// Log detailed blocking errors
|
||||
itemsWithBlockingErrors.forEach(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
logger.error('Validation blocking approval', {
|
||||
submissionId: item.id,
|
||||
itemId: item.id,
|
||||
itemType: item.item_type,
|
||||
blockingErrors: result?.blockingErrors
|
||||
});
|
||||
});
|
||||
|
||||
// Check for warnings (optional - can proceed but inform user)
|
||||
const itemsWithWarnings = itemsWithData.filter(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
return result && result.warnings.length > 0;
|
||||
});
|
||||
const errorDetails = itemsWithBlockingErrors.map(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
const itemName = (item.item_data as any)?.name || item.item_type;
|
||||
const errors = result?.blockingErrors.map(e => `${e.field}: ${e.message}`).join(', ');
|
||||
return `${itemName} - ${errors}`;
|
||||
}).join('; ');
|
||||
|
||||
if (itemsWithWarnings.length > 0) {
|
||||
logger.info('Approval proceeding with warnings', {
|
||||
submissionId: item.id,
|
||||
warningCount: itemsWithWarnings.length
|
||||
throw new Error(`Validation failed: ${errorDetails}`);
|
||||
}
|
||||
|
||||
// Check for warnings (optional - can proceed but inform user)
|
||||
const itemsWithWarnings = itemsWithData.filter(item => {
|
||||
const result = validationResults.get(item.id);
|
||||
return result && result.warnings.length > 0;
|
||||
});
|
||||
|
||||
if (itemsWithWarnings.length > 0) {
|
||||
logger.info('Approval proceeding with warnings', {
|
||||
submissionId: item.id,
|
||||
warningCount: itemsWithWarnings.length
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Check if this is a validation error or system error
|
||||
if (getErrorMessage(error).includes('Validation failed:')) {
|
||||
// This is expected - validation rules preventing approval
|
||||
handleError(error, {
|
||||
action: 'Validation Blocked Approval',
|
||||
userId: user?.id,
|
||||
metadata: {
|
||||
submissionId: item.id,
|
||||
submissionType: item.submission_type,
|
||||
selectedItemCount: itemsWithData.length
|
||||
}
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Cannot Approve - Validation Errors',
|
||||
description: getErrorMessage(error),
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
// Return early - do NOT proceed with approval
|
||||
return;
|
||||
} else {
|
||||
// Unexpected validation system error
|
||||
const errorId = handleError(error, {
|
||||
action: 'Validation System Failure',
|
||||
userId: user?.id,
|
||||
metadata: {
|
||||
submissionId: item.id,
|
||||
submissionType: item.submission_type,
|
||||
phase: 'validation'
|
||||
}
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Validation System Error',
|
||||
description: `Unable to validate submission (ref: ${errorId.slice(0, 8)})`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
// Return early - do NOT proceed with approval
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import { handleNonCriticalError, getErrorMessage } from '@/lib/errorHandler';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
// ============================================
|
||||
// CENTRALIZED VALIDATION SCHEMAS
|
||||
@@ -452,35 +454,71 @@ export async function validateEntityData(
|
||||
entityType: keyof typeof entitySchemas,
|
||||
data: unknown
|
||||
): Promise<ValidationResult> {
|
||||
const schema = entitySchemas[entityType];
|
||||
|
||||
if (!schema) {
|
||||
return {
|
||||
isValid: false,
|
||||
blockingErrors: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }],
|
||||
warnings: [],
|
||||
suggestions: [],
|
||||
allErrors: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }],
|
||||
};
|
||||
}
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
const blockingErrors: ValidationError[] = [];
|
||||
const warnings: ValidationError[] = [];
|
||||
const suggestions: ValidationError[] = [];
|
||||
|
||||
// Process Zod errors
|
||||
if (!result.success) {
|
||||
const zodError = result.error as z.ZodError;
|
||||
zodError.issues.forEach((issue) => {
|
||||
const field = issue.path.join('.');
|
||||
blockingErrors.push({
|
||||
field: field || 'unknown',
|
||||
message: issue.message,
|
||||
severity: 'blocking',
|
||||
try {
|
||||
// Debug logging for operator entity
|
||||
if (entityType === 'operator') {
|
||||
logger.log('Validating operator entity', {
|
||||
dataKeys: data ? Object.keys(data as object) : [],
|
||||
dataTypes: data ? Object.entries(data as object).reduce((acc, [key, val]) => {
|
||||
acc[key] = typeof val;
|
||||
return acc;
|
||||
}, {} as Record<string, string>) : {},
|
||||
rawData: JSON.stringify(data).substring(0, 500)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const schema = entitySchemas[entityType];
|
||||
|
||||
if (!schema) {
|
||||
const error = {
|
||||
field: 'entity_type',
|
||||
message: `Unknown entity type: ${entityType}`,
|
||||
severity: 'blocking' as const
|
||||
};
|
||||
|
||||
handleNonCriticalError(new Error(`Unknown entity type: ${entityType}`), {
|
||||
action: 'Entity Validation',
|
||||
metadata: { entityType, providedData: data }
|
||||
});
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
blockingErrors: [error],
|
||||
warnings: [],
|
||||
suggestions: [],
|
||||
allErrors: [error],
|
||||
};
|
||||
}
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
const blockingErrors: ValidationError[] = [];
|
||||
const warnings: ValidationError[] = [];
|
||||
const suggestions: ValidationError[] = [];
|
||||
|
||||
// Process Zod errors
|
||||
if (!result.success) {
|
||||
const zodError = result.error as z.ZodError;
|
||||
|
||||
// Log detailed validation failure
|
||||
handleNonCriticalError(zodError, {
|
||||
action: 'Zod Validation Failed',
|
||||
metadata: {
|
||||
entityType,
|
||||
issues: zodError.issues,
|
||||
providedData: JSON.stringify(data).substring(0, 500),
|
||||
issueCount: zodError.issues.length
|
||||
}
|
||||
});
|
||||
|
||||
zodError.issues.forEach((issue) => {
|
||||
const field = issue.path.join('.') || entityType;
|
||||
blockingErrors.push({
|
||||
field,
|
||||
message: `${issue.message} (code: ${issue.code})`,
|
||||
severity: 'blocking',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add warnings for optional but recommended fields
|
||||
const validData = data as Record<string, unknown>;
|
||||
@@ -585,16 +623,43 @@ export async function validateEntityData(
|
||||
}
|
||||
}
|
||||
|
||||
const allErrors = [...blockingErrors, ...warnings, ...suggestions];
|
||||
const isValid = blockingErrors.length === 0;
|
||||
const allErrors = [...blockingErrors, ...warnings, ...suggestions];
|
||||
const isValid = blockingErrors.length === 0;
|
||||
|
||||
return {
|
||||
isValid,
|
||||
blockingErrors,
|
||||
warnings,
|
||||
suggestions,
|
||||
allErrors,
|
||||
};
|
||||
return {
|
||||
isValid,
|
||||
blockingErrors,
|
||||
warnings,
|
||||
suggestions,
|
||||
allErrors,
|
||||
};
|
||||
} catch (error) {
|
||||
// Catch any unexpected errors during validation
|
||||
const errorId = handleNonCriticalError(error, {
|
||||
action: 'Entity Validation Unexpected Error',
|
||||
metadata: {
|
||||
entityType,
|
||||
dataType: typeof data,
|
||||
hasData: !!data
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
blockingErrors: [{
|
||||
field: entityType,
|
||||
message: `Validation error: ${getErrorMessage(error)} (ref: ${errorId.slice(0, 8)})`,
|
||||
severity: 'blocking'
|
||||
}],
|
||||
warnings: [],
|
||||
suggestions: [],
|
||||
allErrors: [{
|
||||
field: entityType,
|
||||
message: `Validation error: ${getErrorMessage(error)} (ref: ${errorId.slice(0, 8)})`,
|
||||
severity: 'blocking'
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user