Files
ai_platform/views/dashboard-business-performance.ejs

330 lines
12 KiB
Plaintext

<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<%- include('partials/favicon') %>
<title>경영성과 대시보드 - XAVIS</title>
<link rel="stylesheet" href="/public/styles.css" />
<link rel="stylesheet" href="/mgmt-perf/dashboard.css" />
<style>
/* AI 프롬프트(/ai-explore/prompts)와 동일: main.container.container-ai-full → 전체 너비·좌우 24px 패딩 */
.mgmt-perf-split {
display: flex;
flex-direction: column;
gap: 20px;
}
.mgmt-upload-panel {
border: 1px solid var(--border, #e0e0e0);
border-radius: 10px;
padding: 20px;
background: #ffffff;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
}
.mgmt-upload-panel h2 {
font-size: 1.05rem;
margin: 0 0 12px;
}
.mgmt-upload-form {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-end;
}
.mgmt-upload-form label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
}
.mgmt-upload-form input[type="file"] {
max-width: 280px;
}
.mgmt-upload-form select,
.mgmt-upload-form input[type="number"] {
min-width: 100px;
padding: 8px 10px;
border-radius: 6px;
border: 1px solid #ccc;
}
.mgmt-upload-actions {
display: flex;
gap: 8px;
align-items: center;
}
.btn-mgmt-upload {
padding: 10px 18px;
border-radius: 8px;
border: none;
background: #1565c0;
color: #fff;
font-weight: 600;
cursor: pointer;
}
.btn-mgmt-upload:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.mgmt-upload-msg {
margin-top: 12px;
font-size: 13px;
min-height: 1.2em;
}
.mgmt-upload-msg.ok {
color: #2e7d32;
}
.mgmt-upload-msg.err {
color: #c62828;
}
.mgmt-dash-panel h2 {
font-size: 1.05rem;
margin: 0 0 10px;
}
.mgmt-dash-inline-wrap {
border: 1px solid var(--border, #e0e0e0);
border-radius: 10px;
overflow: hidden;
min-height: 400px;
background: #ffffff;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
}
.mgmt-perf-page .mgmt-perf-embed .container {
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
}
.mgmt-upload-history {
margin-top: 16px;
font-size: 13px;
}
.mgmt-upload-history table {
width: 100%;
border-collapse: collapse;
}
.mgmt-upload-history th,
.mgmt-upload-history td {
padding: 8px;
border-bottom: 1px solid #eee;
text-align: left;
vertical-align: middle;
}
.mgmt-upload-history th:last-child,
.mgmt-upload-history td.mgmt-upload-delete-cell {
text-align: right;
width: 88px;
white-space: nowrap;
}
.mgmt-upload-history th {
font-weight: 600;
color: #555;
}
.btn-mgmt-upload-delete {
padding: 6px 12px;
font-size: 12px;
border-radius: 6px;
border: 1px solid #fecaca;
background: #fff;
color: #b91c1c;
cursor: pointer;
font-weight: 600;
}
.btn-mgmt-upload-delete:hover {
background: #fef2f2;
}
.btn-mgmt-upload-delete:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="app-shell">
<%- include('partials/nav', {
activeMenu: 'dashboard',
adminMode: typeof adminMode !== 'undefined' ? adminMode : false,
}) %>
<div class="content-area mgmt-perf-page">
<header class="topbar">
<h1>경영성과 대시보드</h1>
</header>
<main class="container container-ai-full">
<a href="/dashboard" class="prompts-back" aria-label="대시보드 목록으로 돌아가기">← 대시보드 목록으로</a>
<div class="mgmt-perf-split">
<section class="mgmt-dash-panel" aria-labelledby="mgmt-dash-heading">
<h2 id="mgmt-dash-heading">대시보드 조회</h2>
<div class="mgmt-dash-inline-wrap">
<div class="mgmt-perf-embed" id="mgmtPerfDashRoot">
<%- include('partials/mgmt_perf_dashboard_container', {
dashboardTitle: typeof dashboardTitle !== 'undefined' ? dashboardTitle : '경영성과 대시보드',
quarterLabel: typeof quarterLabel !== 'undefined' ? quarterLabel : 'Q1',
}) %>
</div>
</div>
<script type="application/json" id="mgmt-perf-payload-json"><%- typeof payloadJson !== 'undefined' ? payloadJson : '{}' %></script>
<script src="/mgmt-perf/chart.umd.min.js"></script>
<script src="/mgmt-perf/dashboard-app.js"></script>
</section>
<section class="mgmt-upload-panel" aria-labelledby="mgmt-upload-heading">
<h2 id="mgmt-upload-heading">엑셀 업로드</h2>
<p class="subtitle" style="margin: 0 0 12px; font-size: 14px">
매출 집계 엑셀(<strong>매출일보</strong> 시트 포함)을 업로드하면 스냅샷이 저장되고, <strong>위</strong> 대시보드에 반영됩니다.
</p>
<form id="mgmtPerfUploadForm" class="mgmt-upload-form" enctype="multipart/form-data">
<label>
연도
<input type="number" name="fiscalYear" min="2020" max="2100" value="<%= typeof defaultYear !== 'undefined' ? defaultYear : 2026 %>" required />
</label>
<label>
분기
<select name="quarter" required>
<option value="1" <%= (typeof selectedQuarter !== 'undefined' ? selectedQuarter : 1) === 1 ? 'selected' : '' %>>1분기 (Q1)</option>
<option value="2" <%= (typeof selectedQuarter !== 'undefined' ? selectedQuarter : 1) === 2 ? 'selected' : '' %>>2분기 (Q2)</option>
<option value="3" <%= (typeof selectedQuarter !== 'undefined' ? selectedQuarter : 1) === 3 ? 'selected' : '' %>>3분기 (Q3)</option>
<option value="4" <%= (typeof selectedQuarter !== 'undefined' ? selectedQuarter : 1) === 4 ? 'selected' : '' %>>4분기 (Q4)</option>
</select>
</label>
<label>
파일 (.xlsx)
<input type="file" name="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" required />
</label>
<div class="mgmt-upload-actions">
<button type="submit" class="btn-mgmt-upload" id="mgmtPerfUploadBtn">업로드 및 반영</button>
</div>
</form>
<div id="mgmtPerfUploadMsg" class="mgmt-upload-msg" role="status"></div>
<% if (typeof uploadHistory !== 'undefined' && uploadHistory && uploadHistory.length) { %>
<div class="mgmt-upload-history" id="mgmtUploadHistory">
<strong>최근 업로드</strong>
<table>
<thead>
<tr>
<th>일시</th>
<th>파일명</th>
<th>연도·분기</th>
<th></th>
</tr>
</thead>
<tbody>
<% uploadHistory.forEach(function (u) { %>
<tr>
<td><%= u.created_at ? new Date(u.created_at).toLocaleString('ko-KR') : '-' %></td>
<td><%= u.original_filename || '-' %></td>
<td><%= u.fiscal_year != null && u.fiscal_year !== '' ? u.fiscal_year + '년 Q' + u.quarter : '-' %></td>
<td class="mgmt-upload-delete-cell">
<button type="button" class="btn-mgmt-upload-delete" data-upload-id="<%= u.id != null ? String(u.id) : '' %>">삭제</button>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } %>
</section>
</div>
</main>
</div>
</div>
<script>
(function () {
var form = document.getElementById("mgmtPerfUploadForm");
var btn = document.getElementById("mgmtPerfUploadBtn");
var msg = document.getElementById("mgmtPerfUploadMsg");
if (!form || !btn || !msg) return;
form.addEventListener("submit", function (e) {
e.preventDefault();
msg.textContent = "";
msg.className = "mgmt-upload-msg";
var fd = new FormData(form);
btn.disabled = true;
fetch("/api/mgmt-perf/upload", { method: "POST", body: fd, credentials: "same-origin" })
.then(function (r) {
return r.text().then(function (text) {
var j = {};
if (text) {
try {
j = JSON.parse(text);
} catch (parseErr) {
j = { error: "서버 응답을 해석할 수 없습니다. (" + r.status + ")" };
}
}
return { ok: r.ok, body: j, status: r.status };
});
})
.then(function (_ref) {
var ok = _ref.ok;
var body = _ref.body;
if (ok) {
msg.textContent = body.message || "저장되었습니다. 화면을 새로고침합니다.";
msg.className = "mgmt-upload-msg ok";
window.setTimeout(function () {
window.location.reload();
}, 600);
} else {
msg.textContent =
(body && body.error) ||
"업로드에 실패했습니다. (" + (_ref.status || "") + ")";
msg.className = "mgmt-upload-msg err";
}
})
.catch(function (err) {
msg.textContent =
(err && err.message) || "요청에 실패했습니다. 서버가 최신 코드인지·프록시에서 /api 경로가 Node로 전달되는지 확인하세요.";
msg.className = "mgmt-upload-msg err";
})
.finally(function () {
btn.disabled = false;
});
});
})();
(function () {
var root = document.getElementById("mgmtUploadHistory");
if (!root) return;
root.addEventListener("click", function (e) {
var t = e.target;
if (!t || !t.closest) return;
var del = t.closest(".btn-mgmt-upload-delete");
if (!del) return;
var id = del.getAttribute("data-upload-id");
if (!id) return;
if (!window.confirm("이 업로드 기록을 삭제할까요?")) return;
del.disabled = true;
fetch("/api/mgmt-perf/upload/" + encodeURIComponent(id), {
method: "DELETE",
credentials: "same-origin",
})
.then(function (r) {
return r.text().then(function (text) {
var j = {};
if (text) {
try {
j = JSON.parse(text);
} catch (parseErr) {
j = { error: text.slice(0, 200) };
}
}
return { ok: r.ok, body: j, status: r.status };
});
})
.then(function (ref) {
if (ref.ok && ref.body && ref.body.ok === true) {
window.location.reload();
return;
}
var err =
(ref.body && (ref.body.error || ref.body.message)) || "삭제에 실패했습니다.";
window.alert(err);
})
.catch(function () {
window.alert("삭제 요청에 실패했습니다.");
})
.finally(function () {
del.disabled = false;
});
});
})();
</script>
</body>
</html>