diff --git a/frontend/src/hooks/useLocationMapObjectActions.js b/frontend/src/hooks/useLocationMapObjectActions.js new file mode 100644 index 0000000..ea2a72a --- /dev/null +++ b/frontend/src/hooks/useLocationMapObjectActions.js @@ -0,0 +1,136 @@ +import { useCallback, useState } from "react"; +import { + clampObjectToMap, + createClientObject, + getMapObjectDisplayLabel, + getObjectKey, + objectZoneId, +} from "../lib/locationMapUtils"; + +const EMPTY_ZONES = []; + +export default function useLocationMapObjectActions({ + canManage, + hiddenAreaCount, + mapSize, + mapState, + objects, + selectedObject, + selectedObjectKey, + setEditorTool, + setFuture, + setHasUnsavedChanges, + setHistory, + setObjects, + setSelectedObjectKey, + toast, +}) { + const [pendingDeleteObject, setPendingDeleteObject] = useState(null); + const zones = mapState?.zones || EMPTY_ZONES; + + const remember = useCallback((currentObjects = objects) => { + setHistory((previous) => [...previous.slice(-19), currentObjects.map((object) => ({ ...object }))]); + setFuture([]); + }, [objects, setFuture, setHistory]); + + const updateObjects = useCallback((updater) => { + setHasUnsavedChanges(true); + setObjects((currentObjects) => + updater(currentObjects).map((object) => clampObjectToMap(object, mapSize)) + ); + }, [mapSize, setHasUnsavedChanges, setObjects]); + + const handleAddObject = useCallback(() => { + if (!canManage) return; + remember(); + const usedZoneIds = new Set(objects.map((object) => objectZoneId(object)).filter(Boolean)); + const nextZone = zones.find((zone) => !usedZoneIds.has(String(zone.id))) || zones[0]; + const nextObject = createClientObject(nextZone, objects.length); + setObjects((currentObjects) => [...currentObjects, nextObject]); + setSelectedObjectKey(getObjectKey(nextObject)); + setEditorTool("edit"); + setHasUnsavedChanges(true); + }, [canManage, objects, remember, setEditorTool, setHasUnsavedChanges, setObjects, setSelectedObjectKey, zones]); + + const handleDuplicateObject = useCallback(() => { + if (!selectedObject) return; + remember(); + const displayLabel = getMapObjectDisplayLabel(selectedObject, "Area"); + const nextObject = { + ...selectedObject, + id: undefined, + client_id: `copy-${Date.now()}`, + label: `${displayLabel} Copy`, + x: selectedObject.x + 28, + y: selectedObject.y + 28, + sort_order: objects.length + 1, + }; + setObjects((currentObjects) => [...currentObjects, clampObjectToMap(nextObject, mapSize)]); + setSelectedObjectKey(getObjectKey(nextObject)); + setEditorTool("edit"); + setHasUnsavedChanges(true); + }, [mapSize, objects.length, remember, selectedObject, setEditorTool, setHasUnsavedChanges, setObjects, setSelectedObjectKey]); + + const requestDeleteObject = useCallback(() => { + if (selectedObject) setPendingDeleteObject(selectedObject); + }, [selectedObject]); + + const handleDeleteObject = useCallback(() => { + if (!pendingDeleteObject) return; + remember(); + setObjects((currentObjects) => + currentObjects.filter((object) => getObjectKey(object) !== getObjectKey(pendingDeleteObject)) + ); + setSelectedObjectKey(null); + setHasUnsavedChanges(true); + toast.success("Deleted map area", "Save the draft to keep this change."); + setPendingDeleteObject(null); + }, [pendingDeleteObject, remember, setHasUnsavedChanges, setObjects, setSelectedObjectKey, toast]); + + const handleShowHiddenAreas = useCallback(() => { + if (!canManage || hiddenAreaCount === 0) return; + remember(); + updateObjects((currentObjects) => + currentObjects.map((object) => ( + object.visible === false ? { ...object, visible: true } : object + )) + ); + toast.success("Showed hidden areas", "Save the draft to keep this change."); + }, [canManage, hiddenAreaCount, remember, toast, updateObjects]); + + const handleObjectField = useCallback((field, value) => { + if (!selectedObject) return; + remember(); + updateObjects((currentObjects) => + currentObjects.map((object) => ( + getObjectKey(object) === getObjectKey(selectedObject) ? { ...object, [field]: value } : object + )) + ); + }, [remember, selectedObject, updateObjects]); + + const handleZoneLinkChange = useCallback((zoneId) => { + const zone = zones.find((candidate) => String(candidate.id) === String(zoneId)); + handleObjectField("zone_id", zone?.id || null); + setObjects((currentObjects) => + currentObjects.map((object) => ( + getObjectKey(object) === selectedObjectKey + ? { ...object, zone_name: zone?.name || null, label: object.label ?? zone?.name ?? "" } + : object + )) + ); + }, [handleObjectField, selectedObjectKey, setObjects, zones]); + + return { + handleAddObject, + handleDeleteObject, + handleDuplicateObject, + handleObjectField, + handleShowHiddenAreas, + handleZoneLinkChange, + pendingDeleteObject, + remember, + requestDeleteObject, + setPendingDeleteObject, + updateObjects, + }; +} diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index 8c972c1..81771aa 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useRef } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { AuthContext } from "../context/AuthContext"; import { HouseholdContext } from "../context/HouseholdContext"; @@ -12,19 +12,17 @@ import ConfirmSlideModal from "../components/modals/ConfirmSlideModal"; import useActionToast from "../hooks/useActionToast"; import useLocationMapDraftActions from "../hooks/useLocationMapDraftActions"; import useLocationMapDraftState from "../hooks/useLocationMapDraftState"; +import useLocationMapObjectActions from "../hooks/useLocationMapObjectActions"; import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard"; import { DEFAULT_MAP_SIZE, clampMapZoom, - clampObjectToMap, - createClientObject, getMapObjectDisplayLabel, getMapStatus, getObjectKey, itemMatchesMapFilters, itemsForZone, mapForMode, - objectZoneId, unmappedItems, } from "../lib/locationMapUtils"; import "../styles/pages/LocationMapManager.css"; @@ -84,7 +82,6 @@ export default function LocationMapManager() { locationId, toast, }); - const [pendingDeleteObject, setPendingDeleteObject] = useState(null); const svgRef = useRef(null); const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId)); @@ -168,19 +165,6 @@ export default function LocationMapManager() { } }, [filters.showZones, objects, selectedObjectKey]); - const remember = (currentObjects = objects) => { - setHistory((previous) => [...previous.slice(-19), currentObjects.map((object) => ({ ...object }))]); - setFuture([]); - }; - - const updateObjects = (updater) => { - setHasUnsavedChanges(true); - setObjects((currentObjects) => { - const nextObjects = updater(currentObjects).map((object) => clampObjectToMap(object, mapSize)); - return nextObjects; - }); - }; - const { handleCreateBlank, handleCreateFromZones, @@ -202,95 +186,34 @@ export default function LocationMapManager() { toast, }); - const handleAddObject = () => { - if (!canManage) return; - remember(); - const usedZoneIds = new Set(objects.map((object) => objectZoneId(object)).filter(Boolean)); - const nextZone = (mapState?.zones || []).find((zone) => !usedZoneIds.has(String(zone.id))) || mapState?.zones?.[0]; - const nextObject = createClientObject(nextZone, objects.length); - setObjects((currentObjects) => [...currentObjects, nextObject]); - setSelectedObjectKey(getObjectKey(nextObject)); - setEditorTool("edit"); - setHasUnsavedChanges(true); - }; - - const handleDuplicateObject = () => { - if (!selectedObject) return; - remember(); - const displayLabel = getMapObjectDisplayLabel(selectedObject, "Area"); - const nextObject = { - ...selectedObject, - id: undefined, - client_id: `copy-${Date.now()}`, - label: `${displayLabel} Copy`, - x: selectedObject.x + 28, - y: selectedObject.y + 28, - sort_order: objects.length + 1, - }; - setObjects((currentObjects) => [...currentObjects, clampObjectToMap(nextObject, mapSize)]); - setSelectedObjectKey(getObjectKey(nextObject)); - setEditorTool("edit"); - setHasUnsavedChanges(true); - }; - - const requestDeleteObject = () => { - if (!selectedObject) return; - setPendingDeleteObject(selectedObject); - }; - - const handleDeleteObject = () => { - if (!pendingDeleteObject) return; - remember(); - setObjects((currentObjects) => - currentObjects.filter((object) => getObjectKey(object) !== getObjectKey(pendingDeleteObject)) - ); - setSelectedObjectKey(null); - setHasUnsavedChanges(true); - toast.success( - "Deleted map area", - "Save the draft to keep this change." - ); - setPendingDeleteObject(null); - }; - - const handleShowHiddenAreas = () => { - if (!canManage || hiddenAreaCount === 0) return; - remember(); - updateObjects((currentObjects) => - currentObjects.map((object) => - object.visible === false ? { ...object, visible: true } : object - ) - ); - toast.success("Showed hidden areas", "Save the draft to keep this change."); - }; - - const handleObjectField = (field, value) => { - if (!selectedObject) return; - remember(); - updateObjects((currentObjects) => - currentObjects.map((object) => - getObjectKey(object) === getObjectKey(selectedObject) - ? { ...object, [field]: value } - : object - ) - ); - }; - - const handleZoneLinkChange = (zoneId) => { - const zone = (mapState?.zones || []).find((candidate) => String(candidate.id) === String(zoneId)); - handleObjectField("zone_id", zone?.id || null); - setObjects((currentObjects) => - currentObjects.map((object) => - getObjectKey(object) === selectedObjectKey - ? { - ...object, - zone_name: zone?.name || null, - label: object.label ?? zone?.name ?? "", - } - : object - ) - ); - }; + const { + handleAddObject, + handleDeleteObject, + handleDuplicateObject, + handleObjectField, + handleShowHiddenAreas, + handleZoneLinkChange, + pendingDeleteObject, + remember, + requestDeleteObject, + setPendingDeleteObject, + updateObjects, + } = useLocationMapObjectActions({ + canManage, + hiddenAreaCount, + mapSize, + mapState, + objects, + selectedObject, + selectedObjectKey, + setEditorTool, + setFuture, + setHasUnsavedChanges, + setHistory, + setObjects, + setSelectedObjectKey, + toast, + }); const handleUndo = () => { setHistory((previous) => {