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