mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:11:16 -05:00
feat: Implement emergency hotfixes
This commit is contained in:
@@ -222,17 +222,26 @@ export function SubmissionReviewManager({
|
|||||||
const successCount = data.results.filter((r: any) => r.success).length;
|
const successCount = data.results.filter((r: any) => r.success).length;
|
||||||
const failCount = data.results.filter((r: any) => !r.success).length;
|
const failCount = data.results.filter((r: any) => !r.success).length;
|
||||||
|
|
||||||
|
const allFailed = failCount > 0 && successCount === 0;
|
||||||
|
const someFailed = failCount > 0 && successCount > 0;
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Approval Complete',
|
title: allFailed ? 'Approval Failed' : someFailed ? 'Partial Approval' : 'Approval Complete',
|
||||||
description: failCount > 0
|
description: failCount > 0
|
||||||
? `Approved ${successCount} item(s), ${failCount} failed`
|
? `Approved ${successCount} item(s), ${failCount} failed`
|
||||||
: `Successfully approved ${successCount} item(s)`,
|
: `Successfully approved ${successCount} item(s)`,
|
||||||
variant: failCount > 0 ? 'destructive' : 'default',
|
variant: allFailed ? 'destructive' : someFailed ? 'default' : 'default',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset warning confirmation state after approval
|
// Reset warning confirmation state after approval
|
||||||
setUserConfirmedWarnings(false);
|
setUserConfirmedWarnings(false);
|
||||||
|
|
||||||
|
// If ALL items failed, don't close dialog - show errors
|
||||||
|
if (allFailed) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onComplete();
|
onComplete();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -127,25 +127,34 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
// Refs for tracking
|
// Refs for tracking
|
||||||
const recentlyRemovedRef = useRef<Set<string>>(new Set());
|
const recentlyRemovedRef = useRef<Set<string>>(new Set());
|
||||||
const fetchInProgressRef = useRef(false);
|
const fetchInProgressRef = useRef(false);
|
||||||
const itemsRef = useRef<ModerationItem[]>([]);
|
|
||||||
const lastFetchTimeRef = useRef<number>(0);
|
const lastFetchTimeRef = useRef<number>(0);
|
||||||
const pauseFetchingRef = useRef(false);
|
|
||||||
const initialFetchCompleteRef = useRef(false);
|
const initialFetchCompleteRef = useRef(false);
|
||||||
const isMountingRef = useRef(true);
|
const isMountingRef = useRef(true);
|
||||||
const fetchItemsRef = useRef<((silent?: boolean) => Promise<void>) | null>(null);
|
|
||||||
|
|
||||||
const FETCH_COOLDOWN_MS = 1000;
|
const FETCH_COOLDOWN_MS = 1000;
|
||||||
|
|
||||||
// Store settings in refs to avoid re-creating fetchItems
|
// Store settings, filters, sort, and pagination in refs to stabilize fetchItems
|
||||||
const settingsRef = useRef(settings);
|
const settingsRef = useRef(settings);
|
||||||
|
const filtersRef = useRef(filters);
|
||||||
|
const sortRef = useRef(sort.debouncedConfig);
|
||||||
|
const paginationRef = useRef(pagination);
|
||||||
|
|
||||||
|
// Sync refs with state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
settingsRef.current = settings;
|
settingsRef.current = settings;
|
||||||
}, [settings]);
|
}, [settings]);
|
||||||
|
|
||||||
// Sync items with ref
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
itemsRef.current = items;
|
filtersRef.current = filters;
|
||||||
}, [items]);
|
}, [filters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sortRef.current = sort.debouncedConfig;
|
||||||
|
}, [sort.debouncedConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
paginationRef.current = pagination;
|
||||||
|
}, [pagination]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch queue items from database
|
* Fetch queue items from database
|
||||||
@@ -160,20 +169,13 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
|
|
||||||
console.log("🔄 [FETCH ITEMS] Called", {
|
console.log("🔄 [FETCH ITEMS] Called", {
|
||||||
silent,
|
silent,
|
||||||
pauseFetchingRef: pauseFetchingRef.current,
|
|
||||||
documentHidden: document.hidden,
|
documentHidden: document.hidden,
|
||||||
caller: callerLine,
|
caller: callerLine,
|
||||||
sortField: sort.debouncedConfig.field,
|
sortField: sortRef.current.field,
|
||||||
sortDirection: sort.debouncedConfig.direction,
|
sortDirection: sortRef.current.direction,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if fetching is paused (controlled by visibility handler if enabled)
|
|
||||||
if (pauseFetchingRef.current) {
|
|
||||||
console.log("⏸️ Fetch paused by pauseFetchingRef");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent concurrent calls
|
// Prevent concurrent calls
|
||||||
if (fetchInProgressRef.current) {
|
if (fetchInProgressRef.current) {
|
||||||
console.log("⚠️ Fetch already in progress, skipping");
|
console.log("⚠️ Fetch already in progress, skipping");
|
||||||
@@ -192,10 +194,10 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
lastFetchTimeRef.current = now;
|
lastFetchTimeRef.current = now;
|
||||||
|
|
||||||
console.log("🔍 fetchItems called:", {
|
console.log("🔍 fetchItems called:", {
|
||||||
entityFilter: filters.debouncedEntityFilter,
|
entityFilter: filtersRef.current.debouncedEntityFilter,
|
||||||
statusFilter: filters.debouncedStatusFilter,
|
statusFilter: filtersRef.current.debouncedStatusFilter,
|
||||||
sortField: sort.debouncedConfig.field,
|
sortField: sortRef.current.field,
|
||||||
sortDirection: sort.debouncedConfig.direction,
|
sortDirection: sortRef.current.direction,
|
||||||
silent,
|
silent,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
@@ -237,28 +239,28 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
// CRITICAL: Multi-level ordering
|
// CRITICAL: Multi-level ordering
|
||||||
console.log('📊 [SORT QUERY] Applying multi-level sort:', {
|
console.log('📊 [SORT QUERY] Applying multi-level sort:', {
|
||||||
level1: 'escalated DESC',
|
level1: 'escalated DESC',
|
||||||
level2: `${sort.debouncedConfig.field} ${sort.debouncedConfig.direction.toUpperCase()}`,
|
level2: `${sortRef.current.field} ${sortRef.current.direction.toUpperCase()}`,
|
||||||
level3: sort.debouncedConfig.field !== 'created_at' ? 'created_at ASC' : 'none'
|
level3: sortRef.current.field !== 'created_at' ? 'created_at ASC' : 'none'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Level 1: Always sort by escalated first (descending)
|
// Level 1: Always sort by escalated first (descending)
|
||||||
submissionsQuery = submissionsQuery.order('escalated', { ascending: false });
|
submissionsQuery = submissionsQuery.order('escalated', { ascending: false });
|
||||||
|
|
||||||
// Level 2: Apply user-selected sort (use debounced config)
|
// Level 2: Apply user-selected sort (use ref)
|
||||||
submissionsQuery = submissionsQuery.order(
|
submissionsQuery = submissionsQuery.order(
|
||||||
sort.debouncedConfig.field,
|
sortRef.current.field,
|
||||||
{ ascending: sort.debouncedConfig.direction === 'asc' }
|
{ ascending: sortRef.current.direction === 'asc' }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Level 3: Tertiary sort by created_at (if not already primary)
|
// Level 3: Tertiary sort by created_at (if not already primary)
|
||||||
if (sort.debouncedConfig.field !== 'created_at') {
|
if (sortRef.current.field !== 'created_at') {
|
||||||
submissionsQuery = submissionsQuery.order('created_at', { ascending: true });
|
submissionsQuery = submissionsQuery.order('created_at', { ascending: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply tab-based status filtering
|
// Apply tab-based status filtering (use refs)
|
||||||
const tab = filters.activeTab;
|
const tab = filtersRef.current.activeTab;
|
||||||
const statusFilter = filters.debouncedStatusFilter;
|
const statusFilter = filtersRef.current.debouncedStatusFilter;
|
||||||
const entityFilter = filters.debouncedEntityFilter;
|
const entityFilter = filtersRef.current.debouncedEntityFilter;
|
||||||
|
|
||||||
if (tab === "mainQueue") {
|
if (tab === "mainQueue") {
|
||||||
if (statusFilter === "all") {
|
if (statusFilter === "all") {
|
||||||
@@ -328,11 +330,11 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
|
|
||||||
const { count } = await countQuery;
|
const { count } = await countQuery;
|
||||||
|
|
||||||
pagination.setTotalCount(count || 0);
|
paginationRef.current.setTotalCount(count || 0);
|
||||||
|
|
||||||
// Apply pagination
|
// Apply pagination (use refs)
|
||||||
const startIndex = pagination.startIndex;
|
const startIndex = paginationRef.current.startIndex;
|
||||||
const endIndex = pagination.endIndex;
|
const endIndex = paginationRef.current.endIndex;
|
||||||
submissionsQuery = submissionsQuery.range(startIndex, endIndex);
|
submissionsQuery = submissionsQuery.range(startIndex, endIndex);
|
||||||
|
|
||||||
const { data: submissions, error: submissionsError } = await submissionsQuery;
|
const { data: submissions, error: submissionsError } = await submissionsQuery;
|
||||||
@@ -341,7 +343,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
|
|
||||||
// Log the actual data returned to verify sort order
|
// Log the actual data returned to verify sort order
|
||||||
if (submissions && submissions.length > 0) {
|
if (submissions && submissions.length > 0) {
|
||||||
const sortField = sort.debouncedConfig.field;
|
const sortField = sortRef.current.field;
|
||||||
const preview = submissions.slice(0, 3).map(s => ({
|
const preview = submissions.slice(0, 3).map(s => ({
|
||||||
id: s.id.substring(0, 8),
|
id: s.id.substring(0, 8),
|
||||||
[sortField]: s[sortField],
|
[sortField]: s[sortField],
|
||||||
@@ -426,7 +428,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
|
|
||||||
if (silent) {
|
if (silent) {
|
||||||
// Background polling: detect new submissions
|
// Background polling: detect new submissions
|
||||||
const currentDisplayedIds = new Set(itemsRef.current.map((item) => item.id));
|
const currentDisplayedIds = new Set(items.map((item) => item.id));
|
||||||
const newSubmissions = moderationItems.filter((item) => !currentDisplayedIds.has(item.id));
|
const newSubmissions = moderationItems.filter((item) => !currentDisplayedIds.has(item.id));
|
||||||
|
|
||||||
if (newSubmissions.length > 0) {
|
if (newSubmissions.length > 0) {
|
||||||
@@ -452,7 +454,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
|
|
||||||
case "merge":
|
case "merge":
|
||||||
if (newSubmissions.length > 0) {
|
if (newSubmissions.length > 0) {
|
||||||
const currentIds = new Set(itemsRef.current.map((item) => item.id));
|
const currentIds = new Set(items.map((item) => item.id));
|
||||||
const trulyNewSubmissions = newSubmissions.filter((item) => !currentIds.has(item.id));
|
const trulyNewSubmissions = newSubmissions.filter((item) => !currentIds.has(item.id));
|
||||||
|
|
||||||
if (trulyNewSubmissions.length > 0) {
|
if (trulyNewSubmissions.length > 0) {
|
||||||
@@ -463,7 +465,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "replace":
|
case "replace":
|
||||||
const mergeResult = smartMergeArray(itemsRef.current, moderationItems, {
|
const mergeResult = smartMergeArray(items, moderationItems, {
|
||||||
compareFields: [
|
compareFields: [
|
||||||
"status",
|
"status",
|
||||||
"content",
|
"content",
|
||||||
@@ -527,11 +529,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Store fetchItems in ref to avoid re-creating visibility listener
|
|
||||||
useEffect(() => {
|
|
||||||
fetchItemsRef.current = fetchItems;
|
|
||||||
}, [fetchItems]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show pending new items by merging them into the queue
|
* Show pending new items by merging them into the queue
|
||||||
*/
|
*/
|
||||||
@@ -865,21 +862,23 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
});
|
});
|
||||||
|
|
||||||
pagination.reset();
|
pagination.reset();
|
||||||
fetchItemsRef.current?.(false);
|
fetchItems(false);
|
||||||
}, [
|
}, [
|
||||||
filters.debouncedEntityFilter,
|
filters.debouncedEntityFilter,
|
||||||
filters.debouncedStatusFilter,
|
filters.debouncedStatusFilter,
|
||||||
sort.debouncedConfig.field,
|
sort.debouncedConfig.field,
|
||||||
sort.debouncedConfig.direction,
|
sort.debouncedConfig.direction,
|
||||||
user
|
user,
|
||||||
|
fetchItems,
|
||||||
|
pagination
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Pagination changes trigger refetch
|
// Pagination changes trigger refetch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user || !initialFetchCompleteRef.current || pagination.currentPage === 1) return;
|
if (!user || !initialFetchCompleteRef.current || pagination.currentPage === 1) return;
|
||||||
|
|
||||||
fetchItemsRef.current?.(true);
|
fetchItems(true);
|
||||||
}, [pagination.currentPage, pagination.pageSize]);
|
}, [pagination.currentPage, pagination.pageSize, user, fetchItems]);
|
||||||
|
|
||||||
// Polling effect (when realtime disabled)
|
// Polling effect (when realtime disabled)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -890,14 +889,14 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
console.log("⚠️ Polling ENABLED - interval:", settings.pollInterval);
|
console.log("⚠️ Polling ENABLED - interval:", settings.pollInterval);
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
console.log("🔄 Polling refresh triggered");
|
console.log("🔄 Polling refresh triggered");
|
||||||
fetchItemsRef.current?.(true);
|
fetchItems(true);
|
||||||
}, settings.pollInterval);
|
}, settings.pollInterval);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
console.log("🛑 Polling stopped");
|
console.log("🛑 Polling stopped");
|
||||||
};
|
};
|
||||||
}, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue]);
|
}, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue, fetchItems]);
|
||||||
|
|
||||||
// Initialize realtime subscriptions
|
// Initialize realtime subscriptions
|
||||||
useRealtimeSubscriptions({
|
useRealtimeSubscriptions({
|
||||||
@@ -941,7 +940,6 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
|
|||||||
profileCache,
|
profileCache,
|
||||||
recentlyRemovedIds: recentlyRemovedRef.current,
|
recentlyRemovedIds: recentlyRemovedRef.current,
|
||||||
interactingWithIds: interactingWith,
|
interactingWithIds: interactingWith,
|
||||||
currentItemsRef: itemsRef,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export interface RealtimeSubscriptionConfig {
|
|||||||
/** Set of IDs currently being interacted with */
|
/** Set of IDs currently being interacted with */
|
||||||
interactingWithIds: Set<string>;
|
interactingWithIds: Set<string>;
|
||||||
|
|
||||||
/** Current items in queue (for comparison) - using ref to avoid reconnections */
|
/** Current items in queue (for comparison) - using ref to avoid reconnections (optional) */
|
||||||
currentItemsRef: React.MutableRefObject<ModerationItem[]>;
|
currentItemsRef?: React.MutableRefObject<ModerationItem[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||||
import { validateEntityData } from "./validation.ts";
|
import { validateEntityData, validateEntityDataStrict } from "./validation.ts";
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
@@ -206,10 +206,28 @@ serve(async (req) => {
|
|||||||
try {
|
try {
|
||||||
console.log(`Processing item ${item.id} of type ${item.item_type}`);
|
console.log(`Processing item ${item.id} of type ${item.item_type}`);
|
||||||
|
|
||||||
// Validate entity data before processing
|
// Validate entity data with strict validation
|
||||||
const validation = validateEntityData(item.item_type, item.item_data);
|
const validation = validateEntityDataStrict(item.item_type, item.item_data);
|
||||||
if (!validation.valid) {
|
|
||||||
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
if (validation.blockingErrors.length > 0) {
|
||||||
|
console.error(`❌ Blocking errors for item ${item.id}:`, validation.blockingErrors);
|
||||||
|
|
||||||
|
// Fail the entire batch if ANY item has blocking errors
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
message: 'Validation failed: Items have blocking errors that must be fixed',
|
||||||
|
errors: validation.blockingErrors,
|
||||||
|
failedItemId: item.id,
|
||||||
|
failedItemType: item.item_type
|
||||||
|
}), {
|
||||||
|
status: 400,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validation.warnings.length > 0) {
|
||||||
|
console.warn(`⚠️ Warnings for item ${item.id}:`, validation.warnings);
|
||||||
|
// Continue processing - warnings don't block approval
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set user context for versioning trigger
|
// Set user context for versioning trigger
|
||||||
|
|||||||
@@ -8,8 +8,165 @@ export interface ValidationResult {
|
|||||||
errors: string[];
|
errors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StrictValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
blockingErrors: string[];
|
||||||
|
warnings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate entity data before database write
|
* Strict validation that separates blocking errors from warnings
|
||||||
|
* Used by the approval flow to prevent invalid data from being approved
|
||||||
|
*/
|
||||||
|
export function validateEntityDataStrict(
|
||||||
|
entityType: string,
|
||||||
|
data: any
|
||||||
|
): StrictValidationResult {
|
||||||
|
const result: StrictValidationResult = {
|
||||||
|
valid: true,
|
||||||
|
blockingErrors: [],
|
||||||
|
warnings: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Common validations (blocking)
|
||||||
|
if (!data.name?.trim()) {
|
||||||
|
result.blockingErrors.push('Name is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.slug?.trim()) {
|
||||||
|
result.blockingErrors.push('Slug is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.slug && !/^[a-z0-9-]+$/.test(data.slug)) {
|
||||||
|
result.blockingErrors.push('Slug must contain only lowercase letters, numbers, and hyphens');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.name && data.name.length > 200) {
|
||||||
|
result.blockingErrors.push('Name must be less than 200 characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.description && data.description.length > 2000) {
|
||||||
|
result.blockingErrors.push('Description must be less than 2000 characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL validation (warning)
|
||||||
|
if (data.website_url && data.website_url !== '' && !isValidUrl(data.website_url)) {
|
||||||
|
result.warnings.push('Website URL format may be invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validation (warning)
|
||||||
|
if (data.email && data.email !== '' && !isValidEmail(data.email)) {
|
||||||
|
result.warnings.push('Email format may be invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entity-specific validations
|
||||||
|
switch (entityType) {
|
||||||
|
case 'park':
|
||||||
|
if (!data.park_type) {
|
||||||
|
result.blockingErrors.push('Park type is required');
|
||||||
|
}
|
||||||
|
if (!data.status) {
|
||||||
|
result.blockingErrors.push('Status is required');
|
||||||
|
}
|
||||||
|
if (data.location_id === null || data.location_id === undefined) {
|
||||||
|
result.blockingErrors.push('Location is required for parks');
|
||||||
|
}
|
||||||
|
if (data.opening_date && data.closing_date) {
|
||||||
|
const opening = new Date(data.opening_date);
|
||||||
|
const closing = new Date(data.closing_date);
|
||||||
|
if (closing < opening) {
|
||||||
|
result.blockingErrors.push('Closing date must be after opening date');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ride':
|
||||||
|
if (!data.category) {
|
||||||
|
result.blockingErrors.push('Category is required');
|
||||||
|
}
|
||||||
|
if (!data.status) {
|
||||||
|
result.blockingErrors.push('Status is required');
|
||||||
|
}
|
||||||
|
if (data.park_id === null || data.park_id === undefined) {
|
||||||
|
result.blockingErrors.push('Park is required for rides');
|
||||||
|
}
|
||||||
|
if (data.max_speed_kmh && (data.max_speed_kmh < 0 || data.max_speed_kmh > 300)) {
|
||||||
|
result.blockingErrors.push('Max speed must be between 0 and 300 km/h');
|
||||||
|
}
|
||||||
|
if (data.max_height_meters && (data.max_height_meters < 0 || data.max_height_meters > 200)) {
|
||||||
|
result.blockingErrors.push('Max height must be between 0 and 200 meters');
|
||||||
|
}
|
||||||
|
if (data.drop_height_meters && (data.drop_height_meters < 0 || data.drop_height_meters > 200)) {
|
||||||
|
result.blockingErrors.push('Drop height must be between 0 and 200 meters');
|
||||||
|
}
|
||||||
|
if (data.height_requirement && (data.height_requirement < 0 || data.height_requirement > 300)) {
|
||||||
|
result.blockingErrors.push('Height requirement must be between 0 and 300 cm');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'manufacturer':
|
||||||
|
case 'designer':
|
||||||
|
case 'operator':
|
||||||
|
case 'property_owner':
|
||||||
|
if (!data.company_type) {
|
||||||
|
result.blockingErrors.push(`Company type is required (expected: ${entityType})`);
|
||||||
|
} else if (data.company_type !== entityType) {
|
||||||
|
result.blockingErrors.push(`Company type mismatch: expected '${entityType}' but got '${data.company_type}'`);
|
||||||
|
}
|
||||||
|
if (data.founded_year) {
|
||||||
|
const year = parseInt(data.founded_year);
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
if (year < 1800 || year > currentYear) {
|
||||||
|
result.warnings.push(`Founded year should be between 1800 and ${currentYear}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ride_model':
|
||||||
|
if (!data.category) {
|
||||||
|
result.blockingErrors.push('Category is required');
|
||||||
|
}
|
||||||
|
if (!data.ride_type) {
|
||||||
|
result.blockingErrors.push('Ride type is required');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'photo':
|
||||||
|
if (!data.cloudflare_image_id) {
|
||||||
|
result.blockingErrors.push('Image ID is required');
|
||||||
|
}
|
||||||
|
if (!data.entity_type) {
|
||||||
|
result.blockingErrors.push('Entity type is required');
|
||||||
|
}
|
||||||
|
if (!data.entity_id) {
|
||||||
|
result.blockingErrors.push('Entity ID is required');
|
||||||
|
}
|
||||||
|
if (data.caption && data.caption.length > 500) {
|
||||||
|
result.blockingErrors.push('Caption must be less than 500 characters');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.valid = result.blockingErrors.length === 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function isValidUrl(url: string): boolean {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidEmail(email: string): boolean {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate entity data before database write (legacy function)
|
||||||
*/
|
*/
|
||||||
export function validateEntityData(entityType: string, data: any): ValidationResult {
|
export function validateEntityData(entityType: string, data: any): ValidationResult {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user