feat: wire as-of date into organization and seatmap views
This commit is contained in:
@@ -673,7 +673,7 @@ def ensure_history_backfill(cur) -> None:
|
||||
m.id, m.name, COALESCE(m.company, ''), COALESCE(m.rank, ''), COALESCE(m.role, ''),
|
||||
COALESCE(m.department, ''), COALESCE(m.grp, ''), COALESCE(m.division, ''), COALESCE(m.team, ''), COALESCE(m.cell, ''),
|
||||
COALESCE(m.work_status, ''), COALESCE(m.work_time, ''), COALESCE(m.phone, ''), COALESCE(m.email, ''), COALESCE(m.photo_url, ''),
|
||||
COALESCE(m.updated_at, m.created_at, NOW()), NULL, %s, NULL, 'initial-backfill'
|
||||
TIMESTAMPTZ '1970-01-01 00:00:00+00', NULL, %s, NULL, 'initial-backfill'
|
||||
FROM members AS m
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
@@ -692,7 +692,7 @@ def ensure_history_backfill(cur) -> None:
|
||||
)
|
||||
SELECT
|
||||
sp.member_id, sp.seat_map_id, sp.seat_slot_id, COALESCE(sp.seat_label, ''),
|
||||
COALESCE(sp.updated_at, NOW()), NULL, %s, NULL, 'initial-backfill'
|
||||
TIMESTAMPTZ '1970-01-01 00:00:00+00', NULL, %s, NULL, 'initial-backfill'
|
||||
FROM seat_positions AS sp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
@@ -702,3 +702,23 @@ def ensure_history_backfill(cur) -> None:
|
||||
""",
|
||||
(revision_id,),
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE member_versions
|
||||
SET valid_from = TIMESTAMPTZ '1970-01-01 00:00:00+00'
|
||||
WHERE revision_no = %s
|
||||
AND change_reason = 'initial-backfill'
|
||||
""",
|
||||
(revision_id,),
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE seat_assignment_versions
|
||||
SET valid_from = TIMESTAMPTZ '1970-01-01 00:00:00+00'
|
||||
WHERE revision_no = %s
|
||||
AND change_reason = 'initial-backfill'
|
||||
""",
|
||||
(revision_id,),
|
||||
)
|
||||
|
||||
@@ -4308,8 +4308,8 @@ def get_seat_layout(seat_map_id: int, as_of: str | None = None) -> dict[str, obj
|
||||
|
||||
|
||||
@app.get("/api/seat-maps/{seat_map_id}/viewer")
|
||||
def get_seat_map_viewer(seat_map_id: int) -> HTMLResponse:
|
||||
layout = fetch_seat_layout(seat_map_id)
|
||||
def get_seat_map_viewer(seat_map_id: int, as_of: str | None = None) -> HTMLResponse:
|
||||
layout = fetch_seat_layout(seat_map_id, parse_as_of(as_of))
|
||||
seat_map = layout.get("seat_map") or {}
|
||||
if seat_map.get("source_type") not in {"dxf", "fixed_html"}:
|
||||
raise HTTPException(status_code=400, detail="Viewer is only available for supported seat maps.")
|
||||
|
||||
@@ -155,6 +155,10 @@ const globalDateState = {
|
||||
endDate: "2026-01-31",
|
||||
};
|
||||
|
||||
function getGlobalAsOfDate() {
|
||||
return globalDateState.endDate || "";
|
||||
}
|
||||
|
||||
function getSession() {
|
||||
try {
|
||||
return JSON.parse(sessionStorage.getItem(sessionKey) || "null");
|
||||
@@ -181,7 +185,12 @@ function buildAuthHeaders(headers) {
|
||||
}
|
||||
|
||||
function shouldShowGlobalDateControls() {
|
||||
return currentView === "ledger" || currentView === "project" || currentView === "team" || currentView === "organization";
|
||||
return currentView === "ledger"
|
||||
|| currentView === "project"
|
||||
|| currentView === "team"
|
||||
|| currentView === "organization"
|
||||
|| currentView === "seatmap-admin"
|
||||
|| currentView === "seatmap-readonly";
|
||||
}
|
||||
|
||||
function syncGlobalDateControlVisibility() {
|
||||
@@ -208,6 +217,12 @@ function postGlobalDateRangeToFrame(frame) {
|
||||
frame.contentWindow.postMessage(getGlobalDateRangePayload(), window.location.origin);
|
||||
}
|
||||
|
||||
function buildAsOfQuery() {
|
||||
const asOf = getGlobalAsOfDate();
|
||||
if (!asOf) return "";
|
||||
return `?as_of=${encodeURIComponent(asOf)}`;
|
||||
}
|
||||
|
||||
function notifyEmbeddedTabActivated() {
|
||||
if (currentView === "project" && projectFrame?.contentWindow) {
|
||||
projectFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "project" }, window.location.origin);
|
||||
@@ -229,6 +244,7 @@ async function ensureGlobalDateRangeLoaded() {
|
||||
globalDateState.endDate = ends.length ? String(ends[ends.length - 1]).slice(0, 10) : "";
|
||||
globalDateState.loaded = true;
|
||||
syncGlobalDateControlInputs();
|
||||
postGlobalDateRangeToFrame(organizationFrame);
|
||||
postGlobalDateRangeToFrame(projectFrame);
|
||||
postGlobalDateRangeToFrame(teamFrame);
|
||||
} catch (error) {
|
||||
@@ -858,7 +874,7 @@ function renderDxfSeatMapBoard() {
|
||||
seatMapBoard.innerHTML = `<div class="seatmap-empty-card"><strong>DXF 뷰어 데이터를 준비하지 못했습니다.</strong></div>`;
|
||||
return;
|
||||
}
|
||||
const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer`);
|
||||
const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer${buildAsOfQuery()}`);
|
||||
seatMapBoard.innerHTML = `
|
||||
<div class="seatmap-dxf-frame-shell">
|
||||
<div class="seatmap-dxf-drop-overlay" data-seatmap-drop-overlay></div>
|
||||
@@ -1201,7 +1217,7 @@ async function loadSeatMapData(force = false) {
|
||||
const office = getCurrentSeatMapOffice();
|
||||
const activePayload = await fetchJson(`/api/seat-maps/active?office_key=${encodeURIComponent(office.key)}`);
|
||||
const activeSeatMap = activePayload.item;
|
||||
const layoutPayload = await fetchJson(`/api/seat-maps/${activeSeatMap.id}/layout`);
|
||||
const layoutPayload = await fetchJson(`/api/seat-maps/${activeSeatMap.id}/layout${buildAsOfQuery()}`);
|
||||
seatMapState.seatMap = {
|
||||
...(layoutPayload.seat_map || {}),
|
||||
viewer_data: layoutPayload.viewer_data || null,
|
||||
@@ -1531,6 +1547,7 @@ if (logoutBtn) {
|
||||
if (globalStartDateInput) {
|
||||
globalStartDateInput.addEventListener("change", () => {
|
||||
globalDateState.startDate = globalStartDateInput.value || "";
|
||||
postGlobalDateRangeToFrame(organizationFrame);
|
||||
postGlobalDateRangeToFrame(projectFrame);
|
||||
postGlobalDateRangeToFrame(teamFrame);
|
||||
});
|
||||
@@ -1539,11 +1556,20 @@ if (globalStartDateInput) {
|
||||
if (globalEndDateInput) {
|
||||
globalEndDateInput.addEventListener("change", () => {
|
||||
globalDateState.endDate = globalEndDateInput.value || "";
|
||||
postGlobalDateRangeToFrame(organizationFrame);
|
||||
postGlobalDateRangeToFrame(projectFrame);
|
||||
postGlobalDateRangeToFrame(teamFrame);
|
||||
if (currentView === "seatmap-admin" || currentView === "seatmap-readonly") {
|
||||
seatMapState.loaded = false;
|
||||
loadSeatMapData(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
organizationFrame?.addEventListener("load", () => {
|
||||
postGlobalDateRangeToFrame(organizationFrame);
|
||||
});
|
||||
|
||||
projectFrame?.addEventListener("load", () => {
|
||||
postGlobalDateRangeToFrame(projectFrame);
|
||||
if (currentView === "project") {
|
||||
|
||||
@@ -7,6 +7,7 @@ let isListMode = false;
|
||||
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
|
||||
let photoPreviewObjectUrl = null;
|
||||
let seatMapLayoutCache = null;
|
||||
let activeAsOfDate = '';
|
||||
const seatMapOfficeKeys = ['technical-development-center', 'hanmac-building-6f', 'hanmac-building-7f'];
|
||||
|
||||
const levelOrder = ['부서', '그룹', '디비전', '팀', '셀'];
|
||||
@@ -117,6 +118,14 @@ async function apiFetch(url, options = {}) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
function withAsOf(url) {
|
||||
if (!activeAsOfDate) {
|
||||
return url;
|
||||
}
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return `${url}${separator}as_of=${encodeURIComponent(activeAsOfDate)}`;
|
||||
}
|
||||
|
||||
async function uploadProfilePhoto(file, memberName) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
@@ -140,7 +149,7 @@ async function loadMembers(message) {
|
||||
if (message) {
|
||||
emptyStateMessage = message;
|
||||
}
|
||||
const payload = await apiFetch('/api/members');
|
||||
const payload = await apiFetch(withAsOf('/api/members'));
|
||||
setMembers(payload.items || []);
|
||||
if (!members.length) {
|
||||
emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
|
||||
@@ -160,7 +169,7 @@ async function loadSeatMapLayouts(force = false) {
|
||||
if (!seatMap?.id) {
|
||||
return null;
|
||||
}
|
||||
return await apiFetch(`/api/seat-maps/${seatMap.id}/layout`);
|
||||
return await apiFetch(withAsOf(`/api/seat-maps/${seatMap.id}/layout`));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -666,6 +675,15 @@ window.addEventListener('message', (event) => {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return;
|
||||
}
|
||||
if (data.type === 'date-range') {
|
||||
const nextAsOfDate = String(data.endDate || '').slice(0, 10);
|
||||
if (nextAsOfDate !== activeAsOfDate) {
|
||||
activeAsOfDate = nextAsOfDate;
|
||||
seatMapLayoutCache = null;
|
||||
loadMembers().catch(() => { });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data.type === 'seatmap-layout-updated') {
|
||||
handleSeatMapLayoutUpdated();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user