mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:11:12 -05:00
Fix moderation queue issues
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user