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 (