Update intranet tools and voucher comparison
This commit is contained in:
@@ -25,6 +25,49 @@
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.summary-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -207,6 +250,10 @@
|
||||
<section class="panel">
|
||||
<div class="section-title" style="margin-bottom: 14px;">
|
||||
<h2>수익/비용 현황</h2>
|
||||
<div class="summary-actions">
|
||||
<button type="button" class="button-secondary" id="annualRebuildCacheBtn">캐시 재계산</button>
|
||||
<span class="page-job-status" id="annualJobStatus" data-state="">작업 상태 확인 전</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-panel">
|
||||
<div class="summary-overview">
|
||||
@@ -253,6 +300,7 @@
|
||||
let yearlySeries = [];
|
||||
let monthlySeries = [];
|
||||
let availableYears = [];
|
||||
let annualJobPollTimer = null;
|
||||
const annualMetricCards = {{ annual_metric_cards | tojson }};
|
||||
const annualExpenseChartMetrics = {{ annual_expense_chart_metrics | tojson }};
|
||||
const annualBalanceChartMetrics = {{ annual_balance_chart_metrics | tojson }};
|
||||
@@ -356,6 +404,89 @@
|
||||
return String(availableYears[availableYears.length - 1] || "recent10");
|
||||
}
|
||||
|
||||
async function fetchAnnualJson(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 describeAnnualJob(job) {
|
||||
if (!job) return { text: "최근 작업 없음", state: "" };
|
||||
const status = String(job.status || "");
|
||||
const message = job.error_message || job.message || "";
|
||||
if (status === "queued") return { text: "작업 대기 중", state: status };
|
||||
if (status === "running") return { text: `계산 중... ${message}`.trim(), state: status };
|
||||
if (status === "done") return { text: "계산 완료", state: status };
|
||||
if (status === "failed") return { text: `실패: ${message || "오류"}`, state: status };
|
||||
return { text: message || status || "작업 상태 확인 전", state: status };
|
||||
}
|
||||
|
||||
function renderAnnualJob(job) {
|
||||
const jobStatus = document.getElementById("annualJobStatus");
|
||||
const rebuildButton = document.getElementById("annualRebuildCacheBtn");
|
||||
if (!jobStatus) return;
|
||||
const view = describeAnnualJob(job);
|
||||
jobStatus.textContent = view.text;
|
||||
jobStatus.dataset.state = view.state || "";
|
||||
if (rebuildButton) {
|
||||
rebuildButton.disabled = view.state === "queued" || view.state === "running";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLatestAnnualJob() {
|
||||
const params = new URLSearchParams({
|
||||
page_key: "annual_summary",
|
||||
job_type: "annual_summary_bootstrap",
|
||||
});
|
||||
const payload = await fetchAnnualJson(`/api/system-jobs/latest?${params.toString()}`);
|
||||
renderAnnualJob(payload.job || null);
|
||||
return payload.job || null;
|
||||
}
|
||||
|
||||
function pollAnnualJob(jobId) {
|
||||
if (!jobId) return;
|
||||
if (annualJobPollTimer) window.clearInterval(annualJobPollTimer);
|
||||
annualJobPollTimer = window.setInterval(async () => {
|
||||
try {
|
||||
const payload = await fetchAnnualJson(`/api/system-jobs/${encodeURIComponent(jobId)}`);
|
||||
const job = payload.job || null;
|
||||
renderAnnualJob(job);
|
||||
if (!job || ["done", "failed", "cancelled"].includes(String(job.status || ""))) {
|
||||
window.clearInterval(annualJobPollTimer);
|
||||
annualJobPollTimer = null;
|
||||
if (job && job.status === "done") {
|
||||
window.setTimeout(() => window.location.reload(), 600);
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
// Keep existing charts visible during transient lock or network delays.
|
||||
}
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
async function requestAnnualCacheRebuild() {
|
||||
const rebuildButton = document.getElementById("annualRebuildCacheBtn");
|
||||
if (rebuildButton) rebuildButton.disabled = true;
|
||||
const payload = await fetchAnnualJson("/annual-summary/api/rebuild-cache", {
|
||||
method: "POST",
|
||||
});
|
||||
renderAnnualJob(payload.job || null);
|
||||
pollAnnualJob(payload.job?.id);
|
||||
}
|
||||
|
||||
async function loadAnnualSummaryBootstrapData() {
|
||||
const response = await fetch("/annual-summary/bootstrap-data", {
|
||||
method: "GET",
|
||||
@@ -590,6 +721,21 @@
|
||||
|
||||
document.getElementById("granularity").addEventListener("change", renderAll);
|
||||
document.getElementById("yearFilter").addEventListener("change", renderAll);
|
||||
document.getElementById("annualRebuildCacheBtn")?.addEventListener("click", async () => {
|
||||
const confirmed = window.confirm("연도별 수익/비용 데이터를 서버에서 다시 계산할까요? 계산 중에도 다른 화면을 사용할 수 있습니다.");
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await requestAnnualCacheRebuild();
|
||||
} catch (error) {
|
||||
const jobStatus = document.getElementById("annualJobStatus");
|
||||
if (jobStatus) {
|
||||
jobStatus.textContent = error.message || "작업 등록 실패";
|
||||
jobStatus.dataset.state = "failed";
|
||||
}
|
||||
const rebuildButton = document.getElementById("annualRebuildCacheBtn");
|
||||
if (rebuildButton) rebuildButton.disabled = false;
|
||||
}
|
||||
});
|
||||
const granularitySelect = document.getElementById("granularity");
|
||||
if (granularitySelect) {
|
||||
granularitySelect.value = "yearly";
|
||||
@@ -605,6 +751,19 @@
|
||||
console.error("연도별 수익/비용 부트스트랩 데이터 조회 에러", error);
|
||||
}
|
||||
renderAll();
|
||||
loadLatestAnnualJob()
|
||||
.then((job) => {
|
||||
if (job && ["queued", "running"].includes(String(job.status || ""))) {
|
||||
pollAnnualJob(job.id);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const jobStatus = document.getElementById("annualJobStatus");
|
||||
if (jobStatus) {
|
||||
jobStatus.textContent = "작업 상태 조회 실패";
|
||||
jobStatus.dataset.state = "failed";
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user