feat(cupang): 박스 계산 화면 추가
달력 상단 "박스 계산" 버튼 → /cupang/box-calc. 제품명+수량을 여러 행 입력하면 제품별 박스 수·남은 낱개·총 필요 박스와 박스명별 합계를 계산. 수량/제품을 고치고 "계산 / 재계산" 으로 다시 계산할 수 있다(수량 칸에서 Enter 도 동일). 저장하지 않는 계산 전용 화면. 계산은 POST /cupang/api/box-calc 에서 store.compute_boxes 로 수행 — 클라이언트 계산을 신뢰하지 않는다. 입수량 규칙이 없는 제품은 "미설정" 으로 표시하고 합계에서 제외. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260828s" />{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="cpg">
|
||||
|
||||
<div class="erp-page-actions">
|
||||
<a class="erp-btn erp-btn-primary" href="/cupang/">◀◀ 달력</a>
|
||||
<a class="erp-btn erp-btn-outline" href="/cupang/box-rules">박스 입수량 설정</a>
|
||||
</div>
|
||||
|
||||
{% if not box_rules %}
|
||||
<div class="erp-card cpg-form-card">
|
||||
<p class="erp-muted">
|
||||
등록된 박스 입수량 규칙이 없습니다.
|
||||
<a href="/cupang/box-rules">박스 입수량 설정</a>에서 먼저 제품별 입수량을 등록하세요.
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="erp-card cpg-form-card cpg-calc-card">
|
||||
<div class="cpg-card-head">
|
||||
<h2>박스 계산</h2>
|
||||
<span class="erp-muted">수량을 고치고 다시 계산하면 결과가 갱신됩니다.</span>
|
||||
</div>
|
||||
|
||||
<div class="erp-table-wrap">
|
||||
<table class="erp-table cpg-calc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>제품명</th>
|
||||
<th class="cpg-calc-num">수량</th>
|
||||
<th>박스명</th>
|
||||
<th class="cpg-calc-num">입수량</th>
|
||||
<th class="cpg-calc-num">박스 수</th>
|
||||
<th class="cpg-calc-num">남은 낱개</th>
|
||||
<th class="cpg-calc-num">총 필요 박스</th>
|
||||
<th>동작</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cpg-calc-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="erp-page-actions cpg-calc-actions">
|
||||
<button type="button" class="erp-btn erp-btn-primary" id="cpg-calc-run">계산 / 재계산</button>
|
||||
<button type="button" class="erp-btn erp-btn-outline" id="cpg-calc-add">+ 제품 추가</button>
|
||||
<button type="button" class="erp-btn erp-btn-outline" id="cpg-calc-reset">초기화</button>
|
||||
<span class="erp-muted" id="cpg-calc-msg"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="erp-card cpg-form-card cpg-calc-sum" id="cpg-calc-sum-card" hidden>
|
||||
<div class="cpg-card-head">
|
||||
<h2>박스명별 합계</h2>
|
||||
<span class="erp-muted">가득 채운 박스 / 낱개 포함 총 필요 박스</span>
|
||||
</div>
|
||||
<div class="erp-table-wrap">
|
||||
<table class="erp-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>박스명</th>
|
||||
<th class="cpg-calc-num">박스 수</th>
|
||||
<th class="cpg-calc-num">남은 낱개 합</th>
|
||||
<th class="cpg-calc-num">총 필요 박스</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cpg-calc-sum-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="cpg-calc-rules">{{ box_rules | tojson }}</script>
|
||||
<script>
|
||||
// 박스 계산기 — 행 추가/삭제, 수량 수정 후 재계산.
|
||||
// 계산 자체는 서버(POST /cupang/api/box-calc, store.compute_boxes)에서 수행한다.
|
||||
(function () {
|
||||
var tbody = document.getElementById("cpg-calc-rows");
|
||||
if (!tbody) return;
|
||||
var runBtn = document.getElementById("cpg-calc-run");
|
||||
var addBtn = document.getElementById("cpg-calc-add");
|
||||
var resetBtn = document.getElementById("cpg-calc-reset");
|
||||
var msg = document.getElementById("cpg-calc-msg");
|
||||
var sumCard = document.getElementById("cpg-calc-sum-card");
|
||||
var sumRows = document.getElementById("cpg-calc-sum-rows");
|
||||
|
||||
var rules = [];
|
||||
try { rules = JSON.parse(document.getElementById("cpg-calc-rules").textContent || "[]"); } catch (e) {}
|
||||
|
||||
var optionsHtml = '<option value="">— 제품명 선택 —</option>';
|
||||
rules.forEach(function (r) {
|
||||
var label = (r.product_name_snapshot || r.product_code) +
|
||||
" (" + (r.box_name || "-") + " / " + r.units_per_box + "개)";
|
||||
optionsHtml += '<option value="' + esc(r.product_code) + '">' + esc(label) + "</option>";
|
||||
});
|
||||
|
||||
function esc(s) { var d = document.createElement("div"); d.textContent = s == null ? "" : s; return d.innerHTML; }
|
||||
|
||||
function addRow(code, qty) {
|
||||
var tr = document.createElement("tr");
|
||||
tr.className = "cpg-calc-row";
|
||||
tr.innerHTML =
|
||||
'<td><select class="erp-select cpg-calc-name">' + optionsHtml + "</select></td>" +
|
||||
'<td class="cpg-calc-num"><input class="erp-input cpg-calc-qty" type="number" min="0" step="1" value="' + (qty || 0) + '" /></td>' +
|
||||
'<td class="cpg-calc-box">—</td>' +
|
||||
'<td class="cpg-calc-num cpg-calc-upb">—</td>' +
|
||||
'<td class="cpg-calc-num cpg-calc-boxes">—</td>' +
|
||||
'<td class="cpg-calc-num cpg-calc-rem">—</td>' +
|
||||
'<td class="cpg-calc-num cpg-calc-req">—</td>' +
|
||||
'<td><button type="button" class="erp-btn erp-btn-danger cpg-calc-del">삭제</button></td>';
|
||||
tbody.appendChild(tr);
|
||||
if (code) tr.querySelector(".cpg-calc-name").value = code;
|
||||
return tr;
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
Array.prototype.forEach.call(tbody.querySelectorAll(".cpg-calc-row"), function (tr) {
|
||||
tr.querySelector(".cpg-calc-box").textContent = "—";
|
||||
tr.querySelector(".cpg-calc-upb").textContent = "—";
|
||||
tr.querySelector(".cpg-calc-boxes").textContent = "—";
|
||||
tr.querySelector(".cpg-calc-rem").textContent = "—";
|
||||
tr.querySelector(".cpg-calc-req").textContent = "—";
|
||||
tr.classList.remove("cpg-calc-warn");
|
||||
});
|
||||
sumCard.hidden = true;
|
||||
sumRows.innerHTML = "";
|
||||
}
|
||||
|
||||
function collect() {
|
||||
var items = [];
|
||||
Array.prototype.forEach.call(tbody.querySelectorAll(".cpg-calc-row"), function (tr) {
|
||||
var code = tr.querySelector(".cpg-calc-name").value;
|
||||
if (!code) return;
|
||||
items.push({ product_code: code, quantity: parseInt(tr.querySelector(".cpg-calc-qty").value, 10) || 0, _tr: tr });
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function run() {
|
||||
var items = collect();
|
||||
if (!items.length) { clearResults(); msg.textContent = "제품명을 하나 이상 선택하세요."; return; }
|
||||
msg.textContent = "계산 중…";
|
||||
runBtn.disabled = true;
|
||||
fetch("/cupang/api/box-calc", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: items.map(function (it) { return { product_code: it.product_code, quantity: it.quantity }; }) })
|
||||
})
|
||||
.then(function (r) { if (!r.ok) throw new Error("http " + r.status); return r.json(); })
|
||||
.then(function (data) {
|
||||
var results = (data && data.results) || [];
|
||||
results.forEach(function (res, i) {
|
||||
var tr = items[i] && items[i]._tr;
|
||||
if (!tr) return;
|
||||
tr.querySelector(".cpg-calc-box").textContent = res.box_name || "—";
|
||||
tr.querySelector(".cpg-calc-upb").textContent = res.units_per_box ? res.units_per_box + "개" : "미설정";
|
||||
if (res.configured) {
|
||||
tr.querySelector(".cpg-calc-boxes").textContent = res.full_boxes + "박스";
|
||||
tr.querySelector(".cpg-calc-rem").textContent = res.remainder_units + "개";
|
||||
tr.querySelector(".cpg-calc-req").textContent = res.required_boxes + "박스";
|
||||
tr.classList.remove("cpg-calc-warn");
|
||||
} else {
|
||||
tr.querySelector(".cpg-calc-boxes").textContent = "—";
|
||||
tr.querySelector(".cpg-calc-rem").textContent = "—";
|
||||
tr.querySelector(".cpg-calc-req").textContent = "—";
|
||||
tr.classList.add("cpg-calc-warn");
|
||||
}
|
||||
});
|
||||
var totals = (data && data.totals) || [];
|
||||
if (totals.length) {
|
||||
var html = "";
|
||||
totals.forEach(function (t) {
|
||||
html += "<tr><td>" + esc(t.box_name || "—") + "</td>" +
|
||||
'<td class="cpg-calc-num">' + t.full_boxes + "박스</td>" +
|
||||
'<td class="cpg-calc-num">' + t.remainder_units + "개</td>" +
|
||||
'<td class="cpg-calc-num">' + t.required_boxes + "박스</td></tr>";
|
||||
});
|
||||
sumRows.innerHTML = html;
|
||||
sumCard.hidden = false;
|
||||
} else {
|
||||
sumCard.hidden = true;
|
||||
}
|
||||
msg.textContent = "계산 완료 (" + results.length + "건)";
|
||||
})
|
||||
.catch(function () { msg.textContent = "계산 실패 — 새로고침 후 다시 시도하세요."; })
|
||||
.then(function () { runBtn.disabled = false; });
|
||||
}
|
||||
|
||||
tbody.addEventListener("click", function (e) {
|
||||
var del = e.target.closest(".cpg-calc-del");
|
||||
if (!del) return;
|
||||
var rows = tbody.querySelectorAll(".cpg-calc-row");
|
||||
if (rows.length <= 1) { addRow("", 0); }
|
||||
del.closest("tr").remove();
|
||||
msg.textContent = "";
|
||||
});
|
||||
|
||||
// 수량/제품을 바꾸면 이전 결과는 지운다 — 낡은 숫자 오독 방지.
|
||||
tbody.addEventListener("input", function (e) {
|
||||
if (e.target.classList.contains("cpg-calc-qty")) { msg.textContent = "수량 변경됨 — 재계산하세요."; }
|
||||
});
|
||||
tbody.addEventListener("change", function (e) {
|
||||
if (e.target.classList.contains("cpg-calc-name")) { msg.textContent = "제품 변경됨 — 재계산하세요."; }
|
||||
});
|
||||
tbody.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" && e.target.classList.contains("cpg-calc-qty")) { e.preventDefault(); run(); }
|
||||
});
|
||||
|
||||
runBtn.addEventListener("click", run);
|
||||
addBtn.addEventListener("click", function () { addRow("", 0); });
|
||||
resetBtn.addEventListener("click", function () {
|
||||
tbody.innerHTML = "";
|
||||
addRow("", 0);
|
||||
clearResults();
|
||||
msg.textContent = "";
|
||||
});
|
||||
|
||||
addRow("", 0);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -13,6 +13,7 @@
|
||||
<a class="erp-btn erp-btn-outline" href="/cupang/products">제품명 설정</a>
|
||||
<a class="erp-btn erp-btn-outline" href="/cupang/centers">입고센터 관리</a>
|
||||
<a class="erp-btn erp-btn-outline" href="/cupang/box-rules">박스 입수량 설정</a>
|
||||
<a class="erp-btn erp-btn-outline" href="/cupang/box-calc">박스 계산</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="cpg-actions-spacer"></div>
|
||||
|
||||
Reference in New Issue
Block a user