diff --git a/src/components/moderation/SubmissionReviewManager.tsx b/src/components/moderation/SubmissionReviewManager.tsx index 7c77bcc6..cf49b316 100644 --- a/src/components/moderation/SubmissionReviewManager.tsx +++ b/src/components/moderation/SubmissionReviewManager.tsx @@ -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; - setValidationResults(validationResultsMap); - - // 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) { - setHasBlockingErrors(true); - setShowValidationBlockerDialog(true); - dispatch({ type: 'ERROR', payload: { error: 'Validation failed' } }); - return; // Block approval + try { + validationResultsMap = await validateMultipleItems( + selectedItems.map(item => ({ + item_type: item.item_type, + item_data: item.item_data, + id: item.id + })) + ); + + setValidationResults(validationResultsMap); + + // 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: ( +
+

Unable to validate submission. Please try again.

+

Ref: {errorId.slice(0, 8)}

+
+ ), + variant: 'destructive' + }); + + dispatch({ type: 'ERROR', payload: { error: 'Validation system error' } }); + return; } // Check for warnings diff --git a/src/components/moderation/ValidationBlockerDialog.tsx b/src/components/moderation/ValidationBlockerDialog.tsx index 7896732a..9eb81060 100644 --- a/src/components/moderation/ValidationBlockerDialog.tsx +++ b/src/components/moderation/ValidationBlockerDialog.tsx @@ -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 ( - + @@ -34,28 +40,51 @@ export function ValidationBlockerDialog({ 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.
- {itemNames.map((name, index) => ( -
-
{name}
- - - {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 ( +
+
+ {name} + + {itemErrors.length} error{itemErrors.length > 1 ? 's' : ''} + +
+ + + {itemErrors.map((error, errIndex) => (
{error.field}: {error.message}
))} -
-
-
- ))} +
+
+
+ ); + })}
+ + + + + + +
+
{JSON.stringify(blockingErrors, null, 2)}
+
+
+
diff --git a/src/hooks/moderation/useModerationActions.ts b/src/hooks/moderation/useModerationActions.ts index 728b2dd9..48ae7076 100644 --- a/src/hooks/moderation/useModerationActions.ts +++ b/src/hooks/moderation/useModerationActions.ts @@ -171,42 +171,93 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio }); // Run validation on all items - 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 => { + try { + const validationResults = await validateMultipleItems(itemsWithData); + + // 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; - } - - // 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 + // 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 + }); + }); + + 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('; '); + + 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; + } } } diff --git a/src/lib/entityValidationSchemas.ts b/src/lib/entityValidationSchemas.ts index 8eefff1c..50596cb4 100644 --- a/src/lib/entityValidationSchemas.ts +++ b/src/lib/entityValidationSchemas.ts @@ -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 { - 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) : {}, + 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; @@ -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' + }], + }; + } } /**