mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 17:11:13 -05:00
feat: Implement enhanced moderation system
This commit is contained in:
299
src/lib/submissionItemsService.ts
Normal file
299
src/lib/submissionItemsService.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user