Compare commits

...

2 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
d29e873e14 feat: Implement comprehensive validation error handling 2025-11-05 19:00:28 +00:00
gpt-engineer-app[bot]
882959bce6 Refactor: Use consolidated escalateSubmission action 2025-11-05 18:49:21 +00:00
6 changed files with 357 additions and 148 deletions

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { AlertTriangle } from 'lucide-react';
import { AlertTriangle, AlertCircle } from 'lucide-react';
import {
Dialog,
DialogContent,
@@ -18,12 +18,14 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
interface EscalationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onEscalate: (reason: string) => Promise<void>;
submissionType: string;
error?: { message: string; errorId?: string } | null;
}
const escalationReasons = [
@@ -40,6 +42,7 @@ export function EscalationDialog({
onOpenChange,
onEscalate,
submissionType,
error,
}: EscalationDialogProps) {
const [selectedReason, setSelectedReason] = useState('');
const [additionalNotes, setAdditionalNotes] = useState('');
@@ -76,6 +79,23 @@ export function EscalationDialog({
</DialogDescription>
</DialogHeader>
{error && (
<Alert variant="destructive" className="mt-4">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Escalation Failed</AlertTitle>
<AlertDescription>
<div className="space-y-2">
<p className="text-sm">{error.message}</p>
{error.errorId && (
<p className="text-xs font-mono bg-destructive/10 px-2 py-1 rounded">
Reference: {error.errorId.slice(0, 8)}
</p>
)}
</div>
</AlertDescription>
</Alert>
)}
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Escalation Reason</Label>

View File

@@ -12,12 +12,12 @@ import {
detectDependencyConflicts,
approveSubmissionItems,
rejectSubmissionItems,
escalateSubmission,
checkSubmissionConflict,
type SubmissionItemWithDeps,
type DependencyConflict,
type ConflictCheckResult
} from '@/lib/submissionItemsService';
import { useModerationActions } from '@/hooks/moderation/useModerationActions';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
@@ -77,6 +77,10 @@ export function SubmissionReviewManager({
const [conflictData, setConflictData] = useState<ConflictCheckResult | null>(null);
const [showConflictResolutionModal, setShowConflictResolutionModal] = useState(false);
const [lastModifiedTimestamp, setLastModifiedTimestamp] = useState<string | null>(null);
const [escalationError, setEscalationError] = useState<{
message: string;
errorId?: string;
} | null>(null);
const { toast } = useToast();
const { isAdmin, isSuperuser } = useUserRole();
@@ -87,6 +91,17 @@ export function SubmissionReviewManager({
// Lock monitoring integration
const { extendLock } = useLockMonitor(state, dispatch, submissionId);
// Moderation actions
const { escalateSubmission } = useModerationActions({
user,
onActionStart: (itemId: string) => {
logger.log(`Starting escalation for ${itemId}`);
},
onActionComplete: () => {
logger.log('Escalation complete');
}
});
// Auto-claim on mount
useEffect(() => {
if (open && submissionId && state.status === 'idle') {
@@ -232,7 +247,10 @@ export function SubmissionReviewManager({
}
// Run validation on all selected items
const validationResultsMap = await validateMultipleItems(
let validationResultsMap: Map<string, any>;
try {
validationResultsMap = await validateMultipleItems(
selectedItems.map(item => ({
item_type: item.item_type,
item_data: item.item_data,
@@ -250,11 +268,48 @@ export function SubmissionReviewManager({
// 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
const itemsWithWarnings = selectedItems.filter(item => {
@@ -425,50 +480,35 @@ export function SubmissionReviewManager({
}
try {
const { supabase } = await import('@/integrations/supabase/client');
setEscalationError(null);
// Call the escalation notification edge function
const { data, error, requestId } = await invokeWithTracking(
'send-escalation-notification',
// Use consolidated action from useModerationActions
// This handles: edge function call, fallback, error logging, cache invalidation
await escalateSubmission(
{
submissionId,
escalationReason: reason,
escalatedBy: user.id
},
user.id
id: submissionId,
submission_type: submissionType,
type: 'submission'
} as any,
reason
);
if (error) {
handleError(error, {
action: 'Send escalation notification',
userId: user.id,
metadata: { submissionId }
});
// Fallback to direct database update if email fails
await escalateSubmission(submissionId, reason, user.id);
toast({
title: 'Escalated (Email Failed)',
description: 'Submission escalated but notification email failed to send',
variant: 'default',
});
} else {
toast({
title: 'Escalated Successfully',
description: 'Submission escalated and admin notified via email',
});
}
// Success - close dialog
onComplete();
onOpenChange(false);
} catch (error: unknown) {
handleError(error, {
action: 'Escalate Submission',
userId: user?.id,
metadata: {
submissionId,
reason: reason.substring(0, 100)
}
} catch (error: any) {
// Track error for retry UI
setEscalationError({
message: getErrorMessage(error),
errorId: error.errorId
});
logger.error('Escalation failed in SubmissionReviewManager', {
submissionId,
error: getErrorMessage(error)
});
// Don't close dialog on error - let user retry
}
};
@@ -587,6 +627,7 @@ export function SubmissionReviewManager({
onOpenChange={setShowEscalationDialog}
onEscalate={handleEscalate}
submissionType={submissionType}
error={escalationError}
/>
<RejectionDialog

View File

@@ -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,19 +40,27 @@ 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) => (
{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">{name}</div>
<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">
{blockingErrors
.filter((_, i) => i === index || itemNames.length === 1)
.map((error, errIndex) => (
{itemErrors.map((error, errIndex) => (
<div key={errIndex} className="text-sm">
<span className="font-medium">{error.field}:</span> {error.message}
</div>
@@ -54,9 +68,24 @@ export function ValidationBlockerDialog({
</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

View File

@@ -171,6 +171,7 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
});
// Run validation on all items
try {
const validationResults = await validateMultipleItems(itemsWithData);
// Check for blocking errors
@@ -181,19 +182,25 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
// CRITICAL: Block approval if any item has blocking errors
if (itemsWithBlockingErrors.length > 0) {
const errorDetails = itemsWithBlockingErrors.map(item => {
// Log detailed blocking errors
itemsWithBlockingErrors.forEach(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',
logger.error('Validation blocking approval', {
submissionId: item.id,
itemId: item.id,
itemType: item.item_type,
blockingErrors: result?.blockingErrors
});
});
// Return early - do NOT proceed with approval
return;
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)
@@ -208,6 +215,50 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
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;
}
}
}
const { data, error, requestId } = await invokeWithTracking(

View File

@@ -372,7 +372,10 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
return Math.max(0, currentLock.expiresAt.getTime() - Date.now());
}, [currentLock]);
// Escalate submission
/**
* @deprecated Use escalateSubmission from useModerationActions instead
* This method only updates the database and doesn't send email notifications
*/
const escalateSubmission = useCallback(async (submissionId: string, reason: string): Promise<boolean> => {
if (!user?.id) return false;

View File

@@ -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,15 +454,39 @@ export async function validateEntityData(
entityType: keyof typeof entitySchemas,
data: unknown
): Promise<ValidationResult> {
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: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }],
blockingErrors: [error],
warnings: [],
suggestions: [],
allErrors: [{ field: 'entity_type', message: `Unknown entity type: ${entityType}`, severity: 'blocking' }],
allErrors: [error],
};
}
@@ -472,11 +498,23 @@ export async function validateEntityData(
// 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('.');
const field = issue.path.join('.') || entityType;
blockingErrors.push({
field: field || 'unknown',
message: issue.message,
field,
message: `${issue.message} (code: ${issue.code})`,
severity: 'blocking',
});
});
@@ -595,6 +633,33 @@ export async function validateEntityData(
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'
}],
};
}
}
/**