mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 04:31:13 -05:00
Refactor: Approve tool use
This commit is contained in:
@@ -128,39 +128,61 @@ interface PhotoDeletionPreviewProps {
|
||||
export function PhotoDeletionPreview({ photo, compact = false }: PhotoDeletionPreviewProps) {
|
||||
if (compact) {
|
||||
return (
|
||||
<Badge variant="outline" className="text-red-600 dark:text-red-400">
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
Delete Photo
|
||||
</Badge>
|
||||
<div className="flex items-center gap-2 p-2 rounded-md bg-destructive/10 border border-destructive/20">
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
<span className="text-sm font-medium text-destructive">Delete Photo</span>
|
||||
{photo.deletion_reason && (
|
||||
<span className="text-xs text-muted-foreground">- {photo.deletion_reason}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-3 rounded-md bg-destructive/10">
|
||||
<div className="text-sm font-medium text-red-600 dark:text-red-400">
|
||||
<Trash2 className="h-4 w-4 inline mr-1" />
|
||||
Deleting Photo
|
||||
<div className="flex flex-col gap-3 p-4 rounded-lg bg-destructive/10 border-2 border-destructive/30">
|
||||
<div className="flex items-center gap-2 text-destructive font-semibold">
|
||||
<Trash2 className="h-5 w-5" />
|
||||
<span>Photo Deletion Request</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<img
|
||||
src={photo.url}
|
||||
alt={photo.title || photo.caption || 'Photo to be deleted'}
|
||||
className="w-32 h-32 object-cover rounded opacity-75"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
{photo.url && (
|
||||
<img
|
||||
src={photo.url}
|
||||
alt={photo.title || photo.caption || 'Photo to be deleted'}
|
||||
className="w-48 h-48 object-cover rounded-lg border-2 border-destructive/40 shadow-lg"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 text-sm space-y-2">
|
||||
{photo.title && <div className="font-medium">{photo.title}</div>}
|
||||
{photo.caption && <div className="text-muted-foreground">{photo.caption}</div>}
|
||||
{photo.entity_type && photo.entity_name && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
From: <span className="capitalize">{photo.entity_type}</span> - {photo.entity_name}
|
||||
<div className="flex-1 space-y-3">
|
||||
{photo.title && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-muted-foreground">Title:</span>
|
||||
<div className="font-medium">{photo.title}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{photo.caption && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-muted-foreground">Caption:</span>
|
||||
<div className="text-sm">{photo.caption}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{photo.entity_type && photo.entity_name && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-muted-foreground">From Entity:</span>
|
||||
<div className="text-sm">
|
||||
<span className="capitalize">{photo.entity_type.replace('_', ' ')}</span> - <span className="font-medium">{photo.entity_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{photo.deletion_reason && (
|
||||
<div className="text-xs p-2 bg-destructive/5 rounded border border-destructive/20">
|
||||
<span className="font-medium">Reason:</span> {photo.deletion_reason}
|
||||
<div className="p-3 bg-destructive/20 rounded-md border border-destructive/30">
|
||||
<span className="text-xs font-bold text-destructive uppercase">Deletion Reason:</span>
|
||||
<p className="mt-1 text-sm font-medium">{photo.deletion_reason}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -85,6 +85,33 @@ export function SubmissionChangesDisplay({
|
||||
);
|
||||
|
||||
if (view === 'summary') {
|
||||
// Special compact display for photo deletions
|
||||
if (item.item_type === 'photo_delete') {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{getEntityIcon()}
|
||||
<span className="font-medium">{changes.entityName}</span>
|
||||
{getActionBadge()}
|
||||
</div>
|
||||
|
||||
{changes.photoChanges.length > 0 && changes.photoChanges[0].type === 'deleted' && (
|
||||
<PhotoDeletionPreview
|
||||
photo={{
|
||||
url: changes.photoChanges[0].photo?.url || item.item_data?.cloudflare_image_url || '',
|
||||
title: changes.photoChanges[0].photo?.title || item.item_data?.title,
|
||||
caption: changes.photoChanges[0].photo?.caption || item.item_data?.caption,
|
||||
entity_type: item.item_data?.entity_type,
|
||||
entity_name: changes.entityName,
|
||||
deletion_reason: changes.photoChanges[0].photo?.deletion_reason || item.item_data?.deletion_reason || item.item_data?.reason
|
||||
}}
|
||||
compact={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -146,7 +173,34 @@ export function SubmissionChangesDisplay({
|
||||
);
|
||||
}
|
||||
|
||||
// Detailed view
|
||||
// Detailed view - special handling for photo deletions
|
||||
if (item.item_type === 'photo_delete') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{getEntityIcon()}
|
||||
<h3 className="text-lg font-semibold">{changes.entityName}</h3>
|
||||
{getActionBadge()}
|
||||
</div>
|
||||
|
||||
{changes.photoChanges.length > 0 && changes.photoChanges[0].type === 'deleted' && (
|
||||
<PhotoDeletionPreview
|
||||
photo={{
|
||||
url: changes.photoChanges[0].photo?.url || item.item_data?.cloudflare_image_url || '',
|
||||
title: changes.photoChanges[0].photo?.title || item.item_data?.title,
|
||||
caption: changes.photoChanges[0].photo?.caption || item.item_data?.caption,
|
||||
entity_type: item.item_data?.entity_type,
|
||||
entity_name: changes.entityName,
|
||||
deletion_reason: changes.photoChanges[0].photo?.deletion_reason || item.item_data?.deletion_reason || item.item_data?.reason
|
||||
}}
|
||||
compact={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Detailed view for other items
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -136,10 +136,13 @@ export async function detectChanges(
|
||||
const itemData = item.item_data || {};
|
||||
const originalData = item.original_data || {};
|
||||
|
||||
// Determine action type
|
||||
const action: 'create' | 'edit' | 'delete' =
|
||||
!originalData || Object.keys(originalData).length === 0 ? 'create' :
|
||||
itemData.deleted ? 'delete' : 'edit';
|
||||
// Determine action type - special handling for photo_delete
|
||||
let action: 'create' | 'edit' | 'delete' = 'edit';
|
||||
if (item.item_type === 'photo_delete' || itemData.action === 'delete' || itemData.deleted) {
|
||||
action = 'delete';
|
||||
} else if (!originalData || Object.keys(originalData).length === 0) {
|
||||
action = 'create';
|
||||
}
|
||||
|
||||
const fieldChanges: FieldChange[] = [];
|
||||
const imageChanges: ImageChange[] = [];
|
||||
@@ -269,22 +272,32 @@ export async function detectChanges(
|
||||
const entityId = itemData.entity_id;
|
||||
|
||||
if (entityType === 'park') {
|
||||
const { data } = await supabase.from('parks').select('name').eq('id', entityId).single();
|
||||
const { data } = await supabase.from('parks').select('name').eq('id', entityId).maybeSingle();
|
||||
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
|
||||
} else if (entityType === 'ride') {
|
||||
const { data } = await supabase.from('rides').select('name').eq('id', entityId).single();
|
||||
const { data } = await supabase.from('rides').select('name').eq('id', entityId).maybeSingle();
|
||||
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
|
||||
} else if (entityType === 'ride_model') {
|
||||
const { data } = await supabase.from('ride_models').select('name').eq('id', entityId).single();
|
||||
const { data } = await supabase.from('ride_models').select('name').eq('id', entityId).maybeSingle();
|
||||
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
|
||||
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(entityType)) {
|
||||
const { data } = await supabase.from('companies').select('name').eq('id', entityId).single();
|
||||
const { data } = await supabase.from('companies').select('name').eq('id', entityId).maybeSingle();
|
||||
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching entity name:', err);
|
||||
console.error('Error fetching entity name for photo operation:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Add debugging warning if critical data is missing
|
||||
if (!itemData.entity_name && item.item_type === 'photo_delete') {
|
||||
console.warn(`[Photo Delete] Missing entity_name for photo_delete item`, {
|
||||
item_type: item.item_type,
|
||||
has_entity_type: !!itemData.entity_type,
|
||||
has_entity_id: !!itemData.entity_id,
|
||||
has_cloudflare_url: !!itemData.cloudflare_image_url
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// For regular entities, use name field
|
||||
entityName = itemData.name || originalData?.name || 'Unknown';
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
-- Phase 1: Fix existing photo_delete submissions with missing entity_name
|
||||
|
||||
-- Create a function to backfill missing entity names for photo_delete submissions
|
||||
CREATE OR REPLACE FUNCTION backfill_photo_delete_entity_names()
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO 'public'
|
||||
AS $$
|
||||
DECLARE
|
||||
item_record RECORD;
|
||||
entity_name_value TEXT;
|
||||
BEGIN
|
||||
-- Find all photo_delete submission_items missing entity_name
|
||||
FOR item_record IN
|
||||
SELECT id, item_data
|
||||
FROM submission_items
|
||||
WHERE item_type = 'photo_delete'
|
||||
AND item_data->>'entity_name' IS NULL
|
||||
AND item_data->>'entity_id' IS NOT NULL
|
||||
AND item_data->>'entity_type' IS NOT NULL
|
||||
LOOP
|
||||
-- Fetch entity name based on entity_type
|
||||
CASE item_record.item_data->>'entity_type'
|
||||
WHEN 'park' THEN
|
||||
SELECT name INTO entity_name_value FROM parks WHERE id = (item_record.item_data->>'entity_id')::uuid;
|
||||
WHEN 'ride' THEN
|
||||
SELECT name INTO entity_name_value FROM rides WHERE id = (item_record.item_data->>'entity_id')::uuid;
|
||||
WHEN 'ride_model' THEN
|
||||
SELECT name INTO entity_name_value FROM ride_models WHERE id = (item_record.item_data->>'entity_id')::uuid;
|
||||
WHEN 'manufacturer', 'operator', 'designer', 'property_owner' THEN
|
||||
SELECT name INTO entity_name_value FROM companies WHERE id = (item_record.item_data->>'entity_id')::uuid;
|
||||
ELSE
|
||||
entity_name_value := NULL;
|
||||
END CASE;
|
||||
|
||||
-- Update the item_data if we found a name
|
||||
IF entity_name_value IS NOT NULL THEN
|
||||
UPDATE submission_items
|
||||
SET item_data = item_data || jsonb_build_object('entity_name', entity_name_value)
|
||||
WHERE id = item_record.id;
|
||||
|
||||
RAISE NOTICE 'Updated submission_item % with entity_name: %', item_record.id, entity_name_value;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Run the backfill function
|
||||
SELECT backfill_photo_delete_entity_names();
|
||||
|
||||
-- Drop the function after use (optional, but keeps things clean)
|
||||
DROP FUNCTION IF EXISTS backfill_photo_delete_entity_names();
|
||||
Reference in New Issue
Block a user