const sessionKey = "mh-dashboard-session"; const loginPanel = document.getElementById("login-panel"); const dashboardPanel = document.getElementById("dashboard-panel"); const loginForm = document.getElementById("login-form"); const loginMessage = document.getElementById("login-message"); const logoutBtn = document.getElementById("logout-btn"); const userBadge = document.getElementById("user-badge"); const userPopover = document.getElementById("user-popover"); const currentViewTitle = document.getElementById("current-view-title"); const navButtons = Array.from(document.querySelectorAll(".header-center [data-view]")); const organizationFrame = document.getElementById("organization-frame"); const organizationStage = document.getElementById("organization-stage"); const seatMapStage = document.getElementById("seatmap-stage"); const emptyStage = document.getElementById("empty-stage"); const seatMapName = document.getElementById("seatmap-name"); const seatMapStatus = document.getElementById("seatmap-status"); const seatMapSaveBtn = document.getElementById("seatmap-save-btn"); const seatMapCancelBtn = document.getElementById("seatmap-cancel-btn"); const seatMapBoardWrap = document.getElementById("seatmap-board-wrap"); const seatMapBoard = document.getElementById("seatmap-board"); const seatMapEmpty = document.getElementById("seatmap-empty"); const seatMapSettingsPanel = document.getElementById("seatmap-settings-panel"); const seatMapSettingsForm = document.getElementById("seatmap-settings-form"); const seatMapFormName = document.getElementById("seatmap-form-name"); const seatMapFileName = document.getElementById("seatmap-file-name"); const seatMapFormRows = document.getElementById("seatmap-form-rows"); const seatMapFormCols = document.getElementById("seatmap-form-cols"); 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 viewLabels = { ledger: "사업관리대장", project: "프로젝트별 분석", team: "팀/개인별 분석", organization: "조직 현황", seatmap: "조직 현황", }; const seatMapState = { loaded: false, loading: false, seatMap: null, members: [], slots: [], placements: [], draftPlacements: [], editMode: false, dirty: false, search: "", status: "", statusTone: "info", draggingMemberId: null, zoom: 1, panning: false, panStartX: 0, panStartY: 0, panScrollLeft: 0, panScrollTop: 0, }; let currentView = "organization"; function getSession() { try { return JSON.parse(sessionStorage.getItem(sessionKey) || "null"); } catch { return null; } } function setSession(session) { sessionStorage.setItem(sessionKey, JSON.stringify(session)); } function clearSession() { sessionStorage.removeItem(sessionKey); } function hideUserPopover() { userPopover?.classList.add("hidden"); } function toggleUserPopover() { userPopover?.classList.toggle("hidden"); } function isAdmin() { return getSession()?.user?.role === "admin"; } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function clonePlacements(items) { return items.map((item) => ({ member_id: Number(item.member_id), seat_slot_id: item.seat_slot_id == null ? null : Number(item.seat_slot_id), row_index: Number(item.row_index), col_index: Number(item.col_index), seat_label: item.seat_label || "", })); } function computeSeatLabel(rowIndex, colIndex) { let quotient = rowIndex; let rowLabel = ""; while (true) { const remainder = quotient % 26; rowLabel = String.fromCharCode(65 + remainder) + rowLabel; quotient = Math.floor(quotient / 26); if (quotient === 0) break; quotient -= 1; } return `${rowLabel}-${String(colIndex + 1).padStart(2, "0")}`; } function getInitials(name) { const trimmed = String(name || "").trim(); if (!trimmed) return "?"; return trimmed.slice(0, 2).toUpperCase(); } function getPlacementSource() { return seatMapState.editMode ? seatMapState.draftPlacements : seatMapState.placements; } function setSeatMapStatus(message, tone = "info") { seatMapState.status = message || ""; seatMapState.statusTone = tone; if (seatMapStatus) { seatMapStatus.textContent = seatMapState.status; seatMapStatus.dataset.tone = seatMapState.statusTone; } } function resetSeatMapDraft() { seatMapState.draftPlacements = clonePlacements(seatMapState.placements); seatMapState.dirty = false; } function clampSeatMapZoom(nextZoom) { return Math.min(3, Math.max(0.5, Number(nextZoom.toFixed(2)))); } function setSeatMapZoom(nextZoom) { seatMapState.zoom = clampSeatMapZoom(nextZoom); renderSeatMap(); } function getSeatSlotMap() { return new Map((seatMapState.slots || []).map((slot) => [Number(slot.id), slot])); } function getMemberMap() { return new Map(seatMapState.members.map((member) => [Number(member.id), member])); } function getUnassignedMembers() { const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id))); const keyword = seatMapState.search.trim().toLowerCase(); return seatMapState.members.filter((member) => { if (placedIds.has(Number(member.id))) return false; if (!keyword) return true; const haystack = `${member.name || ""} ${member.department || ""} ${member.team || ""}`.toLowerCase(); return haystack.includes(keyword); }); } function getCellPlacementMap() { const cellMap = new Map(); getPlacementSource().forEach((item) => { cellMap.set(`${item.row_index}:${item.col_index}`, item); }); return cellMap; } function getSlotPlacementMap() { const slotMap = new Map(); getPlacementSource().forEach((item) => { if (item.seat_slot_id != null) { slotMap.set(Number(item.seat_slot_id), item); } }); return slotMap; } function upsertDraftPlacement(memberId, rowIndex, colIndex) { const cellMap = getCellPlacementMap(); const existing = cellMap.get(`${rowIndex}:${colIndex}`); if (existing && Number(existing.member_id) !== Number(memberId)) { setSeatMapStatus("이미 사용 중인 칸입니다. 빈 칸으로 이동해주세요.", "error"); return; } const nextPlacements = seatMapState.draftPlacements.filter((item) => Number(item.member_id) !== Number(memberId)); nextPlacements.push({ member_id: Number(memberId), row_index: Number(rowIndex), col_index: Number(colIndex), seat_label: computeSeatLabel(rowIndex, colIndex), }); seatMapState.draftPlacements = nextPlacements.sort((left, right) => { if (left.row_index !== right.row_index) return left.row_index - right.row_index; return left.col_index - right.col_index; }); seatMapState.dirty = true; setSeatMapStatus("배치를 수정했습니다. 저장 버튼으로 반영하세요.", "info"); } function upsertDraftPlacementForSlot(memberId, seatSlotId) { const placementMap = getSlotPlacementMap(); const existing = placementMap.get(Number(seatSlotId)); if (existing && Number(existing.member_id) !== Number(memberId)) { setSeatMapStatus("이미 사용 중인 좌석입니다. 빈 좌석으로 이동해주세요.", "error"); return; } const seatSlot = getSeatSlotMap().get(Number(seatSlotId)); const nextPlacements = seatMapState.draftPlacements.filter((item) => Number(item.member_id) !== Number(memberId)); nextPlacements.push({ member_id: Number(memberId), seat_slot_id: Number(seatSlotId), row_index: 0, col_index: 0, seat_label: seatSlot?.label || `SLOT-${seatSlotId}`, }); seatMapState.draftPlacements = nextPlacements; seatMapState.dirty = true; setSeatMapStatus("배치를 수정했습니다. 저장 버튼으로 반영하세요.", "info"); } function removeDraftPlacement(memberId) { const before = seatMapState.draftPlacements.length; seatMapState.draftPlacements = seatMapState.draftPlacements.filter((item) => Number(item.member_id) !== Number(memberId)); if (seatMapState.draftPlacements.length !== before) { seatMapState.dirty = true; setSeatMapStatus("구성원을 미배치 목록으로 이동했습니다. 저장 버튼으로 반영하세요.", "info"); } } function renderMemberCard(member, draggable) { const photoUrl = member.photo_url ? escapeHtml(member.photo_url) : ""; const avatar = photoUrl ? `${escapeHtml(member.name)}` : `${escapeHtml(getInitials(member.name))}`; return `
${avatar} ${escapeHtml(member.name || "-")} ${escapeHtml(member.department || member.team || member.rank || "-")}
`; } function renderUnassignedMemberCard(member, draggable) { return `
${escapeHtml(member.name || "-")} ${escapeHtml(member.rank || "-")}
`; } function renderSeatMapBoard() { if (!seatMapBoard || !seatMapState.seatMap) return; if (seatMapState.seatMap.source_type === "dxf") { renderDxfSeatMapBoard(); return; } const memberMap = getMemberMap(); const placementMap = getCellPlacementMap(); const rows = Number(seatMapState.seatMap.grid_rows || 0); const cols = Number(seatMapState.seatMap.grid_cols || 0); const gap = Number(seatMapState.seatMap.cell_gap || 0); const editable = seatMapState.editMode && isAdmin(); const cells = []; for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) { for (let colIndex = 0; colIndex < cols; colIndex += 1) { const key = `${rowIndex}:${colIndex}`; const placement = placementMap.get(key); const member = placement ? memberMap.get(Number(placement.member_id)) : null; cells.push(`
${escapeHtml(computeSeatLabel(rowIndex, colIndex))} ${member ? renderMemberCard(member, editable) : ""}
`); } } seatMapBoard.innerHTML = `
${escapeHtml(seatMapState.seatMap.name)}
${cells.join("")}
`; } 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 `
${member ? renderMemberCard(member, editable) : ""}
`; }) .join(""); seatMapBoard.innerHTML = `
${previewSvg}
${slotHtml}
`; } function renderUnassignedMembers() { if (!seatMapUnassigned) return; const editable = seatMapState.editMode && isAdmin(); const members = getUnassignedMembers(); if (!members.length) { seatMapUnassigned.innerHTML = `
${seatMapState.search ? "검색 결과가 없습니다." : "미배치 인원이 없습니다."}
`; return; } seatMapUnassigned.innerHTML = members.map((member) => renderUnassignedMemberCard(member, editable)).join(""); } function renderSeatMapEmpty() { if (!seatMapEmpty) return; if (seatMapState.seatMap) { seatMapEmpty.classList.add("hidden"); seatMapEmpty.innerHTML = ""; return; } seatMapEmpty.classList.remove("hidden"); seatMapEmpty.innerHTML = `
등록된 자리배치도가 없습니다.

${isAdmin() ? "오른쪽 설정 패널에서 이미지와 그리드를 등록하세요." : "관리자에게 자리배치도 등록을 요청하세요."}

`; } function syncSeatMapSettingsForm() { if (!seatMapSettingsForm) return; if (seatMapFormName) { seatMapFormName.value = seatMapState.seatMap?.name || ""; } if (seatMapFormRows) { seatMapFormRows.value = seatMapState.seatMap?.grid_rows || 12; } if (seatMapFormCols) { seatMapFormCols.value = seatMapState.seatMap?.grid_cols || 24; } if (seatMapFormGap) { seatMapFormGap.value = seatMapState.seatMap?.cell_gap ?? 2; } if (seatMapFormImage) { seatMapFormImage.value = ""; } if (seatMapFileName) { seatMapFileName.textContent = "선택된 파일 없음"; } } function renderSeatMap() { const hasSeatMap = Boolean(seatMapState.seatMap); const admin = isAdmin(); if (seatMapName) { seatMapName.textContent = hasSeatMap ? seatMapState.seatMap.name : "자리배치도"; } if (seatMapStatus) { seatMapStatus.textContent = seatMapState.status; seatMapStatus.dataset.tone = seatMapState.statusTone; } if (seatMapSettingsPanel) { seatMapSettingsPanel.classList.toggle("hidden", !admin); } if (seatMapSaveBtn) { seatMapSaveBtn.hidden = !admin || !hasSeatMap; seatMapSaveBtn.disabled = !seatMapState.dirty; } if (seatMapCancelBtn) { seatMapCancelBtn.hidden = !hasSeatMap; } if (seatMapSettingsForm) { seatMapSettingsForm.querySelector("button[type='submit']").textContent = hasSeatMap ? "배치도 저장" : "배치도 생성"; } renderSeatMapEmpty(); if (seatMapBoardWrap) { seatMapBoardWrap.classList.toggle("hidden", !hasSeatMap); } if (hasSeatMap) { renderSeatMapBoard(); } else if (seatMapBoard) { seatMapBoard.innerHTML = ""; } renderUnassignedMembers(); } function handleEmbeddedNavigationMessage(event) { const data = event.data; if (!data || typeof data !== "object") return; if (data.type === "open-seatmap" && isAdmin()) { hideUserPopover(); setActiveView("seatmap"); } if (data.type === "open-organization") { hideUserPopover(); setActiveView("organization"); } } async function fetchJson(url, options) { const response = await fetch(url, options); let payload = null; try { payload = await response.json(); } catch { payload = null; } if (!response.ok) { const message = payload?.detail || "요청 처리에 실패했습니다."; const error = new Error(message); error.status = response.status; throw error; } return payload; } async function loadSeatMapData(force = false) { if (seatMapState.loading || (seatMapState.loaded && !force)) return; seatMapState.loading = true; setSeatMapStatus("자리배치도를 불러오는 중입니다.", "info"); renderSeatMap(); try { 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.members = Array.isArray(layoutPayload.members) ? layoutPayload.members : []; seatMapState.slots = Array.isArray(layoutPayload.slots) ? layoutPayload.slots : []; seatMapState.placements = clonePlacements(layoutPayload.placements || []); seatMapState.zoom = 1; seatMapState.editMode = isAdmin(); resetSeatMapDraft(); seatMapState.loaded = true; setSeatMapStatus(isAdmin() ? "구성원을 바로 드래그해서 배치한 뒤 저장하세요." : "자리배치도를 불러왔습니다.", "success"); syncSeatMapSettingsForm(); } catch (error) { if (error.status === 404) { seatMapState.seatMap = null; seatMapState.members = []; seatMapState.slots = []; seatMapState.placements = []; seatMapState.zoom = 1; seatMapState.editMode = isAdmin(); resetSeatMapDraft(); seatMapState.loaded = true; setSeatMapStatus("활성화된 자리배치도가 없습니다.", "info"); syncSeatMapSettingsForm(); } else { setSeatMapStatus(error.message || "자리배치도 조회에 실패했습니다.", "error"); } } finally { seatMapState.loading = false; renderSeatMap(); } } async function getImageDimensions(file) { return new Promise((resolve) => { const image = new Image(); const objectUrl = URL.createObjectURL(file); image.onload = () => { resolve({ width: image.naturalWidth || null, height: image.naturalHeight || null }); URL.revokeObjectURL(objectUrl); }; image.onerror = () => { resolve({ width: null, height: null }); URL.revokeObjectURL(objectUrl); }; image.src = objectUrl; }); } async function uploadSeatMapImage(file, name) { const formData = new FormData(); formData.append("file", file); formData.append("name", name); return fetchJson("/api/seat-maps/dxf", { method: "POST", body: formData, }); } async function submitSeatMapSettings(event) { event.preventDefault(); if (!isAdmin()) return; const name = seatMapFormName?.value?.trim() || ""; const imageFile = seatMapFormImage?.files?.[0] || null; if (!name) { setSeatMapStatus("배치도 이름을 입력하세요.", "error"); return; } if (!imageFile) { setSeatMapStatus("DXF 파일을 선택하세요.", "error"); return; } if (!imageFile.name.toLowerCase().endsWith(".dxf")) { setSeatMapStatus("DXF 파일만 업로드할 수 있습니다.", "error"); return; } try { setSeatMapStatus("DXF 자리배치도를 업로드하고 분석하는 중입니다.", "info"); await uploadSeatMapImage(imageFile, name); if (seatMapFormImage) seatMapFormImage.value = ""; if (seatMapFileName) seatMapFileName.textContent = "선택된 파일 없음"; await loadSeatMapData(true); setSeatMapStatus("DXF 자리배치도를 저장했습니다.", "success"); } catch (error) { setSeatMapStatus(error.message || "DXF 자리배치도 저장에 실패했습니다.", "error"); } finally { renderSeatMap(); } } async function saveSeatLayout() { if (!seatMapState.seatMap || !seatMapState.editMode || !seatMapState.dirty) return; try { setSeatMapStatus("자리배치를 저장하는 중입니다.", "info"); await fetchJson(`/api/seat-maps/${seatMapState.seatMap.id}/layout`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ placements: seatMapState.draftPlacements }), }); await loadSeatMapData(true); setSeatMapStatus("자리배치를 저장했습니다.", "success"); } catch (error) { setSeatMapStatus(error.message || "자리배치도 저장에 실패했습니다.", "error"); renderSeatMap(); } } function cancelSeatMapEdit() { resetSeatMapDraft(); setSeatMapStatus("", "info"); setActiveView("organization"); } function getDraggedMemberId(event) { const raw = event.dataTransfer?.getData("text/plain") || seatMapState.draggingMemberId; const memberId = Number(raw); if (!Number.isInteger(memberId) || memberId <= 0) return null; return memberId; } function handleSeatMapCellDrop(event) { if (!seatMapState.editMode) return; event.preventDefault(); const memberId = getDraggedMemberId(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)); } else { const cell = event.target.closest(".seatmap-cell"); if (!cell) return; upsertDraftPlacement(memberId, Number(cell.dataset.row), Number(cell.dataset.col)); } renderSeatMap(); } function handleSeatMapListDrop(event) { if (!seatMapState.editMode) return; event.preventDefault(); const memberId = getDraggedMemberId(event); if (!memberId) return; removeDraftPlacement(memberId); renderSeatMap(); } function setActiveView(view) { const previousView = currentView; currentView = view in viewLabels ? view : "organization"; if (currentViewTitle) { currentViewTitle.textContent = viewLabels[currentView]; } navButtons.forEach((button) => { const active = button.dataset.view === currentView; button.classList.toggle("active", active); button.classList.toggle("muted", !active); }); const isOrganization = currentView === "organization"; const isSeatMap = currentView === "seatmap"; if (organizationStage) { organizationStage.hidden = !isOrganization; organizationStage.style.display = isOrganization ? "flex" : "none"; } if (seatMapStage) { seatMapStage.hidden = !isSeatMap; seatMapStage.style.display = isSeatMap ? "flex" : "none"; } if (emptyStage) { const showEmpty = !isOrganization && !isSeatMap; emptyStage.hidden = !showEmpty; emptyStage.style.display = showEmpty ? "flex" : "none"; } if (isOrganization && previousView !== "organization" && organizationFrame) { const frameSrc = organizationFrame.dataset.src || organizationFrame.src; organizationFrame.src = frameSrc; } if (isSeatMap) { loadSeatMapData(); } } function renderAuth() { const session = getSession(); const authenticated = Boolean(session?.user?.display_name); loginPanel.classList.toggle("hidden", authenticated); dashboardPanel.classList.toggle("hidden", !authenticated); if (authenticated) { const displayName = session.user.display_name || "접속자"; const rank = "-"; const employeeId = session.user.username || "-"; userBadge.innerHTML = `${escapeHtml(displayName)}${escapeHtml(rank)}`; userBadge.title = `${displayName} / -`; if (userPopover) { userPopover.innerHTML = `
이름 ${escapeHtml(displayName)}
직급 ${escapeHtml(rank)}
권한 ${escapeHtml(session.user.role || "-")}
사번 ${escapeHtml(employeeId)}
`; } } renderSeatMap(); } if (loginForm) { loginForm.addEventListener("submit", async (event) => { event.preventDefault(); loginMessage.textContent = "로그인 처리 중입니다."; const formData = new FormData(loginForm); try { const payload = await fetchJson("/api/mock-login", { method: "POST", body: formData, }); setSession(payload); loginForm.reset(); loginMessage.textContent = ""; renderAuth(); if (currentView === "seatmap") { await loadSeatMapData(true); } } catch (error) { loginMessage.textContent = error.message || "로그인에 실패했습니다."; } }); } if (userBadge) { userBadge.addEventListener("click", (event) => { event.stopPropagation(); toggleUserPopover(); }); } if (logoutBtn) { logoutBtn.addEventListener("click", (event) => { event.stopPropagation(); clearSession(); hideUserPopover(); renderAuth(); }); } navButtons.forEach((button) => { button.addEventListener("click", () => { hideUserPopover(); setActiveView(button.dataset.view || "organization"); }); }); if (seatMapSettingsForm) { seatMapSettingsForm.addEventListener("submit", submitSeatMapSettings); } if (seatMapSaveBtn) { seatMapSaveBtn.addEventListener("click", saveSeatLayout); } if (seatMapCancelBtn) { seatMapCancelBtn.addEventListener("click", cancelSeatMapEdit); } if (seatMapSearch) { seatMapSearch.addEventListener("input", () => { seatMapState.search = seatMapSearch.value || ""; renderSeatMap(); }); } if (seatMapFormImage) { seatMapFormImage.addEventListener("change", () => { if (seatMapFileName) { seatMapFileName.textContent = seatMapFormImage.files?.[0]?.name || "선택된 파일 없음"; } }); } 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); }, { passive: false }); seatMapBoard.addEventListener("dragover", (event) => { if (!seatMapState.editMode) return; const target = seatMapState.seatMap?.source_type === "dxf" ? event.target.closest(".seatmap-slot") : event.target.closest(".seatmap-cell"); if (!target) return; event.preventDefault(); event.dataTransfer.dropEffect = "move"; }); seatMapBoard.addEventListener("drop", handleSeatMapCellDrop); } if (seatMapBoardWrap) { seatMapBoardWrap.addEventListener("mousedown", (event) => { if (seatMapState.seatMap?.source_type !== "dxf") return; if (event.button !== 1) return; event.preventDefault(); seatMapState.panning = true; seatMapState.panStartX = event.clientX; seatMapState.panStartY = event.clientY; seatMapState.panScrollLeft = seatMapBoardWrap.scrollLeft; seatMapState.panScrollTop = seatMapBoardWrap.scrollTop; seatMapBoardWrap.classList.add("is-panning"); }); } document.addEventListener("mousemove", (event) => { if (!seatMapState.panning || !seatMapBoardWrap) return; const deltaX = event.clientX - seatMapState.panStartX; const deltaY = event.clientY - seatMapState.panStartY; seatMapBoardWrap.scrollLeft = seatMapState.panScrollLeft - deltaX; seatMapBoardWrap.scrollTop = seatMapState.panScrollTop - deltaY; }); document.addEventListener("mouseup", () => { if (!seatMapState.panning || !seatMapBoardWrap) return; seatMapState.panning = false; seatMapBoardWrap.classList.remove("is-panning"); }); if (seatMapUnassigned) { seatMapUnassigned.addEventListener("dragover", (event) => { if (!seatMapState.editMode) return; event.preventDefault(); event.dataTransfer.dropEffect = "move"; }); seatMapUnassigned.addEventListener("drop", handleSeatMapListDrop); } document.addEventListener("dragstart", (event) => { const card = event.target.closest(".seatmap-member-card"); if (!seatMapState.editMode || !card) return; const memberId = Number(card.dataset.memberId); if (!memberId) return; seatMapState.draggingMemberId = memberId; event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", String(memberId)); }); document.addEventListener("dragend", () => { seatMapState.draggingMemberId = null; }); document.addEventListener("click", () => { hideUserPopover(); }); window.addEventListener("message", handleEmbeddedNavigationMessage); setActiveView(currentView); renderAuth();