import { useCallback, useState } from "react"; import { clampObjectToMap, createClientObject, getMapObjectDisplayLabel, getObjectKey, objectZoneId, } from "../lib/locationMapUtils"; const EMPTY_ZONES = []; const MAP_EDGE_GROW_PADDING = 360; function createHistorySnapshot(objects, selectedObjectKey) { return { objects: objects.map((object) => ({ ...object })), selectedObjectKey: selectedObjectKey ?? null, }; } export default function useLocationMapObjectActions({ canManage, hiddenAreaCount, mapSize, mapState, objects, selectedObject, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory, setMapDraft, setObjects, setSelectedObjectKey, toast, }) { const [pendingDeleteObject, setPendingDeleteObject] = useState(null); const zones = mapState?.zones || EMPTY_ZONES; const remember = useCallback((currentObjects = objects, currentSelectedObjectKey = selectedObjectKey) => { setHistory((previous) => [ ...previous.slice(-19), createHistorySnapshot(currentObjects, currentSelectedObjectKey), ]); setFuture([]); }, [objects, selectedObjectKey, setFuture, setHistory]); const updateObjects = useCallback((updater, bounds = mapSize) => { setHasUnsavedChanges(true); setObjects((currentObjects) => updater(currentObjects).map((object) => clampObjectToMap(object, bounds)) ); }, [mapSize, setHasUnsavedChanges, setObjects]); const expandMapToFitObject = useCallback((object) => { const nextWidth = Math.max( mapSize.width, Math.ceil(Number(object.x || 0) + Number(object.width || 0) + MAP_EDGE_GROW_PADDING) ); const nextHeight = Math.max( mapSize.height, Math.ceil(Number(object.y || 0) + Number(object.height || 0) + MAP_EDGE_GROW_PADDING) ); const nextMapSize = { width: nextWidth, height: nextHeight, }; if (nextWidth > mapSize.width || nextHeight > mapSize.height) { setMapDraft((currentDraft) => ({ ...currentDraft, width: Math.max(Number(currentDraft.width || 0), nextWidth), height: Math.max(Number(currentDraft.height || 0), nextHeight), })); setHasUnsavedChanges(true); } return nextMapSize; }, [mapSize.height, mapSize.width, setHasUnsavedChanges, setMapDraft]); 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 = clampObjectToMap(createClientObject(nextZone, objects.length), mapSize); setObjects((currentObjects) => [...currentObjects, nextObject]); setSelectedObjectKey(getObjectKey(nextObject)); setHasUnsavedChanges(true); }, [canManage, mapSize, objects, remember, 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)); setHasUnsavedChanges(true); }, [mapSize, objects.length, remember, selectedObject, 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; if (selectedObject[field] === value) return; remember(); updateObjects((currentObjects) => currentObjects.map((object) => ( getObjectKey(object) === getObjectKey(selectedObject) ? { ...object, [field]: value } : object )) ); }, [remember, selectedObject, updateObjects]); const handleZoneLinkChange = useCallback((zoneId) => { if (!selectedObject) return; const zone = zones.find((candidate) => String(candidate.id) === String(zoneId)); const nextZoneId = zone?.id || null; const nextZoneName = zone?.name || null; const nextLabel = selectedObject.label ?? zone?.name ?? ""; if ( (selectedObject.zone_id || null) === nextZoneId && (selectedObject.zone_name || null) === nextZoneName && selectedObject.label === nextLabel ) { return; } remember(); updateObjects((currentObjects) => currentObjects.map((object) => ( getObjectKey(object) === selectedObjectKey ? { ...object, zone_id: nextZoneId, zone_name: nextZoneName, label: object.label ?? zone?.name ?? "" } : object )) ); }, [remember, selectedObject, selectedObjectKey, updateObjects, zones]); return { handleAddObject, handleDeleteObject, handleDuplicateObject, handleObjectField, handleShowHiddenAreas, handleZoneLinkChange, expandMapToFitObject, pendingDeleteObject, remember, requestDeleteObject, setPendingDeleteObject, updateObjects, }; }