feat: Implement photo change detection

This commit is contained in:
gpt-engineer-app[bot]
2025-10-03 15:54:46 +00:00
parent 86fb99c696
commit b047291eb6
3 changed files with 117 additions and 11 deletions

View File

@@ -1,6 +1,9 @@
import { useState, useEffect } from 'react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { FieldDiff, ImageDiff, LocationDiff } from './FieldComparison'; import { FieldDiff, ImageDiff, LocationDiff } from './FieldComparison';
import { detectChanges } from '@/lib/submissionChangeDetection'; import { PhotoAdditionPreview, PhotoEditPreview, PhotoDeletionPreview } from './PhotoComparison';
import { detectChanges, type ChangesSummary } from '@/lib/submissionChangeDetection';
import type { SubmissionItemData } from '@/types/submissions'; import type { SubmissionItemData } from '@/types/submissions';
import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService'; import type { SubmissionItemWithDeps } from '@/lib/submissionItemsService';
import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus, AlertTriangle } from 'lucide-react'; import { Building2, Train, MapPin, Building, User, ImageIcon, Trash2, Edit, Plus, AlertTriangle } from 'lucide-react';
@@ -9,6 +12,7 @@ interface SubmissionChangesDisplayProps {
item: SubmissionItemData | SubmissionItemWithDeps; item: SubmissionItemData | SubmissionItemWithDeps;
view?: 'summary' | 'detailed'; view?: 'summary' | 'detailed';
showImages?: boolean; showImages?: boolean;
submissionId?: string;
} }
// Helper to determine change magnitude // Helper to determine change magnitude
@@ -24,9 +28,25 @@ function getChangeMagnitude(totalChanges: number, hasImages: boolean, action: st
export function SubmissionChangesDisplay({ export function SubmissionChangesDisplay({
item, item,
view = 'summary', view = 'summary',
showImages = true showImages = true,
submissionId
}: SubmissionChangesDisplayProps) { }: SubmissionChangesDisplayProps) {
const changes = detectChanges(item); const [changes, setChanges] = useState<ChangesSummary | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadChanges = async () => {
setLoading(true);
const detectedChanges = await detectChanges(item, submissionId);
setChanges(detectedChanges);
setLoading(false);
};
loadChanges();
}, [item, submissionId]);
if (loading || !changes) {
return <Skeleton className="h-16 w-full" />;
}
// Get appropriate icon for entity type // Get appropriate icon for entity type
const getEntityIcon = () => { const getEntityIcon = () => {
@@ -83,6 +103,18 @@ export function SubmissionChangesDisplay({
{changes.imageChanges.map((change, idx) => ( {changes.imageChanges.map((change, idx) => (
<ImageDiff key={`img-${idx}`} change={change} compact /> <ImageDiff key={`img-${idx}`} change={change} compact />
))} ))}
{changes.photoChanges.map((change, idx) => {
if (change.type === 'added' && change.photos) {
return <PhotoAdditionPreview key={`photo-${idx}`} photos={change.photos} compact />;
}
if (change.type === 'edited' && change.photo) {
return <PhotoEditPreview key={`photo-${idx}`} photo={change.photo} compact />;
}
if (change.type === 'deleted' && change.photo) {
return <PhotoDeletionPreview key={`photo-${idx}`} photo={change.photo} compact />;
}
return null;
})}
{changes.hasLocationChange && ( {changes.hasLocationChange && (
<Badge variant="outline" className="text-blue-600 dark:text-blue-400"> <Badge variant="outline" className="text-blue-600 dark:text-blue-400">
Location Location
@@ -156,6 +188,26 @@ export function SubmissionChangesDisplay({
</div> </div>
)} )}
{showImages && changes.photoChanges.length > 0 && (
<div className="flex flex-col gap-2">
<h4 className="text-sm font-medium">Photo Changes</h4>
<div className="grid gap-2">
{changes.photoChanges.map((change, idx) => {
if (change.type === 'added' && change.photos) {
return <PhotoAdditionPreview key={idx} photos={change.photos} compact={false} />;
}
if (change.type === 'edited' && change.photo) {
return <PhotoEditPreview key={idx} photo={change.photo} compact={false} />;
}
if (change.type === 'deleted' && change.photo) {
return <PhotoDeletionPreview key={idx} photo={change.photo} compact={false} />;
}
return null;
})}
</div>
</div>
)}
{changes.hasLocationChange && ( {changes.hasLocationChange && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h4 className="text-sm font-medium">Location Change</h4> <h4 className="text-sm font-medium">Location Change</h4>

View File

@@ -97,6 +97,7 @@ export function SubmissionItemsList({
item={item} item={item}
view={view} view={view}
showImages={showImages} showImages={showImages}
submissionId={submissionId}
/> />
</div> </div>
))} ))}

View File

@@ -1,4 +1,5 @@
import type { SubmissionItemData } from '@/types/submissions'; import type { SubmissionItemData } from '@/types/submissions';
import { supabase } from '@/integrations/supabase/client';
export interface FieldChange { export interface FieldChange {
field: string; field: string;
@@ -17,11 +18,16 @@ export interface ImageChange {
export interface PhotoChange { export interface PhotoChange {
type: 'added' | 'edited' | 'deleted'; type: 'added' | 'edited' | 'deleted';
photoUrl: string; photos?: Array<{ url: string; title?: string; caption?: string }>;
photo?: {
url: string;
title?: string; title?: string;
caption?: string; caption?: string;
oldCaption?: string; oldCaption?: string;
newCaption?: string; newCaption?: string;
oldTitle?: string;
newTitle?: string;
};
} }
export interface ChangesSummary { export interface ChangesSummary {
@@ -35,10 +41,54 @@ export interface ChangesSummary {
totalChanges: number; totalChanges: number;
} }
/**
* Detects photo changes for a submission
*/
async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]> {
const changes: PhotoChange[] = [];
try {
// Fetch photo submission with items
const { data: photoSubmission, error } = await supabase
.from('photo_submissions')
.select(`
*,
items:photo_submission_items(*)
`)
.eq('submission_id', submissionId)
.maybeSingle();
if (error) {
console.error('Error fetching photo submissions:', error);
return changes;
}
if (photoSubmission?.items && photoSubmission.items.length > 0) {
// For now, treat all photos as additions
// TODO: Implement edit/delete detection by comparing with existing entity photos
changes.push({
type: 'added',
photos: photoSubmission.items.map((item: any) => ({
url: item.cloudflare_image_url,
title: item.title,
caption: item.caption
}))
});
}
} catch (err) {
console.error('Error detecting photo changes:', err);
}
return changes;
}
/** /**
* Detects what changed between original_data and item_data * Detects what changed between original_data and item_data
*/ */
export function detectChanges(item: { item_data?: any; original_data?: any; item_type: string }): ChangesSummary { export async function detectChanges(
item: { item_data?: any; original_data?: any; item_type: string },
submissionId?: string
): Promise<ChangesSummary> {
const itemData = item.item_data || {}; const itemData = item.item_data || {};
const originalData = item.original_data || {}; const originalData = item.original_data || {};
@@ -124,15 +174,18 @@ export function detectChanges(item: { item_data?: any; original_data?: any; item
// Get entity name // Get entity name
const entityName = itemData.name || originalData?.name || 'Unknown'; const entityName = itemData.name || originalData?.name || 'Unknown';
// Detect photo changes if submissionId provided
const photoChanges = submissionId ? await detectPhotoChanges(submissionId) : [];
return { return {
action, action,
entityType: item.item_type, entityType: item.item_type,
entityName, entityName,
fieldChanges, fieldChanges,
imageChanges, imageChanges,
photoChanges: [], // Will be populated by component with submissionId photoChanges,
hasLocationChange, hasLocationChange,
totalChanges: fieldChanges.length + imageChanges.length + (hasLocationChange ? 1 : 0) totalChanges: fieldChanges.length + imageChanges.length + photoChanges.length + (hasLocationChange ? 1 : 0)
}; };
} }