import { useCallback, useContext, useEffect, useRef } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { AuthContext } from "../context/AuthContext"; import { HouseholdContext } from "../context/HouseholdContext"; import { StoreContext } from "../context/StoreContext"; import LocationMapBottomSheet from "../components/maps/LocationMapBottomSheet"; import LocationMapCanvas from "../components/maps/LocationMapCanvas"; import LocationMapDialogs from "../components/maps/LocationMapDialogs"; import LocationMapSetupPanel from "../components/maps/LocationMapSetupPanel"; import { LocationMapLoadErrorState, LocationMapMessageState, } from "../components/maps/LocationMapStateViews"; import LocationMapToolbar from "../components/maps/LocationMapToolbar"; import LocationMapTopbar from "../components/maps/LocationMapTopbar"; import useActionToast from "../hooks/useActionToast"; import useLocationMapDerivedState from "../hooks/useLocationMapDerivedState"; import useLocationMapDraftActions from "../hooks/useLocationMapDraftActions"; import useLocationMapDraftState from "../hooks/useLocationMapDraftState"; import useLocationMapBuying from "../hooks/useLocationMapBuying"; import useLocationMapObjectActions from "../hooks/useLocationMapObjectActions"; import useLocationMapViewControls from "../hooks/useLocationMapViewControls"; import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard"; import { getMapStatus } from "../lib/locationMapUtils"; import "../styles/pages/LocationMapManager.css"; function isTextEditingTarget(target) { return Boolean(target?.closest?.("input, textarea, select, [contenteditable]")); } export default function LocationMapManager() { const { storeId, locationId } = useParams(); const navigate = useNavigate(); const routeLocation = useLocation(); const returnPath = routeLocation.state?.returnTo; const toast = useActionToast(); const { username } = useContext(AuthContext); const { activeHousehold, loading: householdLoading, hasLoaded } = useContext(HouseholdContext); const { stores, activeStore, setActiveStore } = useContext(StoreContext); const { beginSaving, dragState, endSaving, filters, future, hasUnsavedChanges, history, layersOpen, loadError, loadMap, loading, mapDraft, mapState, mode, objects, previewDraft, saving, savingAction, selectedObjectKey, setDragState, setFilters, setFuture, setHasUnsavedChanges, setHistory, setLayersOpen, setMapDraft, setMapState, setMode, setObjects, setPreviewDraft, setSelectedObjectKey, setZoom, syncMapDraftFromState, zoom, } = useLocationMapDraftState({ activeHousehold, hasLoaded, householdLoading, locationId, toast, }); const svgRef = useRef(null); const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId)); const canManage = Boolean(mapState?.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role)); const { hiddenAreaCount, mapItems, mapSize, selectedObject, selectedZoneItems, unmappedItemCount, visibleAssignedItemCount, visibleUnmappedItems, } = useLocationMapDerivedState({ filters, mapDraft, mapState, mode, objects, previewDraft, selectedObjectKey, username, }); const { buyModalItem, buyModalItems, handleMapBuyCancel, handleMapBuyConfirm, handleMapBuyNavigate, setBuyModalItem, } = useLocationMapBuying({ activeHouseholdId: activeHousehold?.id, locationId, selectedObject, selectedZoneItems, setMapState, toast, visibleUnmappedItems, }); const shouldGuardLeave = canManage && hasUnsavedChanges; useEffect(() => { const matchingLocation = stores.find((store) => String(store.id) === String(locationId)); if (matchingLocation && String(activeStore?.id) !== String(matchingLocation.id)) { setActiveStore(matchingLocation); } }, [activeStore?.id, locationId, setActiveStore, stores]); const { handleCreateBlank, handleCreateFromZones, handlePublish, handleSaveDraft, } = useLocationMapDraftActions({ activeHouseholdId: activeHousehold?.id, beginSaving, endSaving, hasUnsavedChanges, locationId, mapDraft, objects, selectedObjectKey, setMapState, setMode, setPreviewDraft, syncMapDraftFromState, toast, }); const { handleAddObject, handleDeleteObject, handleDuplicateObject, handleObjectField, handleShowHiddenAreas, handleZoneLinkChange, expandMapToFitObject, pendingDeleteObject, remember, requestDeleteObject, setPendingDeleteObject, updateObjects, } = useLocationMapObjectActions({ canManage, hiddenAreaCount, mapSize, mapState, objects, selectedObject, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory, setMapDraft, setObjects, setSelectedObjectKey, toast, }); useEffect(() => { setPendingDeleteObject(null); }, [activeHousehold?.id, locationId, setPendingDeleteObject]); const { handleEditMode, handleFitMap, handlePreviewDraft, handleRedo, handleUndo, handleViewMode, } = useLocationMapViewControls({ filters, future, hasUnsavedChanges, history, mapSize, mapState, mode, objects, previewDraft, saving, selectedObjectKey, setFuture, setHasUnsavedChanges, setHistory, setLayersOpen, setMode, setObjects, setPreviewDraft, setSelectedObjectKey, setZoom, svgRef, syncMapDraftFromState, zoom, }); const performBackNavigation = useCallback(() => { if (typeof returnPath === "string" && returnPath.startsWith("/")) { navigate(returnPath, { replace: true }); return; } navigate("/"); }, [navigate, returnPath]); const { pendingLeaveTarget, requestBackNavigation, clearPendingLeave, confirmPendingLeave, } = useUnsavedMapLeaveGuard({ shouldGuardLeave, navigate, performBackNavigation, }); useEffect(() => { const hasOpenModal = Boolean(buyModalItem || pendingDeleteObject || pendingLeaveTarget); if (!selectedObjectKey || layersOpen || hasOpenModal || typeof window === "undefined") { return undefined; } const handleKeyDown = (event) => { if ( event.defaultPrevented || event.key !== "Escape" || isTextEditingTarget(event.target) ) { return; } setSelectedObjectKey(null); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [buyModalItem, layersOpen, pendingDeleteObject, pendingLeaveTarget, selectedObjectKey, setSelectedObjectKey]); useEffect(() => { const hasOpenModal = Boolean(buyModalItem || pendingDeleteObject || pendingLeaveTarget); if (mode !== "edit" || !canManage || hasOpenModal || typeof window === "undefined") { return undefined; } const handleKeyDown = (event) => { const wantsSave = (event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "s"; if (!wantsSave) return; event.preventDefault(); if (saving || !hasUnsavedChanges) return; handleSaveDraft(); }; window.addEventListener("keydown", handleKeyDown, true); return () => window.removeEventListener("keydown", handleKeyDown, true); }, [ buyModalItem, canManage, handleSaveDraft, hasUnsavedChanges, mode, pendingDeleteObject, pendingLeaveTarget, saving, ]); useEffect(() => { const hasOpenModal = Boolean(buyModalItem || pendingDeleteObject || pendingLeaveTarget); if ( !selectedObjectKey || mode !== "edit" || !canManage || saving || layersOpen || hasOpenModal || typeof window === "undefined" ) { return undefined; } const handleKeyDown = (event) => { if ( event.defaultPrevented || !["Backspace", "Delete"].includes(event.key) || isTextEditingTarget(event.target) ) { return; } event.preventDefault(); requestDeleteObject(); }; window.addEventListener("keydown", handleKeyDown, true); return () => window.removeEventListener("keydown", handleKeyDown, true); }, [ buyModalItem, canManage, layersOpen, mode, pendingDeleteObject, pendingLeaveTarget, requestDeleteObject, saving, selectedObjectKey, ]); if (!hasLoaded || householdLoading || loading) { return ( ); } if (!activeHousehold) { return ( ); } if (loadError) { return ( ); } const hasAnyMap = Boolean(mapState?.draft_map || mapState?.published_map); const hasVisibleOrManageableMap = Boolean(mapState?.published_map || (mapState?.draft_map && canManage)); const status = getMapStatus(mapState, { mode, previewDraft, hasUnsavedChanges }); return (
{hasAnyMap && mode !== "setup" ? ( ) : null} {!hasAnyMap || (mode === "setup" && mapState?.draft_map && !mapState?.published_map) ? ( { setMode("edit"); setPreviewDraft(true); }} onPreview={handlePreviewDraft} onPublish={handlePublish} onCreateFromZones={handleCreateFromZones} onCreateBlank={handleCreateBlank} /> ) : ( <> setFilters((current) => ({ ...current, showZones: true, })) } onShowHiddenAreas={handleShowHiddenAreas} /> setSelectedObjectKey(null)} onSelectZoneItem={setBuyModalItem} onSelectUnmappedItem={setBuyModalItem} /> )}
setPendingDeleteObject(null)} onDeleteConfirm={handleDeleteObject} pendingLeaveTarget={pendingLeaveTarget} onLeaveClose={clearPendingLeave} onLeaveConfirm={confirmPendingLeave} />
); }