feat: promote seatmap and organization updates
This commit is contained in:
@@ -982,7 +982,7 @@ def parse_fixed_office_template(office_key: str = FIXED_OFFICE_SOURCE_KEY) -> di
|
|||||||
raise HTTPException(status_code=500, detail=f"Fixed office viewer data not found: {office_key}")
|
raise HTTPException(status_code=500, detail=f"Fixed office viewer data not found: {office_key}")
|
||||||
|
|
||||||
html = re.sub(
|
html = re.sub(
|
||||||
r'<script\s+src="\./[^"]+payload[^"]*\.js"></script>',
|
r'<script\s+src="\./[^"]+payload[^"]*\.js(?:\?[^"]*)?"></script>',
|
||||||
f"<script>{payload_js}</script>",
|
f"<script>{payload_js}</script>",
|
||||||
html,
|
html,
|
||||||
count=1,
|
count=1,
|
||||||
@@ -1582,6 +1582,10 @@ def fetch_seat_layout(seat_map_id: int, as_of: datetime | None = None) -> dict[s
|
|||||||
(as_of, as_of, seat_map_id, as_of, as_of),
|
(as_of, as_of, seat_map_id, as_of, as_of),
|
||||||
)
|
)
|
||||||
placements = cur.fetchall()
|
placements = cur.fetchall()
|
||||||
|
cur.execute("SELECT name FROM member_retirements")
|
||||||
|
retired_names = {str(row["name"] or "").strip() for row in cur.fetchall() if str(row["name"] or "").strip()}
|
||||||
|
for member in members:
|
||||||
|
member["is_retired"] = str(member.get("name") or "").strip() in retired_names
|
||||||
viewer_data: dict[str, object] | None = None
|
viewer_data: dict[str, object] | None = None
|
||||||
office_key = str(seat_map.get("source_url") or FIXED_OFFICE_SOURCE_KEY)
|
office_key = str(seat_map.get("source_url") or FIXED_OFFICE_SOURCE_KEY)
|
||||||
fixed_office = FIXED_OFFICE_CONFIGS.get(office_key)
|
fixed_office = FIXED_OFFICE_CONFIGS.get(office_key)
|
||||||
@@ -1807,6 +1811,8 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
|||||||
<script>
|
<script>
|
||||||
const seatAssignments = new Map();
|
const seatAssignments = new Map();
|
||||||
let selectedChairKey = null;
|
let selectedChairKey = null;
|
||||||
|
let focusedChairKey = null;
|
||||||
|
let focusedChairPulseUntil = 0;
|
||||||
let viewerMode = "default";
|
let viewerMode = "default";
|
||||||
const popup = document.createElement("div");
|
const popup = document.createElement("div");
|
||||||
popup.className = "seat-popup";
|
popup.className = "seat-popup";
|
||||||
@@ -1854,6 +1860,9 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
|||||||
function focusChair(chairKey, padding = 2200) {
|
function focusChair(chairKey, padding = 2200) {
|
||||||
const chair = chairGeometry.find((item) => String(item.key) === String(chairKey));
|
const chair = chairGeometry.find((item) => String(item.key) === String(chairKey));
|
||||||
if (!chair) return;
|
if (!chair) return;
|
||||||
|
selectedChairKey = String(chairKey);
|
||||||
|
focusedChairKey = String(chairKey);
|
||||||
|
focusedChairPulseUntil = Date.now() + 2600;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const pad = 24;
|
const pad = 24;
|
||||||
const minX = chair.minX - padding;
|
const minX = chair.minX - padding;
|
||||||
@@ -1919,8 +1928,31 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
|||||||
const originalDraw = draw;
|
const originalDraw = draw;
|
||||||
draw = function drawWithAssignments() {
|
draw = function drawWithAssignments() {
|
||||||
originalDraw();
|
originalDraw();
|
||||||
if (!seatAssignments.size) return;
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const now = Date.now();
|
||||||
|
const focusedChair = focusedChairKey
|
||||||
|
? chairGeometry.find((item) => String(item.key) === String(focusedChairKey))
|
||||||
|
: null;
|
||||||
|
if (focusedChair && now <= focusedChairPulseUntil) {
|
||||||
|
const pulse = (Math.sin(now / 180) + 1) / 2;
|
||||||
|
const center = worldToScreen((focusedChair.minX + focusedChair.maxX) / 2, (focusedChair.minY + focusedChair.maxY) / 2);
|
||||||
|
const width = Math.max(34, (focusedChair.maxX - focusedChair.minX) * camera.scale + 20 + pulse * 18);
|
||||||
|
const height = Math.max(34, (focusedChair.maxY - focusedChair.minY) * camera.scale + 20 + pulse * 18);
|
||||||
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = `rgba(245, 158, 11, ${0.38 + pulse * 0.32})`;
|
||||||
|
ctx.lineWidth = 3 + pulse * 2;
|
||||||
|
ctx.shadowColor = "rgba(245, 158, 11, 0.35)";
|
||||||
|
ctx.shadowBlur = 16 + pulse * 10;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(center.x - width / 2, center.y - height / 2, width, height, 16);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
if (typeof requestDraw === "function") requestDraw();
|
||||||
|
} else if (focusedChair && now > focusedChairPulseUntil) {
|
||||||
|
focusedChairPulseUntil = 0;
|
||||||
|
}
|
||||||
|
if (!seatAssignments.size) return;
|
||||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
for (const chair of chairGeometry) {
|
for (const chair of chairGeometry) {
|
||||||
|
|||||||
@@ -325,9 +325,7 @@ function buildAuthHeaders(headers) {
|
|||||||
function shouldShowGlobalDateControls() {
|
function shouldShowGlobalDateControls() {
|
||||||
return currentView === "ledger"
|
return currentView === "ledger"
|
||||||
|| currentView === "project"
|
|| currentView === "project"
|
||||||
|| currentView === "team"
|
|| currentView === "team";
|
||||||
|| currentView === "seatmap-admin"
|
|
||||||
|| currentView === "seatmap-readonly";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncGlobalDateControlVisibility() {
|
function syncGlobalDateControlVisibility() {
|
||||||
@@ -361,6 +359,10 @@ function buildAsOfQuery() {
|
|||||||
return `?as_of=${encodeURIComponent(asOf)}`;
|
return `?as_of=${encodeURIComponent(asOf)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSeatMapAsOfQuery() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
function notifyEmbeddedTabActivated() {
|
function notifyEmbeddedTabActivated() {
|
||||||
if (currentView === "project" && projectFrame?.contentWindow) {
|
if (currentView === "project" && projectFrame?.contentWindow) {
|
||||||
projectFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "project" }, window.location.origin);
|
projectFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "project" }, window.location.origin);
|
||||||
@@ -787,6 +789,22 @@ function getPlacementForMember(memberId) {
|
|||||||
return getPlacementSource().find((item) => Number(item.member_id) === Number(memberId)) || null;
|
return getPlacementSource().find((item) => Number(item.member_id) === Number(memberId)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMemberAssignedAnywhere(member) {
|
||||||
|
const seatLabel = String(
|
||||||
|
member?.member_seat_label
|
||||||
|
|| member?.seat_label
|
||||||
|
|| ""
|
||||||
|
).trim();
|
||||||
|
return Boolean(seatLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldHideMemberFromSeatMap(member) {
|
||||||
|
if (Boolean(member?.is_retired)) return true;
|
||||||
|
const workStatus = String(member?.work_status || "").trim();
|
||||||
|
if (/(퇴사|퇴직)/u.test(workStatus)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function memberMatchesSeatMapSearch(member) {
|
function memberMatchesSeatMapSearch(member) {
|
||||||
const keyword = seatMapState.search.trim().toLowerCase();
|
const keyword = seatMapState.search.trim().toLowerCase();
|
||||||
if (!keyword) return true;
|
if (!keyword) return true;
|
||||||
@@ -839,7 +857,9 @@ function renderSeatMapOfficeTabs() {
|
|||||||
function getUnassignedMembers() {
|
function getUnassignedMembers() {
|
||||||
const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id)));
|
const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id)));
|
||||||
return seatMapState.members.filter((member) => {
|
return seatMapState.members.filter((member) => {
|
||||||
|
if (shouldHideMemberFromSeatMap(member)) return false;
|
||||||
if (placedIds.has(Number(member.id))) return false;
|
if (placedIds.has(Number(member.id))) return false;
|
||||||
|
if (isMemberAssignedAnywhere(member)) return false;
|
||||||
return memberMatchesSeatMapSearch(member);
|
return memberMatchesSeatMapSearch(member);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -847,6 +867,7 @@ function getUnassignedMembers() {
|
|||||||
function getPlacedMembers() {
|
function getPlacedMembers() {
|
||||||
const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id)));
|
const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id)));
|
||||||
return seatMapState.members.filter((member) => {
|
return seatMapState.members.filter((member) => {
|
||||||
|
if (shouldHideMemberFromSeatMap(member)) return false;
|
||||||
if (!placedIds.has(Number(member.id))) return false;
|
if (!placedIds.has(Number(member.id))) return false;
|
||||||
return memberMatchesSeatMapSearch(member);
|
return memberMatchesSeatMapSearch(member);
|
||||||
});
|
});
|
||||||
@@ -1011,7 +1032,7 @@ function renderDxfSeatMapBoard() {
|
|||||||
seatMapBoard.innerHTML = `<div class="seatmap-empty-card"><strong>DXF 뷰어 데이터를 준비하지 못했습니다.</strong></div>`;
|
seatMapBoard.innerHTML = `<div class="seatmap-empty-card"><strong>DXF 뷰어 데이터를 준비하지 못했습니다.</strong></div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer${buildAsOfQuery()}`);
|
const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer${buildSeatMapAsOfQuery()}`);
|
||||||
seatMapBoard.innerHTML = `
|
seatMapBoard.innerHTML = `
|
||||||
<div class="seatmap-dxf-frame-shell">
|
<div class="seatmap-dxf-frame-shell">
|
||||||
<div class="seatmap-dxf-drop-overlay" data-seatmap-drop-overlay></div>
|
<div class="seatmap-dxf-drop-overlay" data-seatmap-drop-overlay></div>
|
||||||
@@ -1354,7 +1375,7 @@ async function loadSeatMapData(force = false) {
|
|||||||
const office = getCurrentSeatMapOffice();
|
const office = getCurrentSeatMapOffice();
|
||||||
const activePayload = await fetchJson(`/api/seat-maps/active?office_key=${encodeURIComponent(office.key)}`);
|
const activePayload = await fetchJson(`/api/seat-maps/active?office_key=${encodeURIComponent(office.key)}`);
|
||||||
const activeSeatMap = activePayload.item;
|
const activeSeatMap = activePayload.item;
|
||||||
const layoutPayload = await fetchJson(`/api/seat-maps/${activeSeatMap.id}/layout${buildAsOfQuery()}`);
|
const layoutPayload = await fetchJson(`/api/seat-maps/${activeSeatMap.id}/layout${buildSeatMapAsOfQuery()}`);
|
||||||
seatMapState.seatMap = {
|
seatMapState.seatMap = {
|
||||||
...(layoutPayload.seat_map || {}),
|
...(layoutPayload.seat_map || {}),
|
||||||
viewer_data: layoutPayload.viewer_data || null,
|
viewer_data: layoutPayload.viewer_data || null,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -316,7 +316,28 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content.wide {
|
.modal-content.wide {
|
||||||
max-width: 1200px;
|
max-width: 1060px;
|
||||||
|
width: min(1040px, calc(100vw - 48px));
|
||||||
|
max-height: calc(100vh - 28px);
|
||||||
|
padding: 20px 20px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.wide #modal-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.wide #modal-fields {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.wide #modal-footer-area {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-photo-field {
|
.member-photo-field {
|
||||||
@@ -333,37 +354,59 @@ body {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-layout {
|
.member-basic-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-basic-split {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(320px, 380px) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-left-pane,
|
.member-basic-left,
|
||||||
.member-edit-right-pane {
|
.member-basic-right,
|
||||||
|
.member-photo-panel,
|
||||||
|
.member-basic-fields {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-left-pane {
|
.member-basic-left,
|
||||||
display: flex;
|
.member-basic-right,
|
||||||
flex-direction: column;
|
.member-photo-panel {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-basic-left {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-profile-card {
|
.member-basic-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px 12px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-basic-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-seat-field {
|
.member-seat-field {
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-seat-field-emphasis .seat-preview-card {
|
.member-seat-field-compact .seat-preview-card {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-detail-top-row {
|
.member-detail-top-row {
|
||||||
@@ -455,8 +498,6 @@ body {
|
|||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
height: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,13 +508,21 @@ body {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-photo-upload-card-inline {
|
||||||
|
min-height: 168px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.member-photo-preview-wrap {
|
.member-photo-preview-wrap {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-photo-preview {
|
.member-photo-preview {
|
||||||
width: 84px;
|
width: 96px;
|
||||||
height: 84px;
|
height: 96px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border: 3px solid #e0e7ff;
|
border: 3px solid #e0e7ff;
|
||||||
@@ -520,6 +569,11 @@ body {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seat-preview-card.is-assigned {
|
||||||
|
border-color: rgba(79, 70, 229, 0.28);
|
||||||
|
box-shadow: 0 14px 30px rgba(79, 70, 229, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
.seat-preview-head {
|
.seat-preview-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -549,10 +603,11 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: #dbeafe;
|
background: linear-gradient(135deg, #4f46e5 0%, #2563eb 100%);
|
||||||
color: #1d4ed8;
|
color: #ffffff;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
box-shadow: 0 8px 20px rgba(79, 70, 229, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.seat-preview-badge-muted {
|
.seat-preview-badge-muted {
|
||||||
@@ -583,6 +638,11 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seat-preview-card.is-assigned .seat-preview-canvas {
|
||||||
|
border-color: rgba(79, 70, 229, 0.35);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(79, 70, 229, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.seat-preview-frame {
|
.seat-preview-frame {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -591,6 +651,10 @@ body {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seat-preview-card.is-assigned .seat-preview-frame {
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(79, 70, 229, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
.seat-preview-placeholder {
|
.seat-preview-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -601,30 +665,78 @@ body {
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-right-pane .seat-preview-head {
|
.member-seat-field-compact .seat-preview-head {
|
||||||
padding: 18px 20px 12px;
|
padding: 14px 16px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-right-pane .seat-preview-head strong {
|
.member-seat-field-compact .seat-preview-head strong {
|
||||||
font-size: 18px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-right-pane .seat-preview-head p {
|
.member-seat-field-compact .seat-preview-head p {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-right-pane .seat-preview-badge {
|
.member-seat-field-compact .seat-preview-badge {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
padding: 8px 12px;
|
padding: 7px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-right-pane .seat-preview-canvas {
|
.member-seat-field-compact .seat-preview-canvas {
|
||||||
min-height: 360px;
|
min-height: 208px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-right-pane .seat-preview-frame,
|
.member-seat-field-compact .seat-preview-frame,
|
||||||
.member-edit-right-pane .seat-preview-placeholder {
|
.member-seat-field-compact .seat-preview-placeholder {
|
||||||
min-height: 320px;
|
min-height: 208px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-photo-upload-card-inline {
|
||||||
|
min-height: 168px;
|
||||||
|
padding: 16px 14px;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-photo-card-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #475569;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-photo-upload-card-inline .member-photo-preview-wrap {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-photo-upload-card-inline .member-photo-preview {
|
||||||
|
width: 82px;
|
||||||
|
height: 82px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-photo-upload-card-inline .member-photo-upload-controls {
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-photo-upload-card-inline .member-photo-file-label {
|
||||||
|
padding: 9px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-photo-upload-card-inline .member-photo-file-name {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-basic-field input {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-basic-field label {
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -645,7 +757,11 @@ body {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-edit-layout {
|
.member-basic-split {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-basic-fields {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ function cloneMembers(items) {
|
|||||||
return JSON.parse(JSON.stringify(items));
|
return JSON.parse(JSON.stringify(items));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRetiredLegacyMember(member) {
|
||||||
|
const workStatus = String(member?.['근무상태'] || '').trim();
|
||||||
|
return workStatus === '퇴직';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleLegacyMembers(items) {
|
||||||
|
return (items || []).filter((member) => !isRetiredLegacyMember(member));
|
||||||
|
}
|
||||||
|
|
||||||
function getPhotoPlaceholder(name = '') {
|
function getPhotoPlaceholder(name = '') {
|
||||||
return `https://via.placeholder.com/160?text=${encodeURIComponent(name || 'Profile')}`;
|
return `https://via.placeholder.com/160?text=${encodeURIComponent(name || 'Profile')}`;
|
||||||
}
|
}
|
||||||
@@ -155,7 +164,7 @@ async function uploadProfilePhoto(file, memberName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setMembers(items) {
|
function setMembers(items) {
|
||||||
members = items.map(toLegacyMember);
|
members = getVisibleLegacyMembers(items.map(toLegacyMember));
|
||||||
if (selectedDept !== '전체' && !members.some((member) => member['부서'] === selectedDept)) {
|
if (selectedDept !== '전체' && !members.some((member) => member['부서'] === selectedDept)) {
|
||||||
selectedDept = '전체';
|
selectedDept = '전체';
|
||||||
}
|
}
|
||||||
@@ -1014,11 +1023,11 @@ function handlePhotoFileChange(event) {
|
|||||||
|
|
||||||
function renderSeatPreviewCard(seatInfo) {
|
function renderSeatPreviewCard(seatInfo) {
|
||||||
const assigned = Boolean(seatInfo?.assigned);
|
const assigned = Boolean(seatInfo?.assigned);
|
||||||
const safeLabel = escapeHtml(seatInfo?.seatLabel || '');
|
const seatMapLabel = String(seatInfo?.seatMapName || '자리배치도').replace(/\s*자리배치도\s*$/u, '').trim() || '사무실';
|
||||||
const safeSeatMapName = escapeHtml(seatInfo?.seatMapName || '자리배치도');
|
const safeSeatMapName = escapeHtml(seatInfo?.seatMapName || '자리배치도');
|
||||||
const safeSlotKey = escapeHtml(seatInfo?.slotKey || '');
|
const safeOfficeLabel = escapeHtml(seatMapLabel);
|
||||||
const badge = assigned
|
const badge = assigned
|
||||||
? `<span class="seat-preview-badge">${safeLabel || '배치완료'}</span>`
|
? `<span class="seat-preview-badge">${safeOfficeLabel}</span>`
|
||||||
: '<span class="seat-preview-badge seat-preview-badge-muted">미배치</span>';
|
: '<span class="seat-preview-badge seat-preview-badge-muted">미배치</span>';
|
||||||
const body = assigned
|
const body = assigned
|
||||||
? `
|
? `
|
||||||
@@ -1039,11 +1048,11 @@ function renderSeatPreviewCard(seatInfo) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="seat-preview-card">
|
<div class="seat-preview-card${assigned ? ' is-assigned' : ''}">
|
||||||
<div class="seat-preview-head">
|
<div class="seat-preview-head">
|
||||||
<div>
|
<div>
|
||||||
<strong>재석위치</strong>
|
<strong>재석위치</strong>
|
||||||
<p>${assigned ? '현재 자리배치도 기준으로 배치된 좌석 정보를 표시합니다.' : '현재 자리배치도에서 배치된 좌석이 없습니다.'}</p>
|
<p>${assigned ? '현재 배치된 사무실과 좌석 위치를 강조해서 표시합니다.' : '현재 자리배치도에서 배치된 좌석이 없습니다.'}</p>
|
||||||
</div>
|
</div>
|
||||||
${badge}
|
${badge}
|
||||||
</div>
|
</div>
|
||||||
@@ -1177,8 +1186,9 @@ function openModal(id) {
|
|||||||
<div class="col-span-1">
|
<div class="col-span-1">
|
||||||
<label class="text-[11px] font-black text-slate-600 block">근무 상태</label>
|
<label class="text-[11px] font-black text-slate-600 block">근무 상태</label>
|
||||||
<select id="m-status" class="w-full bg-white p-3 rounded-xl border text-sm font-bold outline-none">
|
<select id="m-status" class="w-full bg-white p-3 rounded-xl border text-sm font-bold outline-none">
|
||||||
<option value="근무" ${member['근무상태'] !== '휴직' ? 'selected' : ''}>근무</option>
|
<option value="근무" ${member['근무상태'] !== '휴직' && member['근무상태'] !== '퇴직' ? 'selected' : ''}>근무</option>
|
||||||
<option value="휴직" ${member['근무상태'] === '휴직' ? 'selected' : ''}>휴직</option>
|
<option value="휴직" ${member['근무상태'] === '휴직' ? 'selected' : ''}>휴직</option>
|
||||||
|
<option value="퇴직" ${member['근무상태'] === '퇴직' ? 'selected' : ''}>퇴직</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-1">
|
<div class="col-span-1">
|
||||||
@@ -1199,15 +1209,15 @@ function openModal(id) {
|
|||||||
<button id="modal-tab-basic" onclick="switchModalTab('basic')" class="flex-1 py-3 font-bold border-b-2 border-indigo-600 text-indigo-600 text-sm transition-all">기본 정보</button>
|
<button id="modal-tab-basic" onclick="switchModalTab('basic')" class="flex-1 py-3 font-bold border-b-2 border-indigo-600 text-indigo-600 text-sm transition-all">기본 정보</button>
|
||||||
<button id="modal-tab-org" onclick="switchModalTab('org')" class="flex-1 py-3 font-bold border-b-2 border-transparent text-slate-400 text-sm transition-all">조직 및 근무</button>
|
<button id="modal-tab-org" onclick="switchModalTab('org')" class="flex-1 py-3 font-bold border-b-2 border-transparent text-slate-400 text-sm transition-all">조직 및 근무</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-sec-basic" class="grid grid-cols-2 gap-3 modal-form-grid">
|
<div id="modal-sec-basic" class="modal-form-grid member-basic-editor">
|
||||||
<input type="hidden" id="m-id" value="${id || ''}">
|
<input type="hidden" id="m-id" value="${id || ''}">
|
||||||
<input type="hidden" id="m-photo-hidden" value="${member['사진'] || ''}">
|
<input type="hidden" id="m-photo-hidden" value="${member['사진'] || ''}">
|
||||||
<input type="hidden" id="m-seat-hidden" value="${member['자리위치'] || ''}">
|
<input type="hidden" id="m-seat-hidden" value="${member['자리위치'] || ''}">
|
||||||
<div class="col-span-2 member-edit-layout">
|
<div class="member-basic-split">
|
||||||
<div class="member-edit-left-pane">
|
<div class="member-basic-left">
|
||||||
<div class="member-edit-profile-card">
|
<div class="member-photo-panel">
|
||||||
<label class="text-[11px] font-black text-slate-600 block">프로필 사진</label>
|
<div class="member-photo-upload-card member-photo-upload-card-inline">
|
||||||
<div class="member-photo-upload-card member-photo-upload-card-compact">
|
<div class="member-photo-card-title">프로필 사진</div>
|
||||||
<div class="member-photo-preview-wrap">
|
<div class="member-photo-preview-wrap">
|
||||||
<img id="m-photo-preview" src="${member['사진'] || getPhotoPlaceholder(member['이름'] || '')}" alt="프로필 미리보기" class="member-photo-preview">
|
<img id="m-photo-preview" src="${member['사진'] || getPhotoPlaceholder(member['이름'] || '')}" alt="프로필 미리보기" class="member-photo-preview">
|
||||||
</div>
|
</div>
|
||||||
@@ -1219,28 +1229,28 @@ function openModal(id) {
|
|||||||
<strong id="m-photo-file-name" class="member-photo-file-name">선택된 파일 없음</strong>
|
<strong id="m-photo-file-name" class="member-photo-file-name">선택된 파일 없음</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-name-field member-name-field-compact">
|
</div>
|
||||||
|
<div class="member-basic-fields">
|
||||||
|
<div class="member-basic-field">
|
||||||
<label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label>
|
<label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label>
|
||||||
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
|
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
|
||||||
</div>
|
</div>
|
||||||
<div class="member-inline-info-grid member-inline-info-grid-stacked">
|
<div class="member-basic-field">
|
||||||
<div class="member-inline-info-card member-inline-info-card-full">
|
<label class="text-[11px] font-black text-slate-600 block">사번</label>
|
||||||
<label>사번</label>
|
<input id="m-employee-id" value="${member['사번'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
||||||
<input id="m-employee-id" value="${member['사번'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
</div>
|
||||||
</div>
|
<div class="member-basic-field">
|
||||||
<div class="member-inline-info-card member-inline-info-card-full">
|
<label class="text-[11px] font-black text-slate-600 block">전화번호</label>
|
||||||
<label>전화번호</label>
|
<input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
||||||
<input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
</div>
|
||||||
</div>
|
<div class="member-basic-field">
|
||||||
<div class="member-inline-info-card member-inline-info-card-full">
|
<label class="text-[11px] font-black text-slate-600 block">이메일</label>
|
||||||
<label>이메일</label>
|
<input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
||||||
<input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-edit-right-pane">
|
<div class="member-basic-right">
|
||||||
<div class="member-seat-field member-seat-field-emphasis">
|
<div class="member-seat-field member-seat-field-compact">
|
||||||
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
|
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1583,7 +1593,7 @@ async function loadSnapshotListView() {
|
|||||||
}
|
}
|
||||||
const payload = await apiFetch(`/api/members?as_of=${encodeURIComponent(snapshotDate)}`);
|
const payload = await apiFetch(`/api/members?as_of=${encodeURIComponent(snapshotDate)}`);
|
||||||
listViewState.snapshotDate = snapshotDate;
|
listViewState.snapshotDate = snapshotDate;
|
||||||
listViewState.snapshotMembers = (payload.items || []).map(toLegacyMember);
|
listViewState.snapshotMembers = getVisibleLegacyMembers((payload.items || []).map(toLegacyMember));
|
||||||
listViewState.mode = 'snapshot';
|
listViewState.mode = 'snapshot';
|
||||||
renderListViewModalContent();
|
renderListViewModalContent();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user