Update intranet tools and voucher comparison
This commit is contained in:
@@ -32,6 +32,42 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-job-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
max-width: 320px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #d8e2ec;
|
||||
border-radius: 999px;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.page-job-status[data-state="queued"],
|
||||
.page-job-status[data-state="running"] {
|
||||
border-color: #bae6fd;
|
||||
background: #f0f9ff;
|
||||
color: #075985;
|
||||
}
|
||||
|
||||
.page-job-status[data-state="done"] {
|
||||
border-color: #bbf7d0;
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.page-job-status[data-state="failed"] {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.upload-actions:hover .upload-tooltip,
|
||||
.upload-actions:focus-within .upload-tooltip {
|
||||
opacity: 1;
|
||||
@@ -266,6 +302,8 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
<button type="button" class="button-secondary" id="dashboardRebuildCacheBtn">캐시 재계산</button>
|
||||
<span class="page-job-status" id="dashboardJobStatus" data-state="">작업 상태 확인 전</span>
|
||||
<div class="upload-actions">
|
||||
<form action="/upload" method="post" enctype="multipart/form-data" id="uploadForm">
|
||||
<input id="excel_file" class="hidden-file-input" type="file" name="excel_file" accept=".xlsx,.xlsm,.xltx,.xltm" required>
|
||||
@@ -382,6 +420,7 @@
|
||||
let monthlySummary = [];
|
||||
let revenueYearly = [];
|
||||
let revenueMonthly = [];
|
||||
let dashboardJobPollTimer = null;
|
||||
const dashboardRevenueMetricOptions = {{ dashboard_revenue_metric_options | tojson }};
|
||||
const dashboardExpenseMetricOptions = {{ dashboard_expense_metric_options | tojson }};
|
||||
|
||||
@@ -548,6 +587,92 @@
|
||||
return String(availableYears[availableYears.length - 1] || "all");
|
||||
}
|
||||
|
||||
async function fetchDashboardJson(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
cache: "no-store",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload?.error || payload?.ok === false) {
|
||||
throw new Error(payload?.error || `HTTP ${response.status}`);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function describeDashboardJob(job) {
|
||||
if (!job) return { text: "최근 작업 없음", state: "" };
|
||||
const status = String(job.status || "");
|
||||
const range = job.start_year ? String(job.start_year) : "전체";
|
||||
const message = job.error_message || job.message || "";
|
||||
if (status === "queued") return { text: `${range} 대기 중`, state: status };
|
||||
if (status === "running") return { text: `${range} 계산 중... ${message}`.trim(), state: status };
|
||||
if (status === "done") return { text: `${range} 계산 완료`, state: status };
|
||||
if (status === "failed") return { text: `${range} 실패: ${message || "오류"}`, state: status };
|
||||
return { text: `${range} ${message || status || "작업 상태 확인 전"}`.trim(), state: status };
|
||||
}
|
||||
|
||||
function renderDashboardJob(job) {
|
||||
const jobStatus = document.getElementById("dashboardJobStatus");
|
||||
const rebuildButton = document.getElementById("dashboardRebuildCacheBtn");
|
||||
if (!jobStatus) return;
|
||||
const view = describeDashboardJob(job);
|
||||
jobStatus.textContent = view.text;
|
||||
jobStatus.dataset.state = view.state || "";
|
||||
if (rebuildButton) {
|
||||
rebuildButton.disabled = view.state === "queued" || view.state === "running";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLatestDashboardJob() {
|
||||
const params = new URLSearchParams({
|
||||
page_key: "dashboard",
|
||||
job_type: "dashboard_bootstrap",
|
||||
});
|
||||
const payload = await fetchDashboardJson(`/api/system-jobs/latest?${params.toString()}`);
|
||||
renderDashboardJob(payload.job || null);
|
||||
return payload.job || null;
|
||||
}
|
||||
|
||||
function pollDashboardJob(jobId) {
|
||||
if (!jobId) return;
|
||||
if (dashboardJobPollTimer) window.clearInterval(dashboardJobPollTimer);
|
||||
dashboardJobPollTimer = window.setInterval(async () => {
|
||||
try {
|
||||
const payload = await fetchDashboardJson(`/api/system-jobs/${encodeURIComponent(jobId)}`);
|
||||
const job = payload.job || null;
|
||||
renderDashboardJob(job);
|
||||
if (!job || ["done", "failed", "cancelled"].includes(String(job.status || ""))) {
|
||||
window.clearInterval(dashboardJobPollTimer);
|
||||
dashboardJobPollTimer = null;
|
||||
if (job && job.status === "done") {
|
||||
window.setTimeout(() => window.location.reload(), 600);
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
// Transient DB locks should not block the dashboard controls.
|
||||
}
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
async function requestDashboardCacheRebuild() {
|
||||
const rebuildButton = document.getElementById("dashboardRebuildCacheBtn");
|
||||
if (rebuildButton) rebuildButton.disabled = true;
|
||||
const payload = await fetchDashboardJson("/dashboard/api/rebuild-cache", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ overview_year: pageSelectedYear || null }),
|
||||
});
|
||||
renderDashboardJob(payload.job || null);
|
||||
pollDashboardJob(payload.job?.id);
|
||||
}
|
||||
|
||||
function syncYearSelection(selectId, granularity) {
|
||||
const select = document.getElementById(selectId);
|
||||
if (!select) return;
|
||||
@@ -645,6 +770,22 @@
|
||||
event.target.form?.submit();
|
||||
});
|
||||
|
||||
document.getElementById("dashboardRebuildCacheBtn")?.addEventListener("click", async () => {
|
||||
const confirmed = window.confirm("현재 대시보드 조건의 데이터를 서버에서 다시 계산할까요? 계산 중에도 다른 화면을 사용할 수 있습니다.");
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await requestDashboardCacheRebuild();
|
||||
} catch (error) {
|
||||
const jobStatus = document.getElementById("dashboardJobStatus");
|
||||
if (jobStatus) {
|
||||
jobStatus.textContent = error.message || "작업 등록 실패";
|
||||
jobStatus.dataset.state = "failed";
|
||||
}
|
||||
const rebuildButton = document.getElementById("dashboardRebuildCacheBtn");
|
||||
if (rebuildButton) rebuildButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
const uploadButton = document.getElementById("uploadButton");
|
||||
const excelInput = document.getElementById("excel_file");
|
||||
const uploadForm = document.getElementById("uploadForm");
|
||||
@@ -669,6 +810,19 @@
|
||||
syncYearSelection("expenseYear", document.getElementById("expenseGranularity")?.value || "yearly");
|
||||
updateRevenueChart();
|
||||
updateExpenseChart();
|
||||
loadLatestDashboardJob()
|
||||
.then((job) => {
|
||||
if (job && ["queued", "running"].includes(String(job.status || ""))) {
|
||||
pollDashboardJob(job.id);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const jobStatus = document.getElementById("dashboardJobStatus");
|
||||
if (jobStatus) {
|
||||
jobStatus.textContent = "작업 상태 조회 실패";
|
||||
jobStatus.dataset.state = "failed";
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user