Implement remaining phases

This commit is contained in:
gpt-engineer-app[bot]
2025-10-03 15:59:05 +00:00
parent a4805cf0cc
commit 177e86a77a
4 changed files with 346 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ import { formatFieldName, formatFieldValue } from '@/lib/submissionChangeDetecti
import type { FieldChange, ImageChange } from '@/lib/submissionChangeDetection';
import { ArrowRight } from 'lucide-react';
import { ArrayFieldDiff } from './ArrayFieldDiff';
import { SpecialFieldDisplay } from './SpecialFieldDisplay';
interface FieldDiffProps {
change: FieldChange;
@@ -24,6 +25,12 @@ export function FieldDiff({ change, compact = false }: FieldDiffProps) {
);
}
// Check if this is a special field type that needs custom rendering
const specialDisplay = SpecialFieldDisplay({ change, compact });
if (specialDisplay) {
return specialDisplay;
}
const getChangeColor = () => {
switch (changeType) {
case 'added': return 'text-green-600 dark:text-green-400';

View File

@@ -11,9 +11,10 @@ interface ItemReviewCardProps {
item: SubmissionItemWithDeps;
onEdit: () => void;
onStatusChange: (status: 'approved' | 'rejected') => void;
submissionId: string;
}
export function ItemReviewCard({ item, onEdit, onStatusChange }: ItemReviewCardProps) {
export function ItemReviewCard({ item, onEdit, onStatusChange, submissionId }: ItemReviewCardProps) {
const isMobile = useIsMobile();
const getItemIcon = () => {
@@ -40,8 +41,15 @@ export function ItemReviewCard({ item, onEdit, onStatusChange }: ItemReviewCardP
};
const renderItemPreview = () => {
// Use standardized change detection display
return <SubmissionChangesDisplay item={item} view="summary" showImages={true} />;
// Use detailed view for review manager with photo detection
return (
<SubmissionChangesDisplay
item={item}
view="detailed"
showImages={true}
submissionId={submissionId}
/>
);
};
return (

View File

@@ -0,0 +1,327 @@
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { MeasurementDisplay } from '@/components/ui/measurement-display';
import { SpeedDisplay } from '@/components/ui/speed-display';
import { MapPin, ArrowRight, Calendar, ExternalLink } from 'lucide-react';
import type { FieldChange } from '@/lib/submissionChangeDetection';
import { formatFieldValue } from '@/lib/submissionChangeDetection';
interface SpecialFieldDisplayProps {
change: FieldChange;
compact?: boolean;
}
export function SpecialFieldDisplay({ change, compact = false }: SpecialFieldDisplayProps) {
const fieldName = change.field.toLowerCase();
// Detect field type
if (fieldName.includes('speed') || fieldName === 'max_speed_kmh') {
return <SpeedFieldDisplay change={change} compact={compact} />;
}
if (fieldName.includes('height') || fieldName.includes('length') ||
fieldName === 'max_height_meters' || fieldName === 'length_meters' ||
fieldName === 'drop_height_meters') {
return <MeasurementFieldDisplay change={change} compact={compact} />;
}
if (fieldName === 'status') {
return <StatusFieldDisplay change={change} compact={compact} />;
}
if (fieldName.includes('date') && !fieldName.includes('updated') && !fieldName.includes('created')) {
return <DateFieldDisplay change={change} compact={compact} />;
}
if (fieldName.includes('_id') && fieldName !== 'id' && fieldName !== 'user_id') {
return <RelationshipFieldDisplay change={change} compact={compact} />;
}
if (fieldName === 'latitude' || fieldName === 'longitude') {
return <CoordinateFieldDisplay change={change} compact={compact} />;
}
// Fallback to null, will be handled by regular FieldDiff
return null;
}
function SpeedFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
if (compact) {
return (
<Badge variant="outline" className="text-blue-600 dark:text-blue-400">
Speed
</Badge>
);
}
const formatFieldName = (name: string) =>
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
.replace(/^./, str => str.toUpperCase());
return (
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
<div className="text-sm font-medium">{formatFieldName(change.field)}</div>
{change.changeType === 'modified' && (
<div className="flex items-center gap-3 text-sm">
<div className="text-red-600 dark:text-red-400 line-through">
<SpeedDisplay kmh={change.oldValue} />
</div>
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<div className="text-green-600 dark:text-green-400">
<SpeedDisplay kmh={change.newValue} />
</div>
</div>
)}
{change.changeType === 'added' && (
<div className="text-sm text-green-600 dark:text-green-400">
+ <SpeedDisplay kmh={change.newValue} />
</div>
)}
{change.changeType === 'removed' && (
<div className="text-sm text-red-600 dark:text-red-400 line-through">
<SpeedDisplay kmh={change.oldValue} />
</div>
)}
</div>
);
}
function MeasurementFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
if (compact) {
return (
<Badge variant="outline" className="text-purple-600 dark:text-purple-400">
Measurement
</Badge>
);
}
const formatFieldName = (name: string) =>
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
.replace(/^./, str => str.toUpperCase());
return (
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
<div className="text-sm font-medium">{formatFieldName(change.field)}</div>
{change.changeType === 'modified' && (
<div className="flex items-center gap-3 text-sm">
<div className="text-red-600 dark:text-red-400 line-through">
<MeasurementDisplay value={change.oldValue} type="distance" />
</div>
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<div className="text-green-600 dark:text-green-400">
<MeasurementDisplay value={change.newValue} type="distance" />
</div>
</div>
)}
{change.changeType === 'added' && (
<div className="text-sm text-green-600 dark:text-green-400">
+ <MeasurementDisplay value={change.newValue} type="distance" />
</div>
)}
{change.changeType === 'removed' && (
<div className="text-sm text-red-600 dark:text-red-400 line-through">
<MeasurementDisplay value={change.oldValue} type="distance" />
</div>
)}
</div>
);
}
function StatusFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
const getStatusColor = (status: string) => {
const statusLower = String(status).toLowerCase();
if (statusLower === 'operating' || statusLower === 'active') return 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20';
if (statusLower === 'closed' || statusLower === 'inactive') return 'bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20';
if (statusLower === 'under_construction' || statusLower === 'pending') return 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20';
return 'bg-muted/30 text-muted-foreground';
};
if (compact) {
return (
<Badge variant="outline" className="text-indigo-600 dark:text-indigo-400">
Status
</Badge>
);
}
const formatFieldName = (name: string) =>
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
.replace(/^./, str => str.toUpperCase());
return (
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
<div className="text-sm font-medium">{formatFieldName(change.field)}</div>
{change.changeType === 'modified' && (
<div className="flex items-center gap-3">
<Badge className={`${getStatusColor(change.oldValue)} line-through`}>
{formatFieldValue(change.oldValue)}
</Badge>
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<Badge className={getStatusColor(change.newValue)}>
{formatFieldValue(change.newValue)}
</Badge>
</div>
)}
{change.changeType === 'added' && (
<Badge className={getStatusColor(change.newValue)}>
{formatFieldValue(change.newValue)}
</Badge>
)}
{change.changeType === 'removed' && (
<Badge className={`${getStatusColor(change.oldValue)} line-through opacity-75`}>
{formatFieldValue(change.oldValue)}
</Badge>
)}
</div>
);
}
function DateFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
if (compact) {
return (
<Badge variant="outline" className="text-teal-600 dark:text-teal-400">
<Calendar className="h-3 w-3 mr-1" />
Date
</Badge>
);
}
const formatFieldName = (name: string) =>
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
.replace(/^./, str => str.toUpperCase());
return (
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
<div className="text-sm font-medium flex items-center gap-2">
<Calendar className="h-4 w-4" />
{formatFieldName(change.field)}
</div>
{change.changeType === 'modified' && (
<div className="flex items-center gap-3 text-sm">
<span className="text-red-600 dark:text-red-400 line-through">
{formatFieldValue(change.oldValue)}
</span>
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<span className="text-green-600 dark:text-green-400">
{formatFieldValue(change.newValue)}
</span>
</div>
)}
{change.changeType === 'added' && (
<div className="text-sm text-green-600 dark:text-green-400">
+ {formatFieldValue(change.newValue)}
</div>
)}
{change.changeType === 'removed' && (
<div className="text-sm text-red-600 dark:text-red-400 line-through">
{formatFieldValue(change.oldValue)}
</div>
)}
</div>
);
}
function RelationshipFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
// This would ideally fetch entity names, but for now we show IDs with better formatting
const formatFieldName = (name: string) =>
name.replace(/_id$/, '').replace(/_/g, ' ').trim()
.replace(/^./, str => str.toUpperCase());
if (compact) {
return (
<Badge variant="outline" className="text-cyan-600 dark:text-cyan-400">
{formatFieldName(change.field)}
</Badge>
);
}
return (
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
<div className="text-sm font-medium">{formatFieldName(change.field)}</div>
{change.changeType === 'modified' && (
<div className="flex items-center gap-3 text-sm font-mono">
<span className="text-red-600 dark:text-red-400 line-through text-xs">
{String(change.oldValue).slice(0, 8)}...
</span>
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<span className="text-green-600 dark:text-green-400 text-xs">
{String(change.newValue).slice(0, 8)}...
</span>
</div>
)}
{change.changeType === 'added' && (
<div className="text-sm text-green-600 dark:text-green-400 font-mono text-xs">
+ {String(change.newValue).slice(0, 8)}...
</div>
)}
{change.changeType === 'removed' && (
<div className="text-sm text-red-600 dark:text-red-400 line-through font-mono text-xs">
{String(change.oldValue).slice(0, 8)}...
</div>
)}
</div>
);
}
function CoordinateFieldDisplay({ change, compact }: { change: FieldChange; compact: boolean }) {
if (compact) {
return (
<Badge variant="outline" className="text-orange-600 dark:text-orange-400">
<MapPin className="h-3 w-3 mr-1" />
Coordinates
</Badge>
);
}
const formatFieldName = (name: string) =>
name.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
.replace(/^./, str => str.toUpperCase());
return (
<div className="flex flex-col gap-2 p-3 rounded-md bg-muted/30 border">
<div className="text-sm font-medium flex items-center gap-2">
<MapPin className="h-4 w-4" />
{formatFieldName(change.field)}
</div>
{change.changeType === 'modified' && (
<div className="flex items-center gap-3 text-sm">
<span className="text-red-600 dark:text-red-400 line-through font-mono">
{Number(change.oldValue).toFixed(6)}°
</span>
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<span className="text-green-600 dark:text-green-400 font-mono">
{Number(change.newValue).toFixed(6)}°
</span>
</div>
)}
{change.changeType === 'added' && (
<div className="text-sm text-green-600 dark:text-green-400 font-mono">
+ {Number(change.newValue).toFixed(6)}°
</div>
)}
{change.changeType === 'removed' && (
<div className="text-sm text-red-600 dark:text-red-400 line-through font-mono">
{Number(change.oldValue).toFixed(6)}°
</div>
)}
</div>
);
}

View File

@@ -420,6 +420,7 @@ export function SubmissionReviewManager({
// Status changes handled via approve/reject actions
await loadSubmissionItems();
}}
submissionId={submissionId}
/>
</div>
))}