From 5e2b2e6e5a571d6b8e190ea5b0750aa957867ce9 Mon Sep 17 00:00:00 2001 From: Nico Date: Sun, 31 May 2026 21:41:33 -0700 Subject: [PATCH] feat: add drag reorder for zones --- .../components/manage/StoreZoneManager.jsx | 131 +++++++++++++++--- .../styles/components/manage/ManageStores.css | 50 +++++++ .../tests/available-items-catalog.spec.ts | 35 ++++- 3 files changed, 195 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/manage/StoreZoneManager.jsx b/frontend/src/components/manage/StoreZoneManager.jsx index 71e274a..dd779d4 100644 --- a/frontend/src/components/manage/StoreZoneManager.jsx +++ b/frontend/src/components/manage/StoreZoneManager.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { createLocationZone, deleteLocationZone, @@ -102,9 +102,15 @@ export default function StoreZoneManager({ const [pendingDeleteZones, setPendingDeleteZones] = useState([]); const [editingZone, setEditingZone] = useState(null); const [editingZoneDraft, setEditingZoneDraft] = useState({ name: "" }); + const [draggedZoneId, setDraggedZoneId] = useState(null); + const [dragOverZoneId, setDragOverZoneId] = useState(null); + const [reordering, setReordering] = useState(false); + const draggedZoneIdRef = useRef(null); + const hasDraggedZoneRef = useRef(false); const selectedDeleteZones = zones.filter((zone) => selectedDeleteIds.has(zone.id)); const selectedDeleteCount = selectedDeleteZones.length; + const canDragReorder = canManage && !deleteMode && !loading && !reordering; const loadZones = async () => { if (!householdId || !location?.id) return; @@ -128,6 +134,10 @@ export default function StoreZoneManager({ setSelectedDeleteIds(new Set()); setPendingDeleteZones([]); setEditingZone(null); + setDraggedZoneId(null); + setDragOverZoneId(null); + draggedZoneIdRef.current = null; + hasDraggedZoneRef.current = false; }; const openZoneSettings = (zone) => { @@ -194,30 +204,95 @@ export default function StoreZoneManager({ } }; - const handleMoveZone = async (zone, direction) => { - const currentIndex = zones.findIndex((candidate) => candidate.id === zone.id); - const swapIndex = currentIndex + direction; - if (currentIndex < 0 || swapIndex < 0 || swapIndex >= zones.length) return; + const persistZoneOrder = async (orderedZones) => { + const zonesWithSortOrder = orderedZones.map((zone, index) => ({ + ...zone, + sort_order: (index + 1) * 10, + })); + const changedZones = zonesWithSortOrder.filter((zone) => { + const currentZone = zones.find((candidate) => candidate.id === zone.id); + return currentZone && currentZone.sort_order !== zone.sort_order; + }); - const other = zones[swapIndex]; + if (changedZones.length === 0) return true; + + setReordering(true); + setZones(zonesWithSortOrder); try { - await Promise.all([ - updateLocationZone(householdId, location.id, zone.id, { - sort_order: other.sort_order, - }), - updateLocationZone(householdId, location.id, other.id, { - sort_order: zone.sort_order, - }), - ]); + await Promise.all( + changedZones.map((zone) => + updateLocationZone(householdId, location.id, zone.id, { + sort_order: zone.sort_order, + }) + ) + ); await loadZones(); await refreshActiveZones(); - setEditingZone(null); + toast.success("Reordered zones", "Updated shopping order"); + return true; } catch (error) { const message = getApiErrorMessage(error, "Failed to reorder zones"); toast.error("Reorder zones failed", `Reorder zones failed: ${message}`); + await loadZones(); + return false; + } finally { + setReordering(false); } }; + const moveZone = async (fromIndex, toIndex) => { + if (fromIndex < 0 || toIndex < 0 || fromIndex >= zones.length || toIndex >= zones.length) { + return false; + } + + const orderedZones = [...zones]; + const [movedZone] = orderedZones.splice(fromIndex, 1); + orderedZones.splice(toIndex, 0, movedZone); + return persistZoneOrder(orderedZones); + }; + + const handleMoveZone = async (zone, direction) => { + const currentIndex = zones.findIndex((candidate) => candidate.id === zone.id); + const moved = await moveZone(currentIndex, currentIndex + direction); + if (moved) { + setEditingZone(null); + } + }; + + const handleZoneDragStart = (event, zoneId) => { + if (!canDragReorder) { + event.preventDefault(); + return; + } + + draggedZoneIdRef.current = zoneId; + hasDraggedZoneRef.current = true; + setDraggedZoneId(zoneId); + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", String(zoneId)); + }; + + const clearZoneDrag = () => { + draggedZoneIdRef.current = null; + setDraggedZoneId(null); + setDragOverZoneId(null); + window.setTimeout(() => { + hasDraggedZoneRef.current = false; + }, 0); + }; + + const handleZoneDrop = async (event, targetZoneId) => { + event.preventDefault(); + const sourceZoneId = draggedZoneIdRef.current || event.dataTransfer.getData("text/plain"); + clearZoneDrag(); + + if (!sourceZoneId || String(sourceZoneId) === String(targetZoneId)) return; + + const sourceIndex = zones.findIndex((zone) => String(zone.id) === String(sourceZoneId)); + const targetIndex = zones.findIndex((zone) => String(zone.id) === String(targetZoneId)); + await moveZone(sourceIndex, targetIndex); + }; + const handleSaveZone = async () => { if (!editingZone) return; @@ -341,14 +416,27 @@ export default function StoreZoneManager({