feat: Implement enhanced moderation system

This commit is contained in:
gpt-engineer-app[bot]
2025-09-30 13:41:19 +00:00
parent 04c5ef58ff
commit 083a4af08c
8 changed files with 1140 additions and 2 deletions

View File

@@ -0,0 +1,299 @@
import { supabase } from '@/integrations/supabase/client';
export interface SubmissionItemWithDeps {
id: string;
submission_id: string;
item_type: string;
item_data: any;
original_data: any;
status: 'pending' | 'approved' | 'rejected';
depends_on: string | null;
order_index: number;
approved_entity_id: string | null;
rejection_reason: string | null;
created_at: string;
updated_at: string;
dependencies?: SubmissionItemWithDeps[];
dependents?: SubmissionItemWithDeps[];
}
export interface DependencyConflict {
itemId: string;
type: 'missing_parent' | 'rejected_parent' | 'circular_dependency';
message: string;
suggestions: Array<{
action: 'link_existing' | 'cascade_reject' | 'escalate' | 'create_parent';
label: string;
entityId?: string;
}>;
}
/**
* Fetch all items for a submission with their dependencies
*/
export async function fetchSubmissionItems(submissionId: string): Promise<SubmissionItemWithDeps[]> {
const { data, error } = await supabase
.from('submission_items')
.select('*')
.eq('submission_id', submissionId)
.order('order_index', { ascending: true });
if (error) throw error;
// Cast the data to the correct type
return (data || []).map(item => ({
...item,
status: item.status as 'pending' | 'approved' | 'rejected',
})) as SubmissionItemWithDeps[];
}
/**
* Build dependency tree for submission items
*/
export function buildDependencyTree(items: SubmissionItemWithDeps[]): SubmissionItemWithDeps[] {
const itemMap = new Map(items.map(item => [item.id, { ...item, dependencies: [], dependents: [] }]));
// Build relationships
items.forEach(item => {
if (item.depends_on) {
const parent = itemMap.get(item.depends_on);
const child = itemMap.get(item.id);
if (parent && child) {
parent.dependents = parent.dependents || [];
parent.dependents.push(child);
child.dependencies = child.dependencies || [];
child.dependencies.push(parent);
}
}
});
return Array.from(itemMap.values());
}
/**
* Detect dependency conflicts for selective approval
*/
export async function detectDependencyConflicts(
items: SubmissionItemWithDeps[],
selectedItemIds: string[]
): Promise<DependencyConflict[]> {
const conflicts: DependencyConflict[] = [];
const selectedSet = new Set(selectedItemIds);
for (const item of items) {
// Check if parent is rejected but child is selected
if (item.depends_on && selectedSet.has(item.id)) {
const parent = items.find(i => i.id === item.depends_on);
if (parent && (parent.status === 'rejected' || !selectedSet.has(parent.id))) {
// Find existing entities that could be linked
const suggestions: DependencyConflict['suggestions'] = [];
// Suggest creating parent
if (parent.status !== 'rejected') {
suggestions.push({
action: 'create_parent',
label: `Also approve ${parent.item_type}: ${parent.item_data.name}`,
});
}
// Suggest linking to existing entity
if (parent.item_type === 'park') {
const { data: parks } = await supabase
.from('parks')
.select('id, name')
.ilike('name', `%${parent.item_data.name}%`)
.limit(3);
parks?.forEach(park => {
suggestions.push({
action: 'link_existing',
label: `Link to existing park: ${park.name}`,
entityId: park.id,
});
});
}
suggestions.push({
action: 'escalate',
label: 'Escalate to admin for resolution',
});
conflicts.push({
itemId: item.id,
type: 'missing_parent',
message: `Cannot approve ${item.item_type} without its parent ${parent.item_type}`,
suggestions,
});
}
}
}
return conflicts;
}
/**
* Update individual submission item status
*/
export async function updateSubmissionItem(
itemId: string,
updates: Partial<SubmissionItemWithDeps>
): Promise<void> {
const { error } = await supabase
.from('submission_items')
.update(updates)
.eq('id', itemId);
if (error) throw error;
}
/**
* Approve multiple items with dependency handling
*/
export async function approveSubmissionItems(
items: SubmissionItemWithDeps[],
userId: string
): Promise<void> {
// Sort by dependency order (parents first)
const sortedItems = topologicalSort(items);
for (const item of sortedItems) {
let entityId: string | null = null;
// Create the entity based on type
switch (item.item_type) {
case 'park':
entityId = await createPark(item.item_data);
break;
case 'ride':
entityId = await createRide(item.item_data);
break;
case 'manufacturer':
case 'operator':
case 'property_owner':
case 'designer':
entityId = await createCompany(item.item_data, item.item_type);
break;
case 'ride_model':
entityId = await createRideModel(item.item_data);
break;
case 'photo':
entityId = await approvePhotos(item.item_data);
break;
}
// Update item status
await updateSubmissionItem(item.id, {
status: 'approved',
approved_entity_id: entityId,
});
}
}
/**
* Topological sort for dependency-ordered processing
*/
function topologicalSort(items: SubmissionItemWithDeps[]): SubmissionItemWithDeps[] {
const sorted: SubmissionItemWithDeps[] = [];
const visited = new Set<string>();
const temp = new Set<string>();
function visit(item: SubmissionItemWithDeps) {
if (temp.has(item.id)) {
throw new Error('Circular dependency detected');
}
if (visited.has(item.id)) return;
temp.add(item.id);
if (item.dependencies) {
item.dependencies.forEach(dep => visit(dep));
}
temp.delete(item.id);
visited.add(item.id);
sorted.push(item);
}
items.forEach(item => {
if (!visited.has(item.id)) {
visit(item);
}
});
return sorted;
}
/**
* Helper functions to create entities
*/
async function createPark(data: any): Promise<string> {
const { data: park, error } = await supabase
.from('parks')
.insert(data)
.select('id')
.single();
if (error) throw error;
return park.id;
}
async function createRide(data: any): Promise<string> {
const { data: ride, error } = await supabase
.from('rides')
.insert(data)
.select('id')
.single();
if (error) throw error;
return ride.id;
}
async function createCompany(data: any, companyType: string): Promise<string> {
const { data: company, error } = await supabase
.from('companies')
.insert({ ...data, company_type: companyType })
.select('id')
.single();
if (error) throw error;
return company.id;
}
async function createRideModel(data: any): Promise<string> {
const { data: model, error } = await supabase
.from('ride_models')
.insert(data)
.select('id')
.single();
if (error) throw error;
return model.id;
}
async function approvePhotos(data: any): Promise<string> {
// Photos are already uploaded to Cloudflare
// Just need to associate them with the entity
return data.photos?.[0]?.url || '';
}
/**
* Escalate submission for admin review
*/
export async function escalateSubmission(
submissionId: string,
reason: string,
userId: string
): Promise<void> {
const { error } = await supabase
.from('content_submissions')
.update({
status: 'pending',
escalation_reason: reason,
escalated_by: userId,
reviewer_notes: `Escalated: ${reason}`,
})
.eq('id', submissionId);
if (error) throw error;
}