From eee4b1c6268572c17af0d2de8efa7d67d759ccae Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:43:21 +0000 Subject: [PATCH] Fix moderation queue issues --- src/components/moderation/QueueItem.tsx | 2 +- .../moderation/renderers/QueueItemContext.tsx | 2 +- src/integrations/supabase/types.ts | 12 +- src/lib/moderation/realtime.ts | 33 ++++- src/lib/moderation/validation.ts | 39 +++++- src/types/moderation.ts | 46 +++++-- ...7_be89f25b-b4cc-4566-9a68-c027a03d5933.sql | 113 ++++++++++++++++++ 7 files changed, 220 insertions(+), 27 deletions(-) create mode 100644 supabase/migrations/20251103164127_be89f25b-b4cc-4566-9a68-c027a03d5933.sql diff --git a/src/components/moderation/QueueItem.tsx b/src/components/moderation/QueueItem.tsx index 49ea041f..92513db3 100644 --- a/src/components/moderation/QueueItem.tsx +++ b/src/components/moderation/QueueItem.tsx @@ -286,7 +286,7 @@ export const QueueItem = memo(({
- + {(item.user_profile.display_name || item.user_profile.username)?.slice(0, 2).toUpperCase()} diff --git a/src/components/moderation/renderers/QueueItemContext.tsx b/src/components/moderation/renderers/QueueItemContext.tsx index 05db323f..0288e0ea 100644 --- a/src/components/moderation/renderers/QueueItemContext.tsx +++ b/src/components/moderation/renderers/QueueItemContext.tsx @@ -42,7 +42,7 @@ export const QueueItemContext = memo(({ item }: QueueItemContextProps) => {
- + {(item.user_profile.display_name || item.user_profile.username)?.slice(0, 2).toUpperCase()} diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index c43304b2..7eb2222e 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -4798,22 +4798,26 @@ export type Database = { } moderation_queue_with_entities: { Row: { + assigned_at: string | null assigned_profile: Json | null assigned_to: string | null + content: Json | null + created_at: string | null escalated: boolean | null + escalated_at: string | null escalation_reason: string | null id: string | null is_test_data: boolean | null locked_until: string | null - review_notes: string | null reviewed_at: string | null reviewed_by: string | null + reviewer_notes: string | null reviewer_profile: Json | null status: string | null submission_items: Json | null submission_type: string | null submitted_at: string | null - submitted_by: string | null + submitter_id: string | null submitter_profile: Json | null } Relationships: [ @@ -4847,14 +4851,14 @@ export type Database = { }, { foreignKeyName: "content_submissions_user_id_fkey" - columns: ["submitted_by"] + columns: ["submitter_id"] isOneToOne: false referencedRelation: "filtered_profiles" referencedColumns: ["user_id"] }, { foreignKeyName: "content_submissions_user_id_fkey" - columns: ["submitted_by"] + columns: ["submitter_id"] isOneToOne: false referencedRelation: "profiles" referencedColumns: ["user_id"] diff --git a/src/lib/moderation/realtime.ts b/src/lib/moderation/realtime.ts index d766dc99..3df8626b 100644 --- a/src/lib/moderation/realtime.ts +++ b/src/lib/moderation/realtime.ts @@ -163,15 +163,40 @@ export function buildModerationItem( id: submission.id, type: 'content_submission', content: submission.content, - created_at: submission.created_at, - user_id: submission.user_id, + + // Handle both created_at (from view) and submitted_at (from realtime) + created_at: submission.created_at || submission.submitted_at, + submitted_at: submission.submitted_at, + + // Support both user_id and submitter_id + user_id: submission.user_id || submission.submitter_id, + submitter_id: submission.submitter_id || submission.user_id, + status: submission.status, submission_type: submission.submission_type, - user_profile: profile ? { + + // Use new profile structure from view if available + submitter_profile: submission.submitter_profile || (profile ? { + user_id: submission.user_id || submission.submitter_id, username: profile.username, display_name: profile.display_name, avatar_url: profile.avatar_url, - } : undefined, + } : undefined), + + reviewer_profile: submission.reviewer_profile, + assigned_profile: submission.assigned_profile, + + // Legacy support: create user_profile from submitter_profile + user_profile: submission.submitter_profile ? { + username: submission.submitter_profile.username, + display_name: submission.submitter_profile.display_name, + avatar_url: submission.submitter_profile.avatar_url, + } : (profile ? { + username: profile.username, + display_name: profile.display_name, + avatar_url: profile.avatar_url, + } : undefined), + entity_name: entityName || (submission.content as SubmissionContent)?.name || 'Unknown', park_name: parkName, reviewed_at: submission.reviewed_at || undefined, diff --git a/src/lib/moderation/validation.ts b/src/lib/moderation/validation.ts index 85904018..9d076dfe 100644 --- a/src/lib/moderation/validation.ts +++ b/src/lib/moderation/validation.ts @@ -8,8 +8,16 @@ import { z } from 'zod'; import { logger } from '@/lib/logger'; -// Profile schema +// Profile schema (matches database JSONB structure) const ProfileSchema = z.object({ + user_id: z.string().uuid(), + username: z.string(), + display_name: z.string().optional().nullable(), + avatar_url: z.string().optional().nullable(), +}); + +// Legacy profile schema (for backward compatibility) +const LegacyProfileSchema = z.object({ username: z.string(), display_name: z.string().optional().nullable(), avatar_url: z.string().optional().nullable(), @@ -31,19 +39,42 @@ export const ModerationItemSchema = z.object({ status: z.enum(['pending', 'approved', 'rejected', 'partially_approved', 'flagged']), type: z.string(), submission_type: z.string(), + + // Accept both created_at and submitted_at for flexibility created_at: z.string(), + submitted_at: z.string().optional(), updated_at: z.string().optional().nullable(), + reviewed_at: z.string().optional().nullable(), + content: z.record(z.string(), z.any()), - submitter_id: z.string().uuid(), + + // User fields (support both old and new naming) + submitter_id: z.string().uuid().optional(), + user_id: z.string().uuid().optional(), + assigned_to: z.string().uuid().optional().nullable(), locked_until: z.string().optional().nullable(), - reviewed_at: z.string().optional().nullable(), reviewed_by: z.string().uuid().optional().nullable(), reviewer_notes: z.string().optional().nullable(), - submission_items: z.array(SubmissionItemSchema).optional(), + + // Escalation fields + escalated: z.boolean().optional().default(false), + escalation_reason: z.string().optional().nullable(), + + // Profile objects (new structure from view) submitter_profile: ProfileSchema.optional().nullable(), assigned_profile: ProfileSchema.optional().nullable(), reviewer_profile: ProfileSchema.optional().nullable(), + + // Legacy profile support + user_profile: LegacyProfileSchema.optional().nullable(), + + // Submission items + submission_items: z.array(SubmissionItemSchema).optional(), + + // Entity names + entity_name: z.string().optional(), + park_name: z.string().optional(), }); export const ModerationItemArraySchema = z.array(ModerationItemSchema); diff --git a/src/types/moderation.ts b/src/types/moderation.ts index d27ee20f..629133fc 100644 --- a/src/types/moderation.ts +++ b/src/types/moderation.ts @@ -161,10 +161,10 @@ export interface ModerationItem { /** Raw content data (structure varies by type) */ content: any; - /** Timestamp when the item was created */ + /** Timestamp when the item was created (primary field) */ created_at: string; - /** Timestamp when the item was submitted */ + /** Timestamp when the item was submitted (same as created_at) */ submitted_at?: string; /** Timestamp when the item was last updated */ @@ -173,13 +173,40 @@ export interface ModerationItem { /** ID of the user who submitted this item */ user_id: string; + /** ID of the submitter (from view, same as user_id) */ + submitter_id?: string; + /** Current status of the item */ status: string; /** Type of submission (e.g., 'park', 'ride', 'review') */ submission_type?: string; - /** Pre-loaded submitter profile from view */ + /** Pre-loaded submitter profile from view (new structure) */ + submitter_profile?: { + user_id: string; + username: string; + display_name?: string | null; + avatar_url?: string | null; + }; + + /** Pre-loaded reviewer profile from view (new structure) */ + reviewer_profile?: { + user_id: string; + username: string; + display_name?: string | null; + avatar_url?: string | null; + }; + + /** Pre-loaded assigned moderator profile from view */ + assigned_profile?: { + user_id: string; + username: string; + display_name?: string | null; + avatar_url?: string | null; + }; + + /** Legacy: Submitter (old field name for backward compatibility) */ submitter?: { user_id: string; username: string; @@ -187,7 +214,7 @@ export interface ModerationItem { avatar_url?: string; }; - /** Pre-loaded reviewer profile from view */ + /** Legacy: Reviewer (old field name for backward compatibility) */ reviewer?: { user_id: string; username: string; @@ -198,8 +225,8 @@ export interface ModerationItem { /** Legacy: Profile information of the submitting user */ user_profile?: { username: string; - display_name?: string; - avatar_url?: string; + display_name?: string | null; + avatar_url?: string | null; }; /** Display name of the entity being modified */ @@ -220,13 +247,6 @@ export interface ModerationItem { /** Notes left by the reviewing moderator */ reviewer_notes?: string; - /** Legacy: Profile information of the reviewing moderator */ - reviewer_profile?: { - username: string; - display_name?: string; - avatar_url?: string; - }; - /** Whether this submission has been escalated for senior review */ escalated?: boolean; diff --git a/supabase/migrations/20251103164127_be89f25b-b4cc-4566-9a68-c027a03d5933.sql b/supabase/migrations/20251103164127_be89f25b-b4cc-4566-9a68-c027a03d5933.sql new file mode 100644 index 00000000..49a1a783 --- /dev/null +++ b/supabase/migrations/20251103164127_be89f25b-b4cc-4566-9a68-c027a03d5933.sql @@ -0,0 +1,113 @@ +-- Phase 1: Fix moderation_queue_with_entities view with proper column aliases and structure + +-- Drop existing view +DROP VIEW IF EXISTS moderation_queue_with_entities CASCADE; + +-- Create corrected view with proper aliases and structure +CREATE VIEW moderation_queue_with_entities AS +SELECT + cs.id, + cs.submission_type, + cs.status, + + -- Temporal fields (with backward compatibility alias) + cs.submitted_at AS created_at, -- Primary alias for frontend + cs.submitted_at, -- Also expose for semantic accuracy + cs.reviewed_at, + cs.assigned_at, + cs.escalated_at, + + -- User relationships + cs.user_id as submitter_id, + cs.reviewer_id as reviewed_by, + cs.assigned_to, + cs.locked_until, + + -- Flags and metadata + cs.escalated, + cs.escalation_reason, + cs.reviewer_notes, + cs.is_test_data, + cs.content, + + -- Submitter profile (matches frontend expectations) + CASE + WHEN sp.id IS NOT NULL THEN + jsonb_build_object( + 'user_id', sp.user_id, + 'username', sp.username, + 'display_name', sp.display_name, + 'avatar_url', sp.avatar_url + ) + ELSE NULL + END as submitter_profile, + + -- Reviewer profile + CASE + WHEN rp.id IS NOT NULL THEN + jsonb_build_object( + 'user_id', rp.user_id, + 'username', rp.username, + 'display_name', rp.display_name, + 'avatar_url', rp.avatar_url + ) + ELSE NULL + END as reviewer_profile, + + -- Assigned moderator profile + CASE + WHEN ap.id IS NOT NULL THEN + jsonb_build_object( + 'user_id', ap.user_id, + 'username', ap.username, + 'display_name', ap.display_name, + 'avatar_url', ap.avatar_url + ) + ELSE NULL + END as assigned_profile, + + -- Submission items with entity data + ( + SELECT jsonb_agg( + jsonb_build_object( + 'id', si.id, + 'submission_id', si.submission_id, + 'item_type', si.item_type, + 'item_data_id', si.item_data_id, + 'action_type', si.action_type, + 'status', si.status, + 'order_index', si.order_index, + 'depends_on', si.depends_on, + 'approved_entity_id', si.approved_entity_id, + 'rejection_reason', si.rejection_reason, + 'created_at', si.created_at, + 'updated_at', si.updated_at, + 'entity_data', get_submission_item_entity_data(si.item_type, si.item_data_id) + ) + ORDER BY si.order_index + ) + FROM submission_items si + WHERE si.submission_id = cs.id + ) as submission_items + +FROM content_submissions cs +LEFT JOIN profiles sp ON sp.user_id = cs.user_id +LEFT JOIN profiles rp ON rp.user_id = cs.reviewer_id +LEFT JOIN profiles ap ON ap.user_id = cs.assigned_to; + +-- Add comment for documentation +COMMENT ON VIEW moderation_queue_with_entities IS +'Optimized view for moderation queue with pre-joined profiles and entity data. Exposes both created_at (alias) and submitted_at for backward compatibility.'; + +-- Performance indexes (if not already created) +CREATE INDEX IF NOT EXISTS idx_content_submissions_queue + ON content_submissions(status, escalated DESC, submitted_at ASC) + WHERE status IN ('pending', 'flagged', 'partially_approved', 'approved', 'rejected'); + +CREATE INDEX IF NOT EXISTS idx_content_submissions_locks + ON content_submissions(assigned_to, locked_until) + WHERE assigned_to IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_submission_items_item_data_id + ON submission_items(item_type, item_data_id) + WHERE item_data_id IS NOT NULL; \ No newline at end of file