backup: save fixed office seatmap snapshot
This commit is contained in:
@@ -31,6 +31,7 @@ const seatMapFormGap = document.getElementById("seatmap-form-gap");
|
||||
const seatMapFormImage = document.getElementById("seatmap-form-image");
|
||||
const seatMapSearch = document.getElementById("seatmap-search");
|
||||
const seatMapUnassigned = document.getElementById("seatmap-unassigned");
|
||||
const APP_BASE_URL = String(window.__MH_BASE_URL || "").replace(/\/$/, "");
|
||||
|
||||
const viewLabels = {
|
||||
ledger: "사업관리대장",
|
||||
@@ -60,6 +61,16 @@ const seatMapState = {
|
||||
panStartY: 0,
|
||||
panScrollLeft: 0,
|
||||
panScrollTop: 0,
|
||||
hoveredSlotId: null,
|
||||
viewerOffsetX: 0,
|
||||
viewerOffsetY: 0,
|
||||
viewerPointerX: 0,
|
||||
viewerPointerY: 0,
|
||||
viewerDragging: false,
|
||||
viewerDragStartX: 0,
|
||||
viewerDragStartY: 0,
|
||||
viewerDragOffsetX: 0,
|
||||
viewerDragOffsetY: 0,
|
||||
};
|
||||
|
||||
let currentView = "organization";
|
||||
@@ -101,6 +112,14 @@ function escapeHtml(value) {
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function resolveAppUrl(path) {
|
||||
if (!path) return path;
|
||||
if (/^https?:\/\//i.test(path)) return path;
|
||||
if (!APP_BASE_URL) return path;
|
||||
if (path.startsWith("/")) return `${APP_BASE_URL}${path}`;
|
||||
return path;
|
||||
}
|
||||
|
||||
function clonePlacements(items) {
|
||||
return items.map((item) => ({
|
||||
member_id: Number(item.member_id),
|
||||
@@ -149,7 +168,7 @@ function resetSeatMapDraft() {
|
||||
}
|
||||
|
||||
function clampSeatMapZoom(nextZoom) {
|
||||
return Math.min(3, Math.max(0.5, Number(nextZoom.toFixed(2))));
|
||||
return Math.min(4, Math.max(0.35, Number(nextZoom.toFixed(2))));
|
||||
}
|
||||
|
||||
function setSeatMapZoom(nextZoom) {
|
||||
@@ -157,6 +176,261 @@ function setSeatMapZoom(nextZoom) {
|
||||
renderSeatMap();
|
||||
}
|
||||
|
||||
function getDxfCanvasSize() {
|
||||
return {
|
||||
width: Math.max(960, seatMapBoardWrap?.clientWidth || seatMapBoard?.clientWidth || 960),
|
||||
height: Math.max(680, seatMapBoardWrap?.clientHeight || seatMapBoard?.clientHeight || 680),
|
||||
};
|
||||
}
|
||||
|
||||
function centerSeatMapBoard() {
|
||||
fitDxfSeatMapBoard();
|
||||
}
|
||||
|
||||
function fitDxfSeatMapBoard() {
|
||||
const viewerData = seatMapState.seatMap?.viewer_data;
|
||||
if (!viewerData) return;
|
||||
const world = viewerData.meta?.world;
|
||||
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
|
||||
if (!world || !canvas) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const pad = 36;
|
||||
const scaleX = (rect.width - pad * 2) / Math.max(Number(world.width || 1), 1);
|
||||
const scaleY = (rect.height - pad * 2) / Math.max(Number(world.height || 1), 1);
|
||||
seatMapState.zoom = clampSeatMapZoom(Math.min(scaleX, scaleY));
|
||||
seatMapState.viewerOffsetX =
|
||||
pad - Number(world.min_x) * seatMapState.zoom + (rect.width - pad * 2 - Number(world.width) * seatMapState.zoom) / 2;
|
||||
seatMapState.viewerOffsetY =
|
||||
pad - Number(world.min_y) * seatMapState.zoom + (rect.height - pad * 2 - Number(world.height) * seatMapState.zoom) / 2;
|
||||
drawDxfCanvasViewer();
|
||||
}
|
||||
|
||||
function zoomDxfSeatMapAtPoint(clientX, clientY, factor) {
|
||||
const viewerData = seatMapState.seatMap?.viewer_data;
|
||||
const world = viewerData?.meta?.world;
|
||||
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
|
||||
if (!viewerData || !world || !canvas) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = clientX - rect.left;
|
||||
const my = clientY - rect.top;
|
||||
const before = screenToWorld(mx, my, world);
|
||||
const nextZoom = clampSeatMapZoom(seatMapState.zoom * factor);
|
||||
if (nextZoom === seatMapState.zoom) return;
|
||||
seatMapState.zoom = nextZoom;
|
||||
const after = worldToScreen(before.x, before.y, world);
|
||||
seatMapState.viewerOffsetX += mx - after.x;
|
||||
seatMapState.viewerOffsetY += my - after.y;
|
||||
drawDxfCanvasViewer();
|
||||
}
|
||||
|
||||
function getHoveredSeatMapSlotMeta() {
|
||||
if (seatMapState.hoveredSlotId == null) return null;
|
||||
const slot = getSeatSlotMap().get(Number(seatMapState.hoveredSlotId));
|
||||
if (!slot) return null;
|
||||
const placement = getSlotPlacementMap().get(Number(slot.id));
|
||||
const member = placement ? getMemberMap().get(Number(placement.member_id)) : null;
|
||||
return {
|
||||
label: slot.label || `SLOT-${slot.id}`,
|
||||
memberName: member?.name || "",
|
||||
};
|
||||
}
|
||||
|
||||
function updateSeatMapViewerHoverChip() {
|
||||
const chip = seatMapBoard?.querySelector("[data-seatmap-chip='hover']");
|
||||
if (!chip) return;
|
||||
const hoveredMeta = getHoveredSeatMapSlotMeta();
|
||||
chip.textContent = hoveredMeta
|
||||
? `hover ${hoveredMeta.label}${hoveredMeta.memberName ? ` · ${hoveredMeta.memberName}` : ""}`
|
||||
: "hover none";
|
||||
}
|
||||
|
||||
function worldToScreen(x, y, world) {
|
||||
return {
|
||||
x: x * seatMapState.zoom + seatMapState.viewerOffsetX,
|
||||
y: (Number(world.max_y) - y + Number(world.min_y)) * seatMapState.zoom + seatMapState.viewerOffsetY,
|
||||
};
|
||||
}
|
||||
|
||||
function screenToWorld(x, y, world) {
|
||||
return {
|
||||
x: (x - seatMapState.viewerOffsetX) / seatMapState.zoom,
|
||||
y: Number(world.max_y) + Number(world.min_y) - (y - seatMapState.viewerOffsetY) / seatMapState.zoom,
|
||||
};
|
||||
}
|
||||
|
||||
function pickViewerChair(screenX, screenY, viewerData) {
|
||||
const world = viewerData.meta.world;
|
||||
const threshold = 12;
|
||||
let best = null;
|
||||
|
||||
for (const chair of viewerData.chairs || []) {
|
||||
const min = worldToScreen(Number(chair.min_x), Number(chair.max_y), world);
|
||||
const max = worldToScreen(Number(chair.max_x), Number(chair.min_y), world);
|
||||
const left = Math.min(min.x, max.x) - threshold;
|
||||
const right = Math.max(min.x, max.x) + threshold;
|
||||
const top = Math.min(min.y, max.y) - threshold;
|
||||
const bottom = Math.max(min.y, max.y) + threshold;
|
||||
if (screenX < left || screenX > right || screenY < top || screenY > bottom) continue;
|
||||
|
||||
let dist = Infinity;
|
||||
for (let index = Number(chair.start); index < Number(chair.start) + Number(chair.count); index += 1) {
|
||||
const segment = viewerData.chair_segments[index];
|
||||
if (!segment) continue;
|
||||
const a = worldToScreen(Number(segment[0]), Number(segment[1]), world);
|
||||
const b = worldToScreen(Number(segment[2]), Number(segment[3]), world);
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
let segDist;
|
||||
if (len2 === 0) {
|
||||
segDist = Math.hypot(screenX - a.x, screenY - a.y);
|
||||
} else {
|
||||
let t = ((screenX - a.x) * dx + (screenY - a.y) * dy) / len2;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const px = a.x + t * dx;
|
||||
const py = a.y + t * dy;
|
||||
segDist = Math.hypot(screenX - px, screenY - py);
|
||||
}
|
||||
if (segDist < dist) dist = segDist;
|
||||
if (dist <= threshold) break;
|
||||
}
|
||||
|
||||
if (dist > threshold) continue;
|
||||
if (!best || dist < best.dist) {
|
||||
best = { chair, dist };
|
||||
}
|
||||
}
|
||||
|
||||
return best?.chair || null;
|
||||
}
|
||||
|
||||
function drawViewerSegments(ctx, viewerData) {
|
||||
const world = viewerData.meta.world;
|
||||
const bgSegments = viewerData.background_segments || [];
|
||||
const chairSegments = viewerData.chair_segments || [];
|
||||
const placementMap = getSlotPlacementMap();
|
||||
const slotByKey = new Map((seatMapState.slots || []).map((slot) => [String(slot.slot_key), slot]));
|
||||
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(21,35,48,0.10)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let index = 0; index < bgSegments.length; index += 1) {
|
||||
const segment = bgSegments[index];
|
||||
const a = worldToScreen(Number(segment[0]), Number(segment[1]), world);
|
||||
const b = worldToScreen(Number(segment[2]), Number(segment[3]), world);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.x, a.y);
|
||||
ctx.lineTo(b.x, b.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
for (const chair of viewerData.chairs || []) {
|
||||
const slot = slotByKey.get(String(chair.key));
|
||||
const occupied = slot ? placementMap.has(Number(slot.id)) : false;
|
||||
const hovered = slot && Number(slot.id) === seatMapState.hoveredSlotId;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = occupied
|
||||
? "rgba(220, 38, 38, 0.98)"
|
||||
: hovered
|
||||
? "rgba(15, 118, 110, 0.98)"
|
||||
: "rgba(15, 118, 110, 0.82)";
|
||||
ctx.lineWidth = occupied ? 2.8 : hovered ? 2.2 : 1.5;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.beginPath();
|
||||
for (let index = Number(chair.start); index < Number(chair.start) + Number(chair.count); index += 1) {
|
||||
const segment = chairSegments[index];
|
||||
if (!segment) continue;
|
||||
const a = worldToScreen(Number(segment[0]), Number(segment[1]), world);
|
||||
const b = worldToScreen(Number(segment[2]), Number(segment[3]), world);
|
||||
ctx.moveTo(a.x, a.y);
|
||||
ctx.lineTo(b.x, b.y);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function drawDxfCanvasViewer() {
|
||||
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
|
||||
const viewerData = seatMapState.seatMap?.viewer_data;
|
||||
if (!canvas || !viewerData) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.round(rect.width * pixelRatio);
|
||||
canvas.height = Math.round(rect.height * pixelRatio);
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
drawViewerSegments(ctx, viewerData);
|
||||
updateSeatMapViewerHoverChip();
|
||||
}
|
||||
|
||||
function setupDxfCanvasViewer() {
|
||||
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
|
||||
const viewerData = seatMapState.seatMap?.viewer_data;
|
||||
if (!canvas || !viewerData) return;
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
seatMapState.viewerDragging = true;
|
||||
seatMapState.viewerDragStartX = event.clientX;
|
||||
seatMapState.viewerDragStartY = event.clientY;
|
||||
seatMapState.viewerDragOffsetX = seatMapState.viewerOffsetX;
|
||||
seatMapState.viewerDragOffsetY = seatMapState.viewerOffsetY;
|
||||
canvas.classList.add("dragging");
|
||||
});
|
||||
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
seatMapState.viewerPointerX = event.clientX - rect.left;
|
||||
seatMapState.viewerPointerY = event.clientY - rect.top;
|
||||
if (seatMapState.viewerDragging) {
|
||||
seatMapState.viewerOffsetX = seatMapState.viewerDragOffsetX + (event.clientX - seatMapState.viewerDragStartX);
|
||||
seatMapState.viewerOffsetY = seatMapState.viewerDragOffsetY + (event.clientY - seatMapState.viewerDragStartY);
|
||||
drawDxfCanvasViewer();
|
||||
return;
|
||||
}
|
||||
const chair = pickViewerChair(seatMapState.viewerPointerX, seatMapState.viewerPointerY, viewerData);
|
||||
const slot = chair ? (seatMapState.slots || []).find((item) => String(item.slot_key) === String(chair.key)) : null;
|
||||
const nextSlotId = slot ? Number(slot.id) : null;
|
||||
if (nextSlotId !== seatMapState.hoveredSlotId) {
|
||||
seatMapState.hoveredSlotId = nextSlotId;
|
||||
drawDxfCanvasViewer();
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("pointerleave", () => {
|
||||
seatMapState.hoveredSlotId = null;
|
||||
drawDxfCanvasViewer();
|
||||
});
|
||||
|
||||
canvas.addEventListener("pointerup", () => {
|
||||
if (!seatMapState.viewerDragging) return;
|
||||
seatMapState.viewerDragging = false;
|
||||
canvas.classList.remove("dragging");
|
||||
});
|
||||
|
||||
canvas.addEventListener("click", (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const chair = pickViewerChair(event.clientX - rect.left, event.clientY - rect.top, viewerData);
|
||||
const slot = chair ? (seatMapState.slots || []).find((item) => String(item.slot_key) === String(chair.key)) : null;
|
||||
seatMapState.hoveredSlotId = slot ? Number(slot.id) : null;
|
||||
drawDxfCanvasViewer();
|
||||
});
|
||||
|
||||
canvas.addEventListener("wheel", (event) => {
|
||||
event.preventDefault();
|
||||
zoomDxfSeatMapAtPoint(event.clientX, event.clientY, event.deltaY < 0 ? 1.08 : 0.92);
|
||||
}, { passive: false });
|
||||
|
||||
fitDxfSeatMapBoard();
|
||||
}
|
||||
|
||||
function getSeatSlotMap() {
|
||||
return new Map((seatMapState.slots || []).map((slot) => [Number(slot.id), slot]));
|
||||
}
|
||||
@@ -278,7 +552,7 @@ function renderUnassignedMemberCard(member, draggable) {
|
||||
function renderSeatMapBoard() {
|
||||
if (!seatMapBoard || !seatMapState.seatMap) return;
|
||||
|
||||
if (seatMapState.seatMap.source_type === "dxf") {
|
||||
if (seatMapState.seatMap.source_type === "dxf" || seatMapState.seatMap.source_type === "fixed_html") {
|
||||
renderDxfSeatMapBoard();
|
||||
return;
|
||||
}
|
||||
@@ -315,43 +589,77 @@ function renderSeatMapBoard() {
|
||||
|
||||
function renderDxfSeatMapBoard() {
|
||||
if (!seatMapBoard || !seatMapState.seatMap) return;
|
||||
|
||||
const memberMap = getMemberMap();
|
||||
const placementMap = getSlotPlacementMap();
|
||||
const slots = Array.isArray(seatMapState.slots) ? seatMapState.slots : [];
|
||||
const editable = seatMapState.editMode && isAdmin();
|
||||
const minX = Number(seatMapState.seatMap.view_box_min_x || 0);
|
||||
const minY = Number(seatMapState.seatMap.view_box_min_y || 0);
|
||||
const width = Number(seatMapState.seatMap.view_box_width || 1);
|
||||
const height = Number(seatMapState.seatMap.view_box_height || 1);
|
||||
const previewSvg = seatMapState.seatMap.preview_svg || "";
|
||||
|
||||
const slotHtml = slots
|
||||
.map((slot) => {
|
||||
const slotId = Number(slot.id);
|
||||
const placement = placementMap.get(slotId);
|
||||
const member = placement ? memberMap.get(Number(placement.member_id)) : null;
|
||||
if (!member && !editable) {
|
||||
return "";
|
||||
}
|
||||
const left = ((Number(slot.x) - minX) / width) * 100;
|
||||
const top = (1 - (Number(slot.y) - minY) / height) * 100;
|
||||
return `
|
||||
<div class="seatmap-slot${placement ? " occupied" : ""}${editable ? " editable" : ""}${!member ? " empty" : ""}" data-slot-id="${slotId}" style="left:${left}%; top:${top}%;">
|
||||
${member ? renderMemberCard(member, editable) : ""}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const viewerData = seatMapState.seatMap.viewer_data;
|
||||
if (!viewerData) {
|
||||
seatMapBoard.innerHTML = `<div class="seatmap-empty-card"><strong>DXF 뷰어 데이터를 준비하지 못했습니다.</strong></div>`;
|
||||
return;
|
||||
}
|
||||
const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer`);
|
||||
seatMapBoard.innerHTML = `
|
||||
<div class="seatmap-dxf-canvas">
|
||||
<div class="seatmap-dxf-stage" style="transform: scale(${seatMapState.zoom}); --seatmap-zoom:${seatMapState.zoom};">
|
||||
<div class="seatmap-dxf-preview">${previewSvg}</div>
|
||||
<div class="seatmap-dxf-slots">${slotHtml}</div>
|
||||
</div>
|
||||
<div class="seatmap-dxf-frame-shell">
|
||||
<iframe
|
||||
id="seatmap-dxf-frame"
|
||||
class="seatmap-dxf-frame"
|
||||
src="${escapeHtml(viewerUrl)}"
|
||||
title="${escapeHtml(seatMapState.seatMap.name || "DXF Viewer")}"
|
||||
loading="eager"
|
||||
referrerpolicy="same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
`;
|
||||
setupSeatMapViewerFrame();
|
||||
}
|
||||
|
||||
function getDraftPlacedSlotKeys() {
|
||||
const slotMap = getSeatSlotMap();
|
||||
return (seatMapState.draftPlacements || [])
|
||||
.map((placement) => slotMap.get(Number(placement.seat_slot_id))?.slot_key)
|
||||
.filter(Boolean)
|
||||
.map((value) => String(value));
|
||||
}
|
||||
|
||||
function syncSeatMapViewerFrame() {
|
||||
const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
|
||||
if (!frame?.contentWindow) return;
|
||||
frame.contentWindow.postMessage(
|
||||
{ type: "seatmap-set-placed", keys: getDraftPlacedSlotKeys() },
|
||||
window.location.origin,
|
||||
);
|
||||
}
|
||||
|
||||
function setupSeatMapViewerFrame() {
|
||||
const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
|
||||
if (!frame) return;
|
||||
|
||||
frame.addEventListener("load", () => {
|
||||
syncSeatMapViewerFrame();
|
||||
if (!seatMapState.editMode) return;
|
||||
const frameWindow = frame.contentWindow;
|
||||
const frameDocument = frame.contentDocument;
|
||||
const canvas = frameDocument?.getElementById("canvas");
|
||||
if (!frameWindow || !frameDocument || !canvas || !frameWindow.__mhSeatmap) return;
|
||||
|
||||
canvas.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
});
|
||||
|
||||
canvas.addEventListener("drop", (event) => {
|
||||
event.preventDefault();
|
||||
const memberId = getDraggedMemberId(event);
|
||||
if (!memberId) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const picked = frameWindow.__mhSeatmap.pickChairAt(
|
||||
event.clientX - rect.left,
|
||||
event.clientY - rect.top,
|
||||
);
|
||||
if (!picked?.key) return;
|
||||
const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(picked.key));
|
||||
if (!matchedSlot) return;
|
||||
upsertDraftPlacementForSlot(memberId, Number(matchedSlot.id));
|
||||
renderSeatMap();
|
||||
});
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
function renderUnassignedMembers() {
|
||||
@@ -413,6 +721,7 @@ function syncSeatMapSettingsForm() {
|
||||
function renderSeatMap() {
|
||||
const hasSeatMap = Boolean(seatMapState.seatMap);
|
||||
const admin = isAdmin();
|
||||
const fixedViewerMap = seatMapState.seatMap?.source_type === "fixed_html";
|
||||
|
||||
if (seatMapName) {
|
||||
seatMapName.textContent = hasSeatMap ? seatMapState.seatMap.name : "자리배치도";
|
||||
@@ -422,7 +731,7 @@ function renderSeatMap() {
|
||||
seatMapStatus.dataset.tone = seatMapState.statusTone;
|
||||
}
|
||||
if (seatMapSettingsPanel) {
|
||||
seatMapSettingsPanel.classList.toggle("hidden", !admin);
|
||||
seatMapSettingsPanel.classList.toggle("hidden", !admin || fixedViewerMap);
|
||||
}
|
||||
if (seatMapSaveBtn) {
|
||||
seatMapSaveBtn.hidden = !admin || !hasSeatMap;
|
||||
@@ -461,7 +770,7 @@ function handleEmbeddedNavigationMessage(event) {
|
||||
}
|
||||
|
||||
async function fetchJson(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
const response = await fetch(resolveAppUrl(url), options);
|
||||
let payload = null;
|
||||
try {
|
||||
payload = await response.json();
|
||||
@@ -487,11 +796,15 @@ async function loadSeatMapData(force = false) {
|
||||
const activePayload = await fetchJson("/api/seat-maps/active");
|
||||
const activeSeatMap = activePayload.item;
|
||||
const layoutPayload = await fetchJson(`/api/seat-maps/${activeSeatMap.id}/layout`);
|
||||
seatMapState.seatMap = layoutPayload.seat_map;
|
||||
seatMapState.seatMap = {
|
||||
...(layoutPayload.seat_map || {}),
|
||||
viewer_data: layoutPayload.viewer_data || null,
|
||||
};
|
||||
seatMapState.members = Array.isArray(layoutPayload.members) ? layoutPayload.members : [];
|
||||
seatMapState.slots = Array.isArray(layoutPayload.slots) ? layoutPayload.slots : [];
|
||||
seatMapState.placements = clonePlacements(layoutPayload.placements || []);
|
||||
seatMapState.zoom = 1;
|
||||
seatMapState.hoveredSlotId = null;
|
||||
seatMapState.editMode = isAdmin();
|
||||
resetSeatMapDraft();
|
||||
seatMapState.loaded = true;
|
||||
@@ -504,6 +817,7 @@ async function loadSeatMapData(force = false) {
|
||||
seatMapState.slots = [];
|
||||
seatMapState.placements = [];
|
||||
seatMapState.zoom = 1;
|
||||
seatMapState.hoveredSlotId = null;
|
||||
seatMapState.editMode = isAdmin();
|
||||
resetSeatMapDraft();
|
||||
seatMapState.loaded = true;
|
||||
@@ -615,8 +929,20 @@ function handleSeatMapCellDrop(event) {
|
||||
if (!memberId) return;
|
||||
if (seatMapState.seatMap?.source_type === "dxf") {
|
||||
const slot = event.target.closest(".seatmap-slot");
|
||||
if (!slot) return;
|
||||
upsertDraftPlacementForSlot(memberId, Number(slot.dataset.slotId));
|
||||
if (slot) {
|
||||
upsertDraftPlacementForSlot(memberId, Number(slot.dataset.slotId));
|
||||
renderSeatMap();
|
||||
return;
|
||||
}
|
||||
const viewerData = seatMapState.seatMap.viewer_data;
|
||||
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
|
||||
if (!viewerData || !canvas) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const chair = pickViewerChair(event.clientX - rect.left, event.clientY - rect.top, viewerData);
|
||||
if (!chair) return;
|
||||
const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(chair.key));
|
||||
if (!matchedSlot) return;
|
||||
upsertDraftPlacementForSlot(memberId, Number(matchedSlot.id));
|
||||
} else {
|
||||
const cell = event.target.closest(".seatmap-cell");
|
||||
if (!cell) return;
|
||||
@@ -665,7 +991,7 @@ function setActiveView(view) {
|
||||
|
||||
if (isOrganization && previousView !== "organization" && organizationFrame) {
|
||||
const frameSrc = organizationFrame.dataset.src || organizationFrame.src;
|
||||
organizationFrame.src = frameSrc;
|
||||
organizationFrame.src = resolveAppUrl(frameSrc);
|
||||
}
|
||||
if (isSeatMap) {
|
||||
loadSeatMapData();
|
||||
@@ -784,13 +1110,17 @@ if (seatMapBoard) {
|
||||
seatMapBoard.addEventListener("wheel", (event) => {
|
||||
if (seatMapState.seatMap?.source_type !== "dxf") return;
|
||||
event.preventDefault();
|
||||
const delta = event.deltaY < 0 ? 0.1 : -0.1;
|
||||
setSeatMapZoom(seatMapState.zoom + delta);
|
||||
zoomDxfSeatMapAtPoint(event.clientX, event.clientY, event.deltaY < 0 ? 1.08 : 0.92);
|
||||
}, { passive: false });
|
||||
seatMapBoard.addEventListener("click", (event) => {
|
||||
const fitButton = event.target.closest("[data-seatmap-action='fit']");
|
||||
if (!fitButton) return;
|
||||
fitDxfSeatMapBoard();
|
||||
});
|
||||
seatMapBoard.addEventListener("dragover", (event) => {
|
||||
if (!seatMapState.editMode) return;
|
||||
const target = seatMapState.seatMap?.source_type === "dxf"
|
||||
? event.target.closest(".seatmap-slot")
|
||||
? (event.target.closest(".seatmap-slot") || event.target.closest("#seatmap-dxf-canvas"))
|
||||
: event.target.closest(".seatmap-cell");
|
||||
if (!target) return;
|
||||
event.preventDefault();
|
||||
@@ -802,7 +1132,9 @@ if (seatMapBoard) {
|
||||
if (seatMapBoardWrap) {
|
||||
seatMapBoardWrap.addEventListener("mousedown", (event) => {
|
||||
if (seatMapState.seatMap?.source_type !== "dxf") return;
|
||||
if (event.button !== 1) return;
|
||||
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
|
||||
if (event.button !== 0) return;
|
||||
if (event.target.closest(".seatmap-member-card, button, input, label")) return;
|
||||
event.preventDefault();
|
||||
seatMapState.panning = true;
|
||||
seatMapState.panStartX = event.clientX;
|
||||
@@ -811,6 +1143,21 @@ if (seatMapBoardWrap) {
|
||||
seatMapState.panScrollTop = seatMapBoardWrap.scrollTop;
|
||||
seatMapBoardWrap.classList.add("is-panning");
|
||||
});
|
||||
seatMapBoardWrap.addEventListener("mouseleave", () => {
|
||||
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
|
||||
if (seatMapState.hoveredSlotId == null) return;
|
||||
seatMapState.hoveredSlotId = null;
|
||||
updateSeatMapViewerHoverChip();
|
||||
});
|
||||
seatMapBoardWrap.addEventListener("mousemove", (event) => {
|
||||
if (seatMapState.seatMap?.source_type !== "dxf") return;
|
||||
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
|
||||
const slot = event.target.closest(".seatmap-slot");
|
||||
const nextSlotId = slot ? Number(slot.dataset.slotId) : null;
|
||||
if (nextSlotId === seatMapState.hoveredSlotId) return;
|
||||
seatMapState.hoveredSlotId = nextSlotId;
|
||||
updateSeatMapViewerHoverChip();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("mousemove", (event) => {
|
||||
@@ -858,3 +1205,12 @@ window.addEventListener("message", handleEmbeddedNavigationMessage);
|
||||
|
||||
setActiveView(currentView);
|
||||
renderAuth();
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
if (seatMapState.seatMap?.source_type !== "dxf" || currentView !== "seatmap") return;
|
||||
requestAnimationFrame(() => {
|
||||
if (seatMapState.zoom === 1) {
|
||||
centerSeatMapBoard();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user