384 lines
13 KiB
JavaScript
384 lines
13 KiB
JavaScript
import {
|
|
DEFAULT_MAP_FILTERS,
|
|
MAP_OBJECT_TYPES,
|
|
getMapObjectDisplayLabel,
|
|
} from "../../lib/locationMapUtils";
|
|
|
|
function objectZoneId(object) {
|
|
return object?.zone_id ? String(object.zone_id) : "";
|
|
}
|
|
|
|
const DISPLAY_CONTROLS = [
|
|
["showZones", "Zones"],
|
|
["showLabels", "Labels"],
|
|
["showCounts", "Counts"],
|
|
["showPins", "Pins"],
|
|
["showMyItems", "Mine"],
|
|
["showOtherItems", "Others"],
|
|
["showCompleted", "Done"],
|
|
["showUnmapped", "Unmapped"],
|
|
];
|
|
|
|
export default function LocationMapBottomSheet({
|
|
mode,
|
|
editorTool,
|
|
canManage,
|
|
filters,
|
|
setFilters,
|
|
layersOpen,
|
|
setLayersOpen,
|
|
selectedObject,
|
|
selectedZoneItems,
|
|
visibleUnmappedItems,
|
|
history,
|
|
future,
|
|
saving,
|
|
savingAction,
|
|
hasUnsavedChanges,
|
|
mapState,
|
|
mapSize,
|
|
onAddObject,
|
|
onUndo,
|
|
onRedo,
|
|
onSaveDraft,
|
|
onPublish,
|
|
onPreviewDraft,
|
|
onPanMode,
|
|
onEditObjects,
|
|
onEditMode,
|
|
onObjectField,
|
|
onZoneLinkChange,
|
|
onClearSelection,
|
|
onDuplicateObject,
|
|
onDeleteObject,
|
|
}) {
|
|
const selectedTitle = getMapObjectDisplayLabel(selectedObject);
|
|
const selectedAssignedItems = selectedObject?.zone_id
|
|
? (mapState?.items || []).filter((item) => String(item.zone_id || "") === objectZoneId(selectedObject))
|
|
: [];
|
|
const hasHiddenSelectedItems = selectedAssignedItems.length > 0 && selectedZoneItems.length === 0;
|
|
const selectedItemCountLabel = hasHiddenSelectedItems
|
|
? "0 shown"
|
|
: `${selectedZoneItems.length} item${selectedZoneItems.length === 1 ? "" : "s"}`;
|
|
const sheetTitle = mode === "edit"
|
|
? selectedObject && editorTool === "edit"
|
|
? selectedTitle
|
|
: editorTool === "edit"
|
|
? "Edit Objects"
|
|
: "Pan Mode"
|
|
: selectedObject
|
|
? selectedTitle
|
|
: "Map Details";
|
|
const sheetDescription = selectedObject && editorTool === "edit"
|
|
? "Move, resize, link, rename, or nudge with arrow keys."
|
|
: editorTool === "edit"
|
|
? "Tap an area to select it."
|
|
: "Drag and zoom without changing objects.";
|
|
const showHeaderItemCount = mode !== "edit" && Boolean(selectedObject);
|
|
const showEditorControls = mode === "edit" && canManage;
|
|
const showSelectedObjectForm = Boolean(mode === "edit" && editorTool === "edit" && selectedObject);
|
|
const changedLayerCount = DISPLAY_CONTROLS.filter(([key]) => filters[key] !== DEFAULT_MAP_FILTERS[key]).length;
|
|
const unmappedItemPreview = visibleUnmappedItems.slice(0, 8);
|
|
const hiddenUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length;
|
|
const selectedWidth = Number(selectedObject?.width) || 40;
|
|
const selectedHeight = Number(selectedObject?.height) || 40;
|
|
const maxX = Math.max(0, Math.round((mapSize?.width || selectedWidth) - selectedWidth));
|
|
const maxY = Math.max(0, Math.round((mapSize?.height || selectedHeight) - selectedHeight));
|
|
const maxWidth = Math.max(40, Math.round(mapSize?.width || selectedWidth));
|
|
const maxHeight = Math.max(40, Math.round(mapSize?.height || selectedHeight));
|
|
const saveDraftLabel = savingAction === "save" ? "Saving..." : "Save Draft";
|
|
const publishLabel = savingAction === "publish" ? "Publishing..." : "Publish";
|
|
const showSelectedZoneItems = () => {
|
|
setFilters((current) => ({
|
|
...current,
|
|
showMyItems: true,
|
|
showOtherItems: true,
|
|
showCompleted: true,
|
|
}));
|
|
};
|
|
const primaryDraftActions = showEditorControls ? (
|
|
<div className="location-map-editor-actions location-map-primary-actions">
|
|
<button type="button" className="primary" onClick={onSaveDraft} disabled={saving || !hasUnsavedChanges}>
|
|
{saveDraftLabel}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="primary"
|
|
onClick={onPublish}
|
|
disabled={saving || (!mapState?.draft_map && !hasUnsavedChanges)}
|
|
>
|
|
{publishLabel}
|
|
</button>
|
|
</div>
|
|
) : null;
|
|
const secondaryDraftActions = showEditorControls ? (
|
|
<div className="location-map-editor-actions location-map-secondary-actions">
|
|
<button type="button" onClick={onAddObject} disabled={saving}>Add Area</button>
|
|
<button type="button" onClick={onPreviewDraft} aria-label="Preview Draft" disabled={saving}>Preview</button>
|
|
<button type="button" onClick={onUndo} disabled={saving || history.length === 0}>Undo</button>
|
|
<button type="button" onClick={onRedo} disabled={saving || future.length === 0}>Redo</button>
|
|
</div>
|
|
) : null;
|
|
const editorActions = showEditorControls ? (
|
|
<div className="location-map-editor-action-stack" role="group" aria-label="Map draft actions">
|
|
{primaryDraftActions}
|
|
{secondaryDraftActions}
|
|
</div>
|
|
) : canManage && mapState?.published_map ? (
|
|
<div className="location-map-editor-actions">
|
|
<button type="button" onClick={onEditMode}>Edit Map</button>
|
|
</div>
|
|
) : null;
|
|
|
|
return (
|
|
<aside className="location-map-bottom-sheet">
|
|
<div className="location-map-sheet-header">
|
|
<div>
|
|
<div className="location-map-sheet-heading">
|
|
<strong>{sheetTitle}</strong>
|
|
{showHeaderItemCount ? (
|
|
<span className="location-map-sheet-count">{selectedItemCountLabel}</span>
|
|
) : null}
|
|
</div>
|
|
{mode === "edit" && canManage ? (
|
|
<span>{sheetDescription}</span>
|
|
) : null}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className={`location-map-layer-toggle ${layersOpen ? "active" : ""}`}
|
|
onClick={() => setLayersOpen((current) => !current)}
|
|
aria-expanded={layersOpen}
|
|
aria-label={changedLayerCount ? `Layers, ${changedLayerCount} changed` : "Layers"}
|
|
>
|
|
Layers
|
|
{changedLayerCount ? (
|
|
<span className="location-map-layer-count" aria-hidden="true">{changedLayerCount}</span>
|
|
) : null}
|
|
</button>
|
|
</div>
|
|
|
|
{layersOpen ? (
|
|
<div className="location-map-display-panel">
|
|
{changedLayerCount ? (
|
|
<button
|
|
type="button"
|
|
className="location-map-layer-reset"
|
|
onClick={() => setFilters({ ...DEFAULT_MAP_FILTERS })}
|
|
>
|
|
Reset
|
|
</button>
|
|
) : null}
|
|
<div className="location-map-display-controls">
|
|
{DISPLAY_CONTROLS.map(([key, label]) => (
|
|
<label key={key}>
|
|
<input
|
|
type="checkbox"
|
|
checked={filters[key]}
|
|
onChange={(event) =>
|
|
setFilters((current) => ({ ...current, [key]: event.target.checked }))
|
|
}
|
|
/>
|
|
{label}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{showSelectedObjectForm && primaryDraftActions ? (
|
|
<div
|
|
className="location-map-editor-action-stack location-map-selected-primary-actions"
|
|
role="group"
|
|
aria-label="Map draft actions"
|
|
>
|
|
{primaryDraftActions}
|
|
</div>
|
|
) : editorActions}
|
|
|
|
{showEditorControls ? (
|
|
<div className="location-map-mobile-tool-switch" aria-label="Edit tool">
|
|
<button
|
|
type="button"
|
|
className={[editorTool === "pan" ? "active" : "", "is-pan"].filter(Boolean).join(" ")}
|
|
onClick={onPanMode}
|
|
disabled={saving}
|
|
>
|
|
Pan
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={[editorTool === "edit" ? "active" : "", "is-edit"].filter(Boolean).join(" ")}
|
|
onClick={onEditObjects}
|
|
disabled={saving}
|
|
>
|
|
Objects
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
|
|
{mode === "edit" && editorTool === "edit" && selectedObject ? (
|
|
<div className="location-map-object-form">
|
|
<label>
|
|
Label
|
|
<input
|
|
value={selectedObject.label || ""}
|
|
disabled={saving}
|
|
onChange={(event) => onObjectField("label", event.target.value)}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Type
|
|
<select
|
|
aria-label="Object type"
|
|
value={selectedObject.type}
|
|
disabled={saving}
|
|
onChange={(event) => onObjectField("type", event.target.value)}
|
|
>
|
|
{MAP_OBJECT_TYPES.map((type) => (
|
|
<option key={type} value={type}>{type}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Zone
|
|
<select
|
|
aria-label="Linked zone"
|
|
value={objectZoneId(selectedObject)}
|
|
disabled={saving}
|
|
onChange={(event) => onZoneLinkChange(event.target.value)}
|
|
>
|
|
<option value="">No linked zone</option>
|
|
{(mapState?.zones || []).map((zone) => (
|
|
<option key={zone.id} value={zone.id}>{zone.name}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<div className="location-map-object-grid">
|
|
<label>
|
|
X
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
max={maxX}
|
|
step="1"
|
|
value={Math.round(selectedObject.x)}
|
|
disabled={saving}
|
|
onChange={(event) => onObjectField("x", Number(event.target.value))}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Y
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
max={maxY}
|
|
step="1"
|
|
value={Math.round(selectedObject.y)}
|
|
disabled={saving}
|
|
onChange={(event) => onObjectField("y", Number(event.target.value))}
|
|
/>
|
|
</label>
|
|
<label>
|
|
W
|
|
<input
|
|
type="number"
|
|
min="40"
|
|
max={maxWidth}
|
|
step="1"
|
|
value={Math.round(selectedObject.width)}
|
|
disabled={saving}
|
|
onChange={(event) => onObjectField("width", Number(event.target.value))}
|
|
/>
|
|
</label>
|
|
<label>
|
|
H
|
|
<input
|
|
type="number"
|
|
min="40"
|
|
max={maxHeight}
|
|
step="1"
|
|
value={Math.round(selectedObject.height)}
|
|
disabled={saving}
|
|
onChange={(event) => onObjectField("height", Number(event.target.value))}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<div className="location-map-editor-actions">
|
|
<button type="button" onClick={onClearSelection}>Done</button>
|
|
<button type="button" onClick={onDuplicateObject} disabled={saving}>Duplicate</button>
|
|
<button type="button" className="danger" onClick={onDeleteObject} disabled={saving}>Delete</button>
|
|
</div>
|
|
</div>
|
|
) : mode === "edit" && editorTool === "edit" ? (
|
|
<p className="location-map-muted">
|
|
Tap an area to edit its name, linked zone, size, and position.
|
|
</p>
|
|
) : mode === "edit" ? (
|
|
<p className="location-map-muted">
|
|
Use Pan Mode to move around the map. Switch to Objects when you want to edit areas.
|
|
</p>
|
|
) : selectedObject ? (
|
|
<div className="location-map-zone-items">
|
|
{selectedZoneItems.length === 0 ? (
|
|
<div className="location-map-zone-empty">
|
|
<p className="location-map-muted">
|
|
{hasHiddenSelectedItems
|
|
? "Items are assigned here, but hidden by layer filters."
|
|
: "No items assigned to this zone yet."}
|
|
</p>
|
|
{hasHiddenSelectedItems ? (
|
|
<button type="button" onClick={showSelectedZoneItems}>
|
|
Show Zone Items
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
) : (
|
|
<ul>
|
|
{selectedZoneItems.map((item) => (
|
|
<li key={item.id}>
|
|
<span>{item.item_name}</span>
|
|
<small>x{item.quantity}</small>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<p className="location-map-muted">
|
|
Tap a zone to view assigned items. Item pins stay hidden unless you turn them on in Layers.
|
|
</p>
|
|
)}
|
|
|
|
{showSelectedObjectForm && secondaryDraftActions ? (
|
|
<div
|
|
className="location-map-editor-action-stack location-map-selected-secondary-actions"
|
|
role="group"
|
|
aria-label="More map draft actions"
|
|
>
|
|
{secondaryDraftActions}
|
|
</div>
|
|
) : null}
|
|
|
|
{filters.showUnmapped && visibleUnmappedItems.length > 0 ? (
|
|
<div className="location-map-unmapped-list">
|
|
<strong>Unmapped Items</strong>
|
|
<ul>
|
|
{unmappedItemPreview.map((item) => (
|
|
<li key={item.id}>{item.item_name}</li>
|
|
))}
|
|
{hiddenUnmappedItemCount > 0 ? (
|
|
<li
|
|
className="location-map-unmapped-more"
|
|
aria-label={`${hiddenUnmappedItemCount} more unmapped items`}
|
|
>
|
|
+{hiddenUnmappedItemCount} more
|
|
</li>
|
|
) : null}
|
|
</ul>
|
|
</div>
|
|
) : null}
|
|
</aside>
|
|
);
|
|
}
|