diff --git a/src/app/(dashboard)/images/page.tsx b/src/app/(dashboard)/images/page.tsx index 5fdbee6..71f2e16 100644 --- a/src/app/(dashboard)/images/page.tsx +++ b/src/app/(dashboard)/images/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { useState } from 'react' +import { useState, useCallback } from 'react' import { Header } from '@/components/layout/Header' import { DropZone } from '@/components/images/DropZone' import { ImageCard } from '@/components/images/ImageCard' @@ -10,8 +10,36 @@ import { useImages } from '@/hooks/useImages' import type { ImageItem } from '@/types/images' function ImagesPageInner() { - const { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, removeItem, assignProduct, reprocess } = useImages() + const { items, addFiles, setFeather, setAlphaThreshold, rotate, uploadOne, uploadAll, removeItem, assignProduct, reprocess, detour } = useImages() const [lightboxItem, setLightboxItem] = useState(null) + const [selected, setSelected] = useState>(new Set()) + + const toggleSelect = useCallback((id: string, value: boolean) => { + setSelected(prev => { + const next = new Set(prev) + if (value) next.add(id) + else next.delete(id) + return next + }) + }, []) + + const selectAll = useCallback(() => { + const detourable = items.filter(i => + i.status === 'converted' || i.status === 'not_found' || i.status === 'error' + ) + setSelected(new Set(detourable.map(i => i.id) as string[])) + }, [items]) + + const clearSelection = useCallback(() => setSelected(new Set()), []) + + const detourSelected = useCallback(() => { + Array.from(selected).forEach(id => detour(id)) + setSelected(new Set()) + }, [selected, detour]) + + const detourable = items.filter(i => + i.status === 'converted' || i.status === 'not_found' || i.status === 'error' + ) return ( <> @@ -31,12 +59,39 @@ function ImagesPageInner() { )} + {/* Barre de sélection multiple pour détourage par lot */} + {detourable.length > 0 && ( +
+ {detourable.length} image{detourable.length > 1 ? 's' : ''} sans détourage + + {selected.size > 0 && ( + <> + · + {selected.size} sélectionnée{selected.size > 1 ? 's' : ''} + + + + )} +
+ )} + {items.length > 0 && (
{items.map((item) => ( ))}
diff --git a/src/components/images/ImageCard.tsx b/src/components/images/ImageCard.tsx index 8595c48..0b74946 100644 --- a/src/components/images/ImageCard.tsx +++ b/src/components/images/ImageCard.tsx @@ -4,6 +4,8 @@ import type { ImageItem } from '@/types/images' interface ImageCardProps { item: ImageItem + selected?: boolean + onSelect?: (id: string, selected: boolean) => void onFeatherChange: (id: string, value: number) => void onAlphaThresholdChange: (id: string, value: number) => void onRotate: (id: string, direction: 'cw' | 'ccw') => void @@ -12,19 +14,21 @@ interface ImageCardProps { onOpenLightbox: (item: ImageItem) => void onAssignProduct: (id: string, shopifyProductId: string, shopifyProductTitle: string) => void onReprocess: (id: string) => void + onDetour: (id: string) => void } const STATUS_LABELS: Record = { - pending: { label: '⏳ En attente', color: 'text-slate-400' }, - converting: { label: '🔄 Conversion HEIC', color: 'text-yellow-400' }, - processing: { label: '🤖 Détourage IA', color: 'text-blue-400' }, - searching: { label: '🔍 Recherche…', color: 'text-purple-400' }, - ready: { label: '✓ Prêt', color: 'text-green-400' }, - uploading: { label: '⬆️ Upload…', color: 'text-blue-400' }, - uploaded: { label: '✅ Uploadé', color: 'text-green-500' }, - skipped: { label: '⏭️ Déjà sur Shopify', color: 'text-slate-400' }, - not_found: { label: '❌ Réf. introuvable', color: 'text-red-400' }, - error: { label: '❌ Erreur', color: 'text-red-400' }, + pending: { label: '⏳ En attente', color: 'text-slate-400' }, + converting: { label: '🔄 Conversion HEIC', color: 'text-yellow-400' }, + searching: { label: '🔍 Recherche…', color: 'text-purple-400' }, + converted: { label: '✓ Prêt (sans détourage)', color: 'text-amber-400' }, + processing: { label: '🤖 Détourage IA', color: 'text-blue-400' }, + ready: { label: '✓ Détouré', color: 'text-green-400' }, + uploading: { label: '⬆️ Upload…', color: 'text-blue-400' }, + uploaded: { label: '✅ Uploadé', color: 'text-green-500' }, + skipped: { label: '⏭️ Déjà sur Shopify', color: 'text-slate-400' }, + not_found: { label: '❌ Réf. introuvable', color: 'text-red-400' }, + error: { label: '❌ Erreur', color: 'text-red-400' }, } function BlobThumbnail({ blob, alt }: { blob: Blob; alt: string }) { @@ -95,6 +99,8 @@ function ProductSearch({ itemId, onAssign }: { itemId: string; onAssign: (id: st export function ImageCard({ item, + selected = false, + onSelect, onFeatherChange, onAlphaThresholdChange, onRotate, @@ -103,18 +109,31 @@ export function ImageCard({ onOpenLightbox, onAssignProduct, onReprocess, + onDetour, }: ImageCardProps) { const statusInfo = STATUS_LABELS[item.status] ?? { label: item.status, color: 'text-slate-400' } const isProcessing = ['pending', 'converting', 'processing', 'searching', 'uploading'].includes(item.status) const canAdjust = !!item.rawPngBlob && !isProcessing + const canUpload = (item.status === 'ready' || item.status === 'converted') && !!item.shopifyProductId + const canDetour = (item.status === 'converted' || item.status === 'error' || item.status === 'not_found') && !isProcessing return ( -
+
{/* Miniatures avant / après */}
onOpenLightbox(item)} > + {onSelect && ( +
e.stopPropagation()}> + onSelect(item.id, e.target.checked)} + className="w-4 h-4 accent-indigo-500 cursor-pointer" + /> +
+ )}
@@ -195,7 +214,7 @@ export function ImageCard({ {/* Boutons d'action */}
- {item.status === 'ready' && ( + {canUpload && ( )} - {(item.status === 'ready' || item.status === 'uploaded' || item.status === 'skipped' || item.status === 'error' || item.status === 'not_found') && item.rawPngBlob && ( + {canDetour && ( + + )} + {(item.status === 'ready' || item.status === 'uploaded' || item.status === 'skipped' || item.status === 'error') && item.rawPngBlob && (