From 8121c9cf413be1e28c812fb43892b344e68aa59d Mon Sep 17 00:00:00 2001 From: hyunho Date: Mon, 30 Mar 2026 10:27:21 +0900 Subject: [PATCH] fix: align analysis defaults to latest completed month --- frontend/public/app.js | 64 +++++++++++++++++++++++++++++---- incoming-files/mh.html | 80 ++++++++++++++++++++++++++++-------------- 2 files changed, 111 insertions(+), 33 deletions(-) diff --git a/frontend/public/app.js b/frontend/public/app.js index ccfde08..380b1eb 100644 --- a/frontend/public/app.js +++ b/frontend/public/app.js @@ -153,9 +153,9 @@ const seatMapState = { let currentView = "project"; const globalDateState = { - loaded: true, - startDate: "2026-01-01", - endDate: "2026-01-31", + loaded: false, + startDate: "", + endDate: "", }; const organizationHistoryState = { @@ -194,6 +194,58 @@ function getTodayDate() { return `${now.getFullYear()}-${padDatePart(now.getMonth() + 1)}-${padDatePart(now.getDate())}`; } +function parseDateOnly(value) { + const raw = String(value || "").trim(); + if (!raw) return null; + const match = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!match) return null; + const year = Number(match[1]); + const monthIndex = Number(match[2]) - 1; + const day = Number(match[3]); + const parsed = new Date(year, monthIndex, day); + if ( + Number.isNaN(parsed.getTime()) + || parsed.getFullYear() !== year + || parsed.getMonth() !== monthIndex + || parsed.getDate() !== day + ) { + return null; + } + return parsed; +} + +function formatDateOnly(date) { + if (!(date instanceof Date) || Number.isNaN(date.getTime())) return ""; + return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`; +} + +function getMonthRangeForDate(date) { + if (!(date instanceof Date) || Number.isNaN(date.getTime())) { + return { startDate: "", endDate: "" }; + } + const start = new Date(date.getFullYear(), date.getMonth(), 1); + const end = new Date(date.getFullYear(), date.getMonth() + 1, 0); + return { + startDate: formatDateOnly(start), + endDate: formatDateOnly(end), + }; +} + +function getPreviousMonthRange(baseDate = new Date()) { + return getMonthRangeForDate(new Date(baseDate.getFullYear(), baseDate.getMonth() - 1, 1)); +} + +function resolveLatestCompletedMonthRange(maxDateText) { + const fallbackRange = getPreviousMonthRange(); + const latestAvailableDate = parseDateOnly(String(maxDateText || "").slice(0, 10)); + if (!latestAvailableDate) return fallbackRange; + const fallbackStart = parseDateOnly(fallbackRange.startDate); + if (fallbackStart && latestAvailableDate >= fallbackStart) { + return fallbackRange; + } + return getMonthRangeForDate(latestAvailableDate); +} + function syncOrganizationHistoryControls() { if (!organizationHistoryControls) return; const visible = currentView === "organization"; @@ -324,10 +376,10 @@ async function ensureGlobalDateRangeLoaded() { const payload = await fetchJson("/api/integration/summary"); const work = payload?.date_ranges?.work || {}; const voucher = payload?.date_ranges?.voucher || {}; - const starts = [work.min_work_date, voucher.min_voucher_date].filter(Boolean).sort(); const ends = [work.max_work_date, voucher.max_voucher_date].filter(Boolean).sort(); - globalDateState.startDate = starts[0] ? String(starts[0]).slice(0, 10) : ""; - globalDateState.endDate = ends.length ? String(ends[ends.length - 1]).slice(0, 10) : ""; + const defaultRange = resolveLatestCompletedMonthRange(ends.length ? ends[ends.length - 1] : ""); + globalDateState.startDate = defaultRange.startDate; + globalDateState.endDate = defaultRange.endDate; globalDateState.loaded = true; syncGlobalDateControlInputs(); postGlobalDateRangeToFrame(projectFrame); diff --git a/incoming-files/mh.html b/incoming-files/mh.html index 7e92681..a32fa73 100644 --- a/incoming-files/mh.html +++ b/incoming-files/mh.html @@ -1546,10 +1546,10 @@ if (isNaN(d.getTime())) return ""; return `${d.getFullYear()}-${("0" + (d.getMonth() + 1)).slice(-2)}-${("0" + d.getDate()).slice(-2)}`; }; - const calculateTargetHours = (startStr, endStr) => { - const start = new Date(startStr); - const end = new Date(endStr); - if (isNaN(start.getTime()) || isNaN(end.getTime()) || end < start) return 0; + const calculateTargetHours = (startStr, endStr) => { + const start = new Date(startStr); + const end = new Date(endStr); + if (isNaN(start.getTime()) || isNaN(end.getTime()) || end < start) return 0; const startUtc = Date.UTC(start.getFullYear(), start.getMonth(), start.getDate()); const endUtc = Date.UTC(end.getFullYear(), end.getMonth(), end.getDate()); const oneDay = 86400000; @@ -1561,13 +1561,37 @@ const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; const isHoliday = isWeekend || HOLIDAYS.includes(dateText); totalTarget += isHoliday ? HOLIDAY_TARGET_HOURS : WEEKDAY_TARGET_HOURS; - } - return Math.round(totalTarget); - }; - const findHeaderIndex = (normalizedHeaders, candidates) => { - const targets = candidates.map(normalizeHeader); - for (let i = 0; i < normalizedHeaders.length; i++) { - if (targets.includes(normalizedHeaders[i])) return i; + } + return Math.round(totalTarget); + }; + const formatLocalDate = (date) => { + if (!(date instanceof Date) || isNaN(date.getTime())) return ''; + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + }; + const getMonthRangeForDate = (date) => { + if (!(date instanceof Date) || isNaN(date.getTime())) return { startDate: '', endDate: '' }; + const start = new Date(date.getFullYear(), date.getMonth(), 1); + const end = new Date(date.getFullYear(), date.getMonth() + 1, 0); + return { + startDate: formatLocalDate(start), + endDate: formatLocalDate(end) + }; + }; + const getDefaultMhDateRange = (dates = []) => { + const now = new Date(); + const previousMonthRange = getMonthRangeForDate(new Date(now.getFullYear(), now.getMonth() - 1, 1)); + const latestDateText = [...dates].filter(Boolean).sort().slice(-1)[0] || ''; + const latestDate = latestDateText ? new Date(latestDateText) : null; + const previousMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1); + if (latestDate instanceof Date && !isNaN(latestDate.getTime()) && latestDate < previousMonthStart) { + return getMonthRangeForDate(latestDate); + } + return previousMonthRange; + }; + const findHeaderIndex = (normalizedHeaders, candidates) => { + const targets = candidates.map(normalizeHeader); + for (let i = 0; i < normalizedHeaders.length; i++) { + if (targets.includes(normalizedHeaders[i])) return i; } return -1; }; @@ -2501,11 +2525,12 @@ function initTeamTabAfterUpload() { if (teamData.length < 2) return; const dates = teamData.slice(1).map(r => dStr(r[columnMap.date])).filter(Boolean).sort(); - document.getElementById('start-date').value = dates[0]; - document.getElementById('end-date').value = dates[dates.length - 1]; - syncScopeButtons(); - refreshScopedSelections(false); - render(); + const defaultRange = getDefaultMhDateRange(dates); + document.getElementById('start-date').value = defaultRange.startDate; + document.getElementById('end-date').value = defaultRange.endDate; + syncScopeButtons(); + refreshScopedSelections(false); + render(); lucide.createIcons(); } @@ -2688,17 +2713,18 @@ if (mainSearch) mainSearch.value = ''; if (searchDropdown) searchDropdown.classList.add('hidden'); - refreshScopedSelections(false); - if (personSelect) personSelect.value = ''; - - const dates = teamData.slice(1).map(r => dStr(r[columnMap.date])).filter(Boolean).sort(); - if (dates.length > 0) { - startInput.value = dates[0]; - endInput.value = dates[dates.length - 1]; - } - - render(); - } + refreshScopedSelections(false); + if (personSelect) personSelect.value = ''; + + const dates = teamData.slice(1).map(r => dStr(r[columnMap.date])).filter(Boolean).sort(); + if (dates.length > 0) { + const defaultRange = getDefaultMhDateRange(dates); + startInput.value = defaultRange.startDate; + endInput.value = defaultRange.endDate; + } + + render(); + } document.getElementById('team-select').addEventListener('change', () => { updateFilters(); render(); });