Add store location map manager #20

Open
nalalangan wants to merge 206 commits from feature/location-map-manager into feature/store-selector-modal
2 changed files with 267 additions and 189 deletions
Showing only changes of commit 505d9ce0f2 - Show all commits

View File

@ -2,22 +2,17 @@ import { useMemo } from "react";
import { import {
DEFAULT_MAP_FILTERS, DEFAULT_MAP_FILTERS,
getMapObjectDisplayLabel, getMapObjectDisplayLabel,
objectZoneId,
} from "../../lib/locationMapUtils"; } from "../../lib/locationMapUtils";
import {
LocationMapLayerPanel,
LocationMapOverview,
MAP_DISPLAY_CONTROLS,
SelectedMapObjectForm,
SelectedZoneItemsPanel,
UnmappedItemsPanel,
} from "./LocationMapBottomSheetSections";
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", "Bought"],
["showUnmapped", "Unmapped"],
];
const EMPTY_ITEMS = []; const EMPTY_ITEMS = [];
export default function LocationMapBottomSheet({ export default function LocationMapBottomSheet({
@ -87,7 +82,7 @@ export default function LocationMapBottomSheet({
const showEditorControls = mode === "edit" && canManage; const showEditorControls = mode === "edit" && canManage;
const showSelectedObjectForm = Boolean(mode === "edit" && editorTool === "edit" && selectedObject); const showSelectedObjectForm = Boolean(mode === "edit" && editorTool === "edit" && selectedObject);
const changedLayerCount = useMemo( const changedLayerCount = useMemo(
() => DISPLAY_CONTROLS.filter(([key]) => filters[key] !== DEFAULT_MAP_FILTERS[key]).length, () => MAP_DISPLAY_CONTROLS.filter(([key]) => filters[key] !== DEFAULT_MAP_FILTERS[key]).length,
[filters] [filters]
); );
const totalAreaCount = mapObjects.length; const totalAreaCount = mapObjects.length;
@ -102,11 +97,6 @@ export default function LocationMapBottomSheet({
[mapObjects] [mapObjects]
); );
const showDraftOverview = mode === "edit" && editorTool === "edit" && !selectedObject; const showDraftOverview = mode === "edit" && editorTool === "edit" && !selectedObject;
const unmappedItemPreview = useMemo(
() => visibleUnmappedItems.slice(0, 8),
[visibleUnmappedItems]
);
const overflowUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length;
const hiddenUnmappedItemCount = Math.max(0, unmappedItemCount - visibleUnmappedItems.length); const hiddenUnmappedItemCount = Math.max(0, unmappedItemCount - visibleUnmappedItems.length);
const hasHiddenUnmappedItems = const hasHiddenUnmappedItems =
filters.showUnmapped && unmappedItemCount > 0 && visibleUnmappedItems.length === 0; filters.showUnmapped && unmappedItemCount > 0 && visibleUnmappedItems.length === 0;
@ -211,23 +201,7 @@ export default function LocationMapBottomSheet({
</div> </div>
{layersOpen ? ( {layersOpen ? (
<div className="location-map-display-panel"> <LocationMapLayerPanel filters={filters} setFilters={setFilters} />
<div className="location-map-display-controls">
{DISPLAY_CONTROLS.map(([key, label]) => (
<button
key={key}
type="button"
className={filters[key] ? "active" : ""}
aria-pressed={filters[key]}
onClick={() =>
setFilters((current) => ({ ...current, [key]: !current[key] }))
}
>
{label}
</button>
))}
</div>
</div>
) : null} ) : null}
{showSelectedObjectForm && primaryDraftActions ? ( {showSelectedObjectForm && primaryDraftActions ? (
@ -249,128 +223,46 @@ export default function LocationMapBottomSheet({
) : null} ) : null}
{showMapOverview ? ( {showMapOverview ? (
<div className="location-map-overview" aria-label="Map summary"> <LocationMapOverview
<div className="location-map-overview-row"> ariaLabel="Map summary"
<span>Areas</span> rows={[
<strong>{filters.showZones ? `${visibleAreaCount} shown` : "Hidden"}</strong> { label: "Areas", value: filters.showZones ? `${visibleAreaCount} shown` : "Hidden" },
</div> { label: "Mapped items", value: mappedItemSummary },
<div className="location-map-overview-row"> { label: "Unmapped", value: unmappedItemSummary },
<span>Mapped items</span> ]}
<strong>{mappedItemSummary}</strong> />
</div>
<div className="location-map-overview-row">
<span>Unmapped</span>
<strong>{unmappedItemSummary}</strong>
</div>
</div>
) : null} ) : null}
{showDraftOverview ? ( {showDraftOverview ? (
<div className="location-map-overview" aria-label="Draft map summary"> <LocationMapOverview
<div className="location-map-overview-row"> ariaLabel="Draft map summary"
<span>Areas</span> rows={[
<strong>{totalAreaCount}</strong> { label: "Areas", value: totalAreaCount },
</div> { label: "Hidden", value: hiddenAreaCount },
<div className="location-map-overview-row"> { label: "Unlinked", value: unlinkedAreaCount },
<span>Hidden</span> ]}
<strong>{hiddenAreaCount}</strong> />
</div>
<div className="location-map-overview-row">
<span>Unlinked</span>
<strong>{unlinkedAreaCount}</strong>
</div>
</div>
) : null} ) : null}
{mode === "edit" && editorTool === "edit" && selectedObject ? ( {mode === "edit" && editorTool === "edit" && selectedObject ? (
<div className="location-map-object-form"> <SelectedMapObjectForm
<label className="location-map-field-row"> selectedObject={selectedObject}
<span>Label</span> zones={mapState?.zones || EMPTY_ITEMS}
<input saving={saving}
value={selectedObject.label || ""} onObjectField={onObjectField}
disabled={saving} onZoneLinkChange={onZoneLinkChange}
onChange={(event) => onObjectField("label", event.target.value)} onDuplicateObject={onDuplicateObject}
/> onDeleteObject={onDeleteObject}
</label> />
<label className="location-map-field-row">
<span>Zone</span>
<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-flags">
<button
type="button"
className={selectedObject.visible !== false ? "active" : ""}
aria-pressed={selectedObject.visible !== false}
disabled={saving}
onClick={() => onObjectField("visible", selectedObject.visible === false)}
>
Visible
</button>
<button
type="button"
className={selectedObject.locked ? "active" : ""}
aria-pressed={Boolean(selectedObject.locked)}
disabled={saving}
onClick={() => onObjectField("locked", !selectedObject.locked)}
>
Locked
</button>
</div>
<div className="location-map-editor-actions">
<button type="button" onClick={onDuplicateObject} disabled={saving}>Duplicate</button>
<button type="button" className="danger" onClick={onDeleteObject} disabled={saving}>Delete</button>
</div>
</div>
) : mode === "edit" ? ( ) : mode === "edit" ? (
null null
) : selectedObject ? ( ) : selectedObject ? (
<div className="location-map-zone-items"> <SelectedZoneItemsPanel
{selectedZoneItems.length === 0 ? ( selectedZoneItems={selectedZoneItems}
<div className="location-map-zone-empty"> hasHiddenSelectedItems={hasHiddenSelectedItems}
<p className="location-map-muted"> hiddenSelectedItemCount={hiddenSelectedItemCount}
{hasHiddenSelectedItems onShowSelectedZoneItems={showSelectedZoneItems}
? "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>
{hasHiddenSelectedItems ? (
<div className="location-map-zone-hidden-note">
<p className="location-map-muted">
{hiddenSelectedItemCount} more hidden by layer filters.
</p>
<button type="button" onClick={showSelectedZoneItems}>
Show All Zone Items
</button>
</div>
) : null}
</>
)}
</div>
) : null} ) : null}
{showSelectedObjectForm && secondaryDraftActions ? ( {showSelectedObjectForm && secondaryDraftActions ? (
@ -383,45 +275,13 @@ export default function LocationMapBottomSheet({
</div> </div>
) : null} ) : null}
{filters.showUnmapped && visibleUnmappedItems.length > 0 ? ( {filters.showUnmapped ? (
<div className="location-map-unmapped-list"> <UnmappedItemsPanel
<strong>Unmapped Items</strong> visibleUnmappedItems={visibleUnmappedItems}
<ul> hiddenUnmappedItemCount={hiddenUnmappedItemCount}
{unmappedItemPreview.map((item) => ( hasHiddenUnmappedItems={hasHiddenUnmappedItems}
<li key={item.id}>{item.item_name}</li> onShowUnmappedItems={showUnmappedItems}
))} />
{overflowUnmappedItemCount > 0 ? (
<li
className="location-map-unmapped-more"
aria-label={`${overflowUnmappedItemCount} more unmapped items`}
>
+{overflowUnmappedItemCount} more
</li>
) : null}
</ul>
{hiddenUnmappedItemCount > 0 ? (
<div className="location-map-zone-hidden-note">
<p className="location-map-muted">
{hiddenUnmappedItemCount} more hidden by layer filters.
</p>
<button type="button" onClick={showUnmappedItems}>
Show All Unmapped Items
</button>
</div>
) : null}
</div>
) : hasHiddenUnmappedItems ? (
<div className="location-map-unmapped-list">
<strong>Unmapped Items</strong>
<div className="location-map-zone-empty">
<p className="location-map-muted">
Unmapped items are hidden by layer filters.
</p>
<button type="button" onClick={showUnmappedItems}>
Show Unmapped Items
</button>
</div>
</div>
) : null} ) : null}
</aside> </aside>
); );

View File

@ -0,0 +1,218 @@
import { objectZoneId } from "../../lib/locationMapUtils";
export const MAP_DISPLAY_CONTROLS = [
["showZones", "Zones"],
["showLabels", "Labels"],
["showCounts", "Counts"],
["showPins", "Pins"],
["showMyItems", "Mine"],
["showOtherItems", "Others"],
["showCompleted", "Bought"],
["showUnmapped", "Unmapped"],
];
const UNMAPPED_PREVIEW_LIMIT = 8;
export function LocationMapLayerPanel({ filters, setFilters }) {
return (
<div className="location-map-display-panel">
<div className="location-map-display-controls">
{MAP_DISPLAY_CONTROLS.map(([key, label]) => (
<button
key={key}
type="button"
className={filters[key] ? "active" : ""}
aria-pressed={filters[key]}
onClick={() =>
setFilters((current) => ({ ...current, [key]: !current[key] }))
}
>
{label}
</button>
))}
</div>
</div>
);
}
export function LocationMapOverview({ ariaLabel, rows }) {
return (
<div className="location-map-overview" aria-label={ariaLabel}>
{rows.map((row) => (
<div className="location-map-overview-row" key={row.label}>
<span>{row.label}</span>
<strong>{row.value}</strong>
</div>
))}
</div>
);
}
export function SelectedMapObjectForm({
selectedObject,
zones = [],
saving,
onObjectField,
onZoneLinkChange,
onDuplicateObject,
onDeleteObject,
}) {
return (
<div className="location-map-object-form">
<label className="location-map-field-row">
<span>Label</span>
<input
value={selectedObject.label || ""}
disabled={saving}
onChange={(event) => onObjectField("label", event.target.value)}
/>
</label>
<label className="location-map-field-row">
<span>Zone</span>
<select
aria-label="Linked zone"
value={objectZoneId(selectedObject)}
disabled={saving}
onChange={(event) => onZoneLinkChange(event.target.value)}
>
<option value="">No linked zone</option>
{zones.map((zone) => (
<option key={zone.id} value={zone.id}>{zone.name}</option>
))}
</select>
</label>
<div className="location-map-object-flags">
<button
type="button"
className={selectedObject.visible !== false ? "active" : ""}
aria-pressed={selectedObject.visible !== false}
disabled={saving}
onClick={() => onObjectField("visible", selectedObject.visible === false)}
>
Visible
</button>
<button
type="button"
className={selectedObject.locked ? "active" : ""}
aria-pressed={Boolean(selectedObject.locked)}
disabled={saving}
onClick={() => onObjectField("locked", !selectedObject.locked)}
>
Locked
</button>
</div>
<div className="location-map-editor-actions">
<button type="button" onClick={onDuplicateObject} disabled={saving}>Duplicate</button>
<button type="button" className="danger" onClick={onDeleteObject} disabled={saving}>Delete</button>
</div>
</div>
);
}
export function SelectedZoneItemsPanel({
selectedZoneItems,
hasHiddenSelectedItems,
hiddenSelectedItemCount,
onShowSelectedZoneItems,
}) {
if (selectedZoneItems.length === 0) {
return (
<div className="location-map-zone-items">
<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={onShowSelectedZoneItems}>
Show Zone Items
</button>
) : null}
</div>
</div>
);
}
return (
<div className="location-map-zone-items">
<ul>
{selectedZoneItems.map((item) => (
<li key={item.id}>
<span>{item.item_name}</span>
<small>x{item.quantity}</small>
</li>
))}
</ul>
{hasHiddenSelectedItems ? (
<div className="location-map-zone-hidden-note">
<p className="location-map-muted">
{hiddenSelectedItemCount} more hidden by layer filters.
</p>
<button type="button" onClick={onShowSelectedZoneItems}>
Show All Zone Items
</button>
</div>
) : null}
</div>
);
}
export function UnmappedItemsPanel({
visibleUnmappedItems,
hiddenUnmappedItemCount,
hasHiddenUnmappedItems,
onShowUnmappedItems,
}) {
if (visibleUnmappedItems.length === 0 && hasHiddenUnmappedItems) {
return (
<div className="location-map-unmapped-list">
<strong>Unmapped Items</strong>
<div className="location-map-zone-empty">
<p className="location-map-muted">
Unmapped items are hidden by layer filters.
</p>
<button type="button" onClick={onShowUnmappedItems}>
Show Unmapped Items
</button>
</div>
</div>
);
}
if (visibleUnmappedItems.length === 0) {
return null;
}
const unmappedItemPreview = visibleUnmappedItems.slice(0, UNMAPPED_PREVIEW_LIMIT);
const overflowUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length;
return (
<div className="location-map-unmapped-list">
<strong>Unmapped Items</strong>
<ul>
{unmappedItemPreview.map((item) => (
<li key={item.id}>{item.item_name}</li>
))}
{overflowUnmappedItemCount > 0 ? (
<li
className="location-map-unmapped-more"
aria-label={`${overflowUnmappedItemCount} more unmapped items`}
>
+{overflowUnmappedItemCount} more
</li>
) : null}
</ul>
{hiddenUnmappedItemCount > 0 ? (
<div className="location-map-zone-hidden-note">
<p className="location-map-muted">
{hiddenUnmappedItemCount} more hidden by layer filters.
</p>
<button type="button" onClick={onShowUnmappedItems}>
Show All Unmapped Items
</button>
</div>
) : null}
</div>
);
}