feat: Implement item editing capability

This commit is contained in:
gpt-engineer-app[bot]
2025-09-30 14:16:59 +00:00
parent a7288a0d4c
commit 337552224b
4 changed files with 385 additions and 1 deletions

View File

@@ -0,0 +1,276 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { toast } from '@/hooks/use-toast';
import { useIsMobile } from '@/hooks/use-mobile';
import { useUserRole } from '@/hooks/useUserRole';
import { useAuth } from '@/hooks/useAuth';
import { editSubmissionItem, type SubmissionItemWithDeps } from '@/lib/submissionItemsService';
import { ParkForm } from '@/components/admin/ParkForm';
import { RideForm } from '@/components/admin/RideForm';
import { ManufacturerForm } from '@/components/admin/ManufacturerForm';
import { DesignerForm } from '@/components/admin/DesignerForm';
import { OperatorForm } from '@/components/admin/OperatorForm';
import { PropertyOwnerForm } from '@/components/admin/PropertyOwnerForm';
import { RideModelForm } from '@/components/admin/RideModelForm';
import { Save, X, Edit } from 'lucide-react';
interface ItemEditDialogProps {
item: SubmissionItemWithDeps | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onComplete: () => void;
}
export function ItemEditDialog({ item, open, onOpenChange, onComplete }: ItemEditDialogProps) {
const [submitting, setSubmitting] = useState(false);
const isMobile = useIsMobile();
const { isModerator } = useUserRole();
const { user } = useAuth();
const Container = isMobile ? Sheet : Dialog;
if (!item) return null;
const handleSubmit = async (data: any) => {
if (!user?.id) {
toast({
title: 'Authentication Required',
description: 'You must be logged in to edit items',
variant: 'destructive',
});
return;
}
setSubmitting(true);
try {
await editSubmissionItem(item.id, data, user.id);
toast({
title: isModerator() ? 'Item Updated' : 'Edit Submitted',
description: isModerator()
? 'The item has been updated successfully.'
: 'Your edit has been submitted and will be reviewed by a moderator.',
});
onComplete();
onOpenChange(false);
} catch (error: any) {
toast({
title: 'Error',
description: error.message || 'Failed to save changes',
variant: 'destructive',
});
} finally {
setSubmitting(false);
}
};
const handlePhotoSubmit = async (caption: string, credit: string) => {
const photoData = {
...item.item_data,
photos: item.item_data.photos?.map((photo: any) => ({
...photo,
caption,
credit,
})),
};
await handleSubmit(photoData);
};
const renderEditForm = () => {
const data = item.item_data;
switch (item.item_type) {
case 'park':
return (
<ParkForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={data}
isEditing
/>
);
case 'ride':
return (
<RideForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={data}
isEditing
/>
);
case 'manufacturer':
return (
<ManufacturerForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={data}
/>
);
case 'designer':
return (
<DesignerForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={data}
/>
);
case 'operator':
return (
<OperatorForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={data}
/>
);
case 'property_owner':
return (
<PropertyOwnerForm
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={data}
/>
);
case 'ride_model':
return (
<RideModelForm
manufacturerName={data.manufacturer_name || 'Unknown'}
manufacturerId={data.manufacturer_id}
onSubmit={handleSubmit}
onCancel={() => onOpenChange(false)}
initialData={data}
/>
);
case 'photo':
return (
<PhotoEditForm
photos={data.photos || []}
onSubmit={handlePhotoSubmit}
onCancel={() => onOpenChange(false)}
submitting={submitting}
/>
);
default:
return (
<div className="text-center py-8 text-muted-foreground">
No edit form available for this item type
</div>
);
}
};
return (
<Container open={open} onOpenChange={onOpenChange}>
{isMobile ? (
<SheetContent side="bottom" className="h-[90vh] overflow-y-auto">
<SheetHeader>
<SheetTitle className="flex items-center gap-2">
<Edit className="w-5 h-5" />
Edit {item.item_type.replace('_', ' ')}
</SheetTitle>
<SheetDescription>
Make changes to this submission item
</SheetDescription>
</SheetHeader>
<div className="mt-6">
{renderEditForm()}
</div>
</SheetContent>
) : (
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Edit className="w-5 h-5" />
Edit {item.item_type.replace('_', ' ')}
</DialogTitle>
<DialogDescription>
Make changes to this submission item
</DialogDescription>
</DialogHeader>
<div className="mt-4">
{renderEditForm()}
</div>
</DialogContent>
)}
</Container>
);
}
// Simple photo editing form for caption and credit
function PhotoEditForm({
photos,
onSubmit,
onCancel,
submitting
}: {
photos: any[];
onSubmit: (caption: string, credit: string) => void;
onCancel: () => void;
submitting: boolean;
}) {
const [caption, setCaption] = useState(photos[0]?.caption || '');
const [credit, setCredit] = useState(photos[0]?.credit || '');
return (
<div className="space-y-6">
{/* Photo Preview */}
<div className="grid grid-cols-3 gap-2">
{photos.slice(0, 3).map((photo, idx) => (
<img
key={idx}
src={photo.url}
alt={photo.caption || 'Photo'}
className="w-full h-32 object-cover rounded"
/>
))}
</div>
{/* Caption */}
<div className="space-y-2">
<Label htmlFor="caption">Caption</Label>
<Textarea
id="caption"
value={caption}
onChange={(e) => setCaption(e.target.value)}
placeholder="Describe the photo..."
rows={3}
/>
</div>
{/* Credit */}
<div className="space-y-2">
<Label htmlFor="credit">Photo Credit</Label>
<Input
id="credit"
value={credit}
onChange={(e) => setCredit(e.target.value)}
placeholder="Photographer name"
/>
</div>
{/* Actions */}
<div className="flex gap-3 justify-end">
<Button type="button" variant="outline" onClick={onCancel} disabled={submitting}>
<X className="w-4 h-4 mr-2" />
Cancel
</Button>
<Button onClick={() => onSubmit(caption, credit)} disabled={submitting}>
<Save className="w-4 h-4 mr-2" />
{submitting ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
);
}

View File

@@ -127,6 +127,11 @@ export function ItemReviewCard({ item, onEdit, onStatusChange }: ItemReviewCardP
<CardTitle className="text-base">
{item.item_type.replace('_', ' ').toUpperCase()}
</CardTitle>
{item.original_data && (
<Badge variant="outline" className="text-xs">
Edited
</Badge>
)}
</div>
<div className="flex items-center gap-2">
<Badge variant={getStatusColor()}>

View File

@@ -27,6 +27,7 @@ import { DependencyVisualizer } from './DependencyVisualizer';
import { ConflictResolutionDialog } from './ConflictResolutionDialog';
import { EscalationDialog } from './EscalationDialog';
import { RejectionDialog } from './RejectionDialog';
import { ItemEditDialog } from './ItemEditDialog';
interface SubmissionReviewManagerProps {
submissionId: string;
@@ -48,6 +49,8 @@ export function SubmissionReviewManager({
const [showConflictDialog, setShowConflictDialog] = useState(false);
const [showEscalationDialog, setShowEscalationDialog] = useState(false);
const [showRejectionDialog, setShowRejectionDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
const [editingItem, setEditingItem] = useState<SubmissionItemWithDeps | null>(null);
const [activeTab, setActiveTab] = useState<'items' | 'dependencies'>('items');
const { toast } = useToast();
@@ -240,6 +243,17 @@ export function SubmissionReviewManager({
}
};
const handleEdit = (item: SubmissionItemWithDeps) => {
setEditingItem(item);
setShowEditDialog(true);
};
const handleEditComplete = async () => {
setShowEditDialog(false);
setEditingItem(null);
await loadSubmissionItems();
};
const pendingCount = items.filter(item => item.status === 'pending').length;
const selectedCount = selectedItemIds.size;
@@ -292,6 +306,13 @@ export function SubmissionReviewManager({
)}
onReject={handleReject}
/>
<ItemEditDialog
item={editingItem}
open={showEditDialog}
onOpenChange={setShowEditDialog}
onComplete={handleEditComplete}
/>
</>
);
@@ -331,7 +352,7 @@ export function SubmissionReviewManager({
/>
<ItemReviewCard
item={item}
onEdit={() => {/* TODO: Implement editing */}}
onEdit={() => handleEdit(item)}
onStatusChange={(status) => {/* TODO: Update status */}}
/>
</div>

View File

@@ -388,6 +388,88 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi
}
}
/**
* Edit a submission item - moderators edit directly, users auto-escalate
*/
export async function editSubmissionItem(
itemId: string,
newData: any,
userId: string
): Promise<void> {
if (!userId) {
throw new Error('User authentication required to edit items');
}
// Get current item to preserve original_data
const { data: currentItem, error: fetchError } = await supabase
.from('submission_items')
.select('*, submission:content_submissions(user_id)')
.eq('id', itemId)
.single();
if (fetchError) throw fetchError;
// Check if user has moderator/admin permissions
const { data: userRoles } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', userId);
const isModerator = userRoles?.some(r =>
['moderator', 'admin', 'superuser'].includes(r.role)
);
// Preserve original_data if not already set
const originalData = currentItem.original_data || currentItem.item_data;
if (isModerator) {
// Moderators can edit directly
const { error: updateError } = await supabase
.from('submission_items')
.update({
item_data: newData,
original_data: originalData,
updated_at: new Date().toISOString(),
})
.eq('id', itemId);
if (updateError) throw updateError;
// Log admin action
await supabase
.from('admin_audit_log')
.insert({
admin_user_id: userId,
target_user_id: (currentItem.submission as any).user_id,
action: 'edit_submission_item',
details: {
item_id: itemId,
item_type: currentItem.item_type,
changes: 'Item data updated',
},
});
} else {
// Regular users: update data and auto-escalate
const { error: updateError } = await supabase
.from('submission_items')
.update({
item_data: newData,
original_data: originalData,
updated_at: new Date().toISOString(),
})
.eq('id', itemId);
if (updateError) throw updateError;
// Auto-escalate the parent submission
await escalateSubmission(
currentItem.submission_id,
`User requested edit to ${currentItem.item_type}`,
userId
);
}
}
/**
* Escalate submission for admin review
*/