feat: add seatmap context panel and smoke checks
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import FileResponse, Response
|
||||
@@ -88,7 +89,7 @@ def build_business_ledger_default_response(cur) -> Response:
|
||||
headers = {
|
||||
"Content-Disposition": 'inline; filename="business-ledger-default.xlsx"',
|
||||
"X-Source-Filename": "business-ledger-default.xlsx",
|
||||
"X-Original-Filename": filename,
|
||||
"X-Original-Filename": quote(filename),
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
||||
"Pragma": "no-cache",
|
||||
}
|
||||
|
||||
@@ -1663,6 +1663,11 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
"member_id": int(member["id"]),
|
||||
"name": str(member.get("name") or "-"),
|
||||
"rank": str(member.get("rank") or "-"),
|
||||
"team": str(member.get("team") or ""),
|
||||
"department": str(member.get("department") or ""),
|
||||
"grp": str(member.get("grp") or ""),
|
||||
"division": str(member.get("division") or ""),
|
||||
"cell": str(member.get("cell") or ""),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1844,6 +1849,40 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
return seatAssignments.get(String(key)) || null;
|
||||
}
|
||||
|
||||
function getTeamTone(teamName) {
|
||||
const team = String(teamName || "").trim();
|
||||
if (!team) {
|
||||
return { accent: "rgba(120, 132, 119, 0.86)", soft: "rgba(120, 132, 119, 0.18)", label: "미분류" };
|
||||
}
|
||||
const palette = [
|
||||
{ accent: "rgba(13, 148, 136, 0.9)", soft: "rgba(13, 148, 136, 0.18)" },
|
||||
{ accent: "rgba(202, 138, 4, 0.9)", soft: "rgba(202, 138, 4, 0.18)" },
|
||||
{ accent: "rgba(5, 150, 105, 0.9)", soft: "rgba(5, 150, 105, 0.18)" },
|
||||
{ accent: "rgba(180, 83, 9, 0.9)", soft: "rgba(180, 83, 9, 0.18)" },
|
||||
{ accent: "rgba(20, 83, 45, 0.9)", soft: "rgba(20, 83, 45, 0.16)" },
|
||||
{ accent: "rgba(11, 110, 79, 0.9)", soft: "rgba(11, 110, 79, 0.18)" },
|
||||
];
|
||||
let hash = 0;
|
||||
for (const char of team) {
|
||||
hash = ((hash * 31) + char.charCodeAt(0)) % 2147483647;
|
||||
}
|
||||
return { ...palette[Math.abs(hash) % palette.length], label: team };
|
||||
}
|
||||
|
||||
function getAssignmentGroupLabel(assignment) {
|
||||
return String((assignment && assignment.team) || "").trim();
|
||||
}
|
||||
|
||||
function notifyParentSelection(assignment) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "seatmap-member-selected",
|
||||
memberId: assignment ? Number(assignment.memberId || 0) : null,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
}
|
||||
|
||||
function hideSeatPopup() {
|
||||
popup.hidden = true;
|
||||
popup.innerHTML = "";
|
||||
@@ -1870,6 +1909,7 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
popup.innerHTML = `
|
||||
<strong>${assignment.name}</strong>
|
||||
<div>직급: ${assignment.rank || "-"}</div>
|
||||
<div>팀: ${assignment.team || "미분류"}</div>
|
||||
<div>상태: 배치완료</div>
|
||||
${viewerMode === "default" ? `<button type="button" data-seatmap-delete="${chairKey}">자리 비우기</button>` : ""}
|
||||
`;
|
||||
@@ -1908,6 +1948,11 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
key,
|
||||
name: item.name || "-",
|
||||
rank: item.rank || "-",
|
||||
team: item.team || "",
|
||||
department: item.department || "",
|
||||
grp: item.grp || "",
|
||||
division: item.division || "",
|
||||
cell: item.cell || "",
|
||||
memberId: Number(item.member_id || 0),
|
||||
};
|
||||
seatAssignments.set(key, assignment);
|
||||
@@ -1974,11 +2019,67 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
focusedChairPulseUntil = 0;
|
||||
}
|
||||
if (!seatAssignments.size) return;
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
ctx.textBaseline = "middle";
|
||||
const groupRects = new Map();
|
||||
for (const chair of chairGeometry) {
|
||||
const assignment = getAssignment(chair.key);
|
||||
if (!assignment) continue;
|
||||
const label = getAssignmentGroupLabel(assignment);
|
||||
if (!groupRects.has(label)) {
|
||||
groupRects.set(label, {
|
||||
label,
|
||||
tone: getTeamTone(label),
|
||||
minX: chair.minX,
|
||||
maxX: chair.maxX,
|
||||
minY: chair.minY,
|
||||
maxY: chair.maxY,
|
||||
count: 1,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const group = groupRects.get(label);
|
||||
group.minX = Math.min(group.minX, chair.minX);
|
||||
group.maxX = Math.max(group.maxX, chair.maxX);
|
||||
group.minY = Math.min(group.minY, chair.minY);
|
||||
group.maxY = Math.max(group.maxY, chair.maxY);
|
||||
group.count += 1;
|
||||
}
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
ctx.textBaseline = "middle";
|
||||
for (const group of groupRects.values()) {
|
||||
if (!group.label || group.count < 2) continue;
|
||||
const topLeft = worldToScreen(group.minX, group.maxY);
|
||||
const bottomRight = worldToScreen(group.maxX, group.minY);
|
||||
const pad = Math.max(26, 24 + camera.scale * 2200);
|
||||
const boxX = Math.min(topLeft.x, bottomRight.x) - pad;
|
||||
const boxY = Math.min(topLeft.y, bottomRight.y) - pad;
|
||||
const boxWidth = Math.abs(bottomRight.x - topLeft.x) + pad * 2;
|
||||
const boxHeight = Math.abs(bottomRight.y - topLeft.y) + pad * 2;
|
||||
ctx.save();
|
||||
const softFill = group.tone.soft
|
||||
.replace("0.16", "0.24")
|
||||
.replace("0.18", "0.28");
|
||||
ctx.fillStyle = softFill;
|
||||
ctx.strokeStyle = group.tone.accent;
|
||||
ctx.lineWidth = 2.6;
|
||||
ctx.shadowColor = group.tone.accent.replace("0.9", "0.18");
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 22);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.96)";
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(boxX + 14, boxY + 14, Math.max(72, group.label.length * 12), 28, 999);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = group.tone.accent;
|
||||
ctx.font = "900 12px Pretendard, sans-serif";
|
||||
ctx.fillText(group.label, boxX + 26, boxY + 28);
|
||||
ctx.restore();
|
||||
}
|
||||
for (const chair of chairGeometry) {
|
||||
const assignment = getAssignment(chair.key);
|
||||
if (!assignment) continue;
|
||||
const tone = getTeamTone(assignment.team);
|
||||
const center = worldToScreen((chair.minX + chair.maxX) / 2, (chair.minY + chair.maxY) / 2);
|
||||
if (center.x < -120 || center.x > rect.width + 120 || center.y < -50 || center.y > rect.height + 50) continue;
|
||||
const primary = `${assignment.name}`;
|
||||
@@ -1992,7 +2093,7 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
const boxX = center.x - boxWidth / 2;
|
||||
const boxY = center.y - 46;
|
||||
ctx.fillStyle = "rgba(255,255,255,0.96)";
|
||||
ctx.strokeStyle = "rgba(220,38,38,0.18)";
|
||||
ctx.strokeStyle = tone.accent;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 10);
|
||||
@@ -2001,7 +2102,7 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
ctx.fillStyle = "#111827";
|
||||
ctx.font = "700 12px Pretendard, sans-serif";
|
||||
ctx.fillText(primary, boxX + 10, boxY + 12);
|
||||
ctx.fillStyle = "#6b7280";
|
||||
ctx.fillStyle = tone.accent;
|
||||
ctx.font = "600 10px Pretendard, sans-serif";
|
||||
ctx.fillText(secondary, boxX + 10, boxY + 25);
|
||||
}
|
||||
@@ -2032,12 +2133,18 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
if (!picked) {
|
||||
selectedChairKey = null;
|
||||
hideSeatPopup();
|
||||
notifyParentSelection(null);
|
||||
if (typeof requestDraw === "function") requestDraw();
|
||||
return;
|
||||
}
|
||||
selectedChairKey = seatAssignments.has(String(picked.key)) ? String(picked.key) : null;
|
||||
if (selectedChairKey) showSeatPopup(selectedChairKey, event.clientX - rect.left, event.clientY - rect.top);
|
||||
else hideSeatPopup();
|
||||
if (selectedChairKey) {
|
||||
showSeatPopup(selectedChairKey, event.clientX - rect.left, event.clientY - rect.top);
|
||||
notifyParentSelection(getAssignment(selectedChairKey));
|
||||
} else {
|
||||
hideSeatPopup();
|
||||
notifyParentSelection(null);
|
||||
}
|
||||
if (typeof requestDraw === "function") requestDraw();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user