Fix moderation queue issues

This commit is contained in:
gpt-engineer-app[bot]
2025-11-03 16:43:21 +00:00
parent 47b317b7c0
commit eee4b1c626
7 changed files with 220 additions and 27 deletions

View File

@@ -286,7 +286,7 @@ export const QueueItem = memo(({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src={item.user_profile.avatar_url} /> <AvatarImage src={item.user_profile.avatar_url ?? undefined} />
<AvatarFallback className="text-xs"> <AvatarFallback className="text-xs">
{(item.user_profile.display_name || item.user_profile.username)?.slice(0, 2).toUpperCase()} {(item.user_profile.display_name || item.user_profile.username)?.slice(0, 2).toUpperCase()}
</AvatarFallback> </AvatarFallback>

View File

@@ -42,7 +42,7 @@ export const QueueItemContext = memo(({ item }: QueueItemContextProps) => {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src={item.user_profile.avatar_url} /> <AvatarImage src={item.user_profile.avatar_url ?? undefined} />
<AvatarFallback className="text-xs"> <AvatarFallback className="text-xs">
{(item.user_profile.display_name || item.user_profile.username)?.slice(0, 2).toUpperCase()} {(item.user_profile.display_name || item.user_profile.username)?.slice(0, 2).toUpperCase()}
</AvatarFallback> </AvatarFallback>

View File

@@ -4798,22 +4798,26 @@ export type Database = {
} }
moderation_queue_with_entities: { moderation_queue_with_entities: {
Row: { Row: {
assigned_at: string | null
assigned_profile: Json | null assigned_profile: Json | null
assigned_to: string | null assigned_to: string | null
content: Json | null
created_at: string | null
escalated: boolean | null escalated: boolean | null
escalated_at: string | null
escalation_reason: string | null escalation_reason: string | null
id: string | null id: string | null
is_test_data: boolean | null is_test_data: boolean | null
locked_until: string | null locked_until: string | null
review_notes: string | null
reviewed_at: string | null reviewed_at: string | null
reviewed_by: string | null reviewed_by: string | null
reviewer_notes: string | null
reviewer_profile: Json | null reviewer_profile: Json | null
status: string | null status: string | null
submission_items: Json | null submission_items: Json | null
submission_type: string | null submission_type: string | null
submitted_at: string | null submitted_at: string | null
submitted_by: string | null submitter_id: string | null
submitter_profile: Json | null submitter_profile: Json | null
} }
Relationships: [ Relationships: [
@@ -4847,14 +4851,14 @@ export type Database = {
}, },
{ {
foreignKeyName: "content_submissions_user_id_fkey" foreignKeyName: "content_submissions_user_id_fkey"
columns: ["submitted_by"] columns: ["submitter_id"]
isOneToOne: false isOneToOne: false
referencedRelation: "filtered_profiles" referencedRelation: "filtered_profiles"
referencedColumns: ["user_id"] referencedColumns: ["user_id"]
}, },
{ {
foreignKeyName: "content_submissions_user_id_fkey" foreignKeyName: "content_submissions_user_id_fkey"
columns: ["submitted_by"] columns: ["submitter_id"]
isOneToOne: false isOneToOne: false
referencedRelation: "profiles" referencedRelation: "profiles"
referencedColumns: ["user_id"] referencedColumns: ["user_id"]

View File

@@ -163,15 +163,40 @@ export function buildModerationItem(
id: submission.id, id: submission.id,
type: 'content_submission', type: 'content_submission',
content: submission.content, 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, status: submission.status,
submission_type: submission.submission_type, 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, username: profile.username,
display_name: profile.display_name, display_name: profile.display_name,
avatar_url: profile.avatar_url, 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', entity_name: entityName || (submission.content as SubmissionContent)?.name || 'Unknown',
park_name: parkName, park_name: parkName,
reviewed_at: submission.reviewed_at || undefined, reviewed_at: submission.reviewed_at || undefined,

View File

@@ -8,8 +8,16 @@
import { z } from 'zod'; import { z } from 'zod';
import { logger } from '@/lib/logger'; import { logger } from '@/lib/logger';
// Profile schema // Profile schema (matches database JSONB structure)
const ProfileSchema = z.object({ 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(), username: z.string(),
display_name: z.string().optional().nullable(), display_name: z.string().optional().nullable(),
avatar_url: 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']), status: z.enum(['pending', 'approved', 'rejected', 'partially_approved', 'flagged']),
type: z.string(), type: z.string(),
submission_type: z.string(), submission_type: z.string(),
// Accept both created_at and submitted_at for flexibility
created_at: z.string(), created_at: z.string(),
submitted_at: z.string().optional(),
updated_at: z.string().optional().nullable(), updated_at: z.string().optional().nullable(),
reviewed_at: z.string().optional().nullable(),
content: z.record(z.string(), z.any()), 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(), assigned_to: z.string().uuid().optional().nullable(),
locked_until: z.string().optional().nullable(), locked_until: z.string().optional().nullable(),
reviewed_at: z.string().optional().nullable(),
reviewed_by: z.string().uuid().optional().nullable(), reviewed_by: z.string().uuid().optional().nullable(),
reviewer_notes: z.string().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(), submitter_profile: ProfileSchema.optional().nullable(),
assigned_profile: ProfileSchema.optional().nullable(), assigned_profile: ProfileSchema.optional().nullable(),
reviewer_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); export const ModerationItemArraySchema = z.array(ModerationItemSchema);

View File

@@ -161,10 +161,10 @@ export interface ModerationItem {
/** Raw content data (structure varies by type) */ /** Raw content data (structure varies by type) */
content: any; content: any;
/** Timestamp when the item was created */ /** Timestamp when the item was created (primary field) */
created_at: string; created_at: string;
/** Timestamp when the item was submitted */ /** Timestamp when the item was submitted (same as created_at) */
submitted_at?: string; submitted_at?: string;
/** Timestamp when the item was last updated */ /** Timestamp when the item was last updated */
@@ -173,13 +173,40 @@ export interface ModerationItem {
/** ID of the user who submitted this item */ /** ID of the user who submitted this item */
user_id: string; user_id: string;
/** ID of the submitter (from view, same as user_id) */
submitter_id?: string;
/** Current status of the item */ /** Current status of the item */
status: string; status: string;
/** Type of submission (e.g., 'park', 'ride', 'review') */ /** Type of submission (e.g., 'park', 'ride', 'review') */
submission_type?: string; 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?: { submitter?: {
user_id: string; user_id: string;
username: string; username: string;
@@ -187,7 +214,7 @@ export interface ModerationItem {
avatar_url?: string; avatar_url?: string;
}; };
/** Pre-loaded reviewer profile from view */ /** Legacy: Reviewer (old field name for backward compatibility) */
reviewer?: { reviewer?: {
user_id: string; user_id: string;
username: string; username: string;
@@ -198,8 +225,8 @@ export interface ModerationItem {
/** Legacy: Profile information of the submitting user */ /** Legacy: Profile information of the submitting user */
user_profile?: { user_profile?: {
username: string; username: string;
display_name?: string; display_name?: string | null;
avatar_url?: string; avatar_url?: string | null;
}; };
/** Display name of the entity being modified */ /** Display name of the entity being modified */
@@ -220,13 +247,6 @@ export interface ModerationItem {
/** Notes left by the reviewing moderator */ /** Notes left by the reviewing moderator */
reviewer_notes?: string; 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 */ /** Whether this submission has been escalated for senior review */
escalated?: boolean; escalated?: boolean;

View File

@@ -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;