feat: 전화접수 관리 — 스캔 현황·포장량·포장 명세 보기 UI
전화접수 관리 화면에 스캔 현황(포장량 합계, 최근 스캔), 포장 명세 보기 모달 추가. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -121,26 +121,25 @@
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between px-1 mb-1 text-sm">
|
||||
<span class="font-semibold text-gray-700">포장 명세</span>
|
||||
<span class="text-gray-600">포장량: <span id="packing-total" class="font-bold text-blue-700">0</span> 개</span>
|
||||
<span class="font-semibold text-gray-700">스캔 현황</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="text-gray-600">포장량: <span id="packing-total" class="font-bold text-blue-700">0</span> 개</span>
|
||||
<button type="button" id="btn-packing-modal" class="border border-gray-300 text-gray-600 px-2 py-0.5 rounded-sm text-xs hover:bg-gray-50 disabled:opacity-50" disabled>포장 명세 보기</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="packing-scroll" class="border border-gray-300 rounded-lg overflow-auto max-h-[179px]">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-10 text-center sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">No</th>
|
||||
<th class="w-40 text-left sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투종류</th>
|
||||
<th class="text-left sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투코드</th>
|
||||
<th class="w-16 text-right sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">수량</th>
|
||||
<th class="w-16 text-center sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">포장</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="packing-body">
|
||||
<tr><td colspan="5" class="text-center py-6 text-gray-400">주문을 선택해 주세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<!-- 최근 스캔 2건 -->
|
||||
<div class="border border-gray-300 rounded-lg overflow-hidden">
|
||||
<div class="px-2 py-1 bg-gray-50 border-b border-gray-200 text-xs font-semibold text-gray-600">최근 스캔</div>
|
||||
<div id="recent-scans" class="p-2 text-xs text-gray-400 min-h-[3.5rem]">스캔 내역이 없습니다.</div>
|
||||
</div>
|
||||
<!-- 앞으로 찍을 목록(품목별 남은 수량) -->
|
||||
<div class="border border-gray-300 rounded-lg overflow-hidden">
|
||||
<div class="px-2 py-1 bg-amber-50 border-b border-amber-200 text-xs font-semibold text-amber-800">앞으로 찍을 목록 (남은 수량)</div>
|
||||
<div id="remaining-list" class="p-2 text-xs text-gray-400 max-h-[9rem] overflow-auto min-h-[3.5rem]">주문을 선택해 주세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /포장 명세 -->
|
||||
</div><!-- /스캔 현황 -->
|
||||
</form>
|
||||
|
||||
<!-- 주문 취소 폼 — 버튼은 접수 품목 내역 타이틀 옆(form 속성으로 연결) -->
|
||||
@@ -150,6 +149,36 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 포장 명세(전체 봉투코드) 팝업 -->
|
||||
<div id="packing-modal-overlay" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/40 p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[85vh] flex flex-col">
|
||||
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-2">
|
||||
<span class="text-sm font-bold text-gray-800">포장 명세 — <span id="packing-modal-title">-</span></span>
|
||||
<button type="button" id="packing-modal-close" class="text-gray-400 hover:text-gray-700 text-lg leading-none">×</button>
|
||||
</div>
|
||||
<div class="p-3 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-10 text-center" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">No</th>
|
||||
<th class="w-40 text-left" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투종류</th>
|
||||
<th class="text-left" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투코드</th>
|
||||
<th class="w-16 text-right" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">수량</th>
|
||||
<th class="w-16 text-center" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">포장</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="packing-modal-body">
|
||||
<tr><td colspan="5" class="text-center py-6 text-gray-400">스캔된 포장 내역이 없습니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 px-4 py-2 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500">총 <span id="packing-modal-count">0</span>건 · 포장량 <span id="packing-modal-total">0</span></span>
|
||||
<button type="button" id="packing-modal-close2" class="border border-gray-300 text-gray-600 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- [개발용 임시] 선택 주문 판매소 기준 스캔 가능 바코드 후보 — 페이지 맨 아래 배치 (개발 완료 후 이 블록과 API 제거) -->
|
||||
<div id="dev-saleable-panel" class="mt-3 hidden border border-amber-400 bg-amber-50/50 p-2 rounded-sm">
|
||||
<p class="text-[11px] text-amber-900 mb-1 leading-relaxed">
|
||||
@@ -464,9 +493,6 @@
|
||||
// 현재 이 주문을 보고 있으면 상세·포장명세 즉시 갱신
|
||||
if (String(selectedId) === String(data.so_idx)) {
|
||||
renderPacking(order);
|
||||
// 새로 스캔한 봉투가 항상 맨 위(최신)에 보이도록 스크롤을 맨 위로 이동
|
||||
const packingScrollEl = document.getElementById('packing-scroll');
|
||||
if (packingScrollEl) packingScrollEl.scrollTop = 0;
|
||||
const cell = detailBody.querySelector('.item-packed-cell[data-bag-code="' + (window.CSS && CSS.escape ? CSS.escape(String(data.bag_code)) : String(data.bag_code)) + '"]');
|
||||
if (cell) {
|
||||
cell.textContent = nf(Number(data.packed_qty || 0));
|
||||
@@ -580,32 +606,93 @@
|
||||
applySplit(currentPct());
|
||||
}
|
||||
|
||||
// ── 포장 명세 탭: 주문 품목을 박스/팩/낱장 단위 행으로 펼침 ──────────
|
||||
const packingBody = document.getElementById('packing-body');
|
||||
// ── 스캔 현황(최근 2건 + 앞으로 찍을 목록) + 포장 명세 팝업 ──────────
|
||||
const packingTotal = document.getElementById('packing-total');
|
||||
const recentScansEl = document.getElementById('recent-scans');
|
||||
const remainingListEl = document.getElementById('remaining-list');
|
||||
const btnPackingModal = document.getElementById('btn-packing-modal');
|
||||
const packingModalOverlay = document.getElementById('packing-modal-overlay');
|
||||
const packingModalBody = document.getElementById('packing-modal-body');
|
||||
const packingModalTitle = document.getElementById('packing-modal-title');
|
||||
const packingModalCount = document.getElementById('packing-modal-count');
|
||||
const packingModalTotal = document.getElementById('packing-modal-total');
|
||||
let packingModalOrderId = null;
|
||||
function packEsc(s) { return String(s == null ? '' : s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); }
|
||||
// 포장 명세 = 실제 스캔(=판매/포장)된 봉투코드 내역. 최근 스캔이 위로 오도록 역순 표시.
|
||||
function renderPacking(order) {
|
||||
if (!packingBody) return;
|
||||
|
||||
// 스캔 코드 전체 → 표 행(HTML) + 건수/합계 (팝업용). 최근이 위로.
|
||||
function scanFullRows(order) {
|
||||
const scans = Array.isArray(order.scans) ? order.scans.slice().reverse() : [];
|
||||
let no = 0;
|
||||
let totalQty = 0;
|
||||
let no = 0, total = 0;
|
||||
const rows = scans.map((s) => {
|
||||
no++;
|
||||
const code = s.bag_code || '';
|
||||
const name = s.bag_name || '';
|
||||
const qty = parseInt(s.qty || 0, 10) || 0;
|
||||
totalQty += qty;
|
||||
const code = s.bag_code || '', name = s.bag_name || '', qty = parseInt(s.qty || 0, 10) || 0;
|
||||
total += qty;
|
||||
return '<tr><td class="text-center">' + no + '</td>'
|
||||
+ '<td class="text-left pl-2 truncate" title="' + packEsc(code) + ' ' + packEsc(name) + '">' + packEsc(code) + ' ' + packEsc(name) + '</td>'
|
||||
+ '<td class="text-left pl-2 font-mono text-gray-600">' + packEsc(s.code || '') + '</td>'
|
||||
+ '<td class="text-right pr-2">' + nf(qty) + '</td>'
|
||||
+ '<td class="text-center">' + packEsc(s.unit || '') + '</td></tr>';
|
||||
});
|
||||
packingBody.innerHTML = rows.length ? rows.join('')
|
||||
: '<tr><td colspan="5" class="text-center py-6 text-gray-400">스캔된 포장 내역이 없습니다.</td></tr>';
|
||||
if (packingTotal) packingTotal.textContent = nf(totalQty);
|
||||
return { html: rows.join(''), count: no, total: total };
|
||||
}
|
||||
function fillPackingModal(order) {
|
||||
const full = scanFullRows(order);
|
||||
if (packingModalBody) packingModalBody.innerHTML = full.count ? full.html
|
||||
: '<tr><td colspan="5" class="text-center py-6 text-gray-400">스캔된 포장 내역이 없습니다.</td></tr>';
|
||||
if (packingModalCount) packingModalCount.textContent = nf(full.count);
|
||||
if (packingModalTotal) packingModalTotal.textContent = nf(full.total);
|
||||
}
|
||||
// 앞으로 찍을 목록 = 품목별 남은(접수량 − 포장량)
|
||||
function renderRemaining(order) {
|
||||
if (!remainingListEl) return;
|
||||
const items = Array.isArray(order.items) ? order.items : [];
|
||||
const rows = items.map((it) => {
|
||||
const qty = parseInt(it.soi_qty || 0, 10) || 0;
|
||||
const packed = parseInt(it.soi_packed_qty || 0, 10) || 0;
|
||||
if (qty <= 0) return '';
|
||||
const remain = Math.max(0, qty - packed);
|
||||
const name = (String(it.soi_bag_code || '') + ' ' + String(it.soi_bag_name || '')).trim();
|
||||
if (remain <= 0) {
|
||||
return '<div class="flex items-center justify-between text-emerald-600 py-0.5"><span class="truncate mr-2">' + packEsc(name) + '</span><span class="whitespace-nowrap">✓ 완료</span></div>';
|
||||
}
|
||||
return '<div class="flex items-center justify-between text-gray-700 py-0.5"><span class="truncate mr-2">' + packEsc(name) + '</span><span class="whitespace-nowrap font-semibold text-amber-700">' + nf(remain) + ' 매</span></div>';
|
||||
}).filter(Boolean);
|
||||
remainingListEl.innerHTML = rows.length ? rows.join('') : '<span class="text-gray-400">남은 수량이 없습니다.</span>';
|
||||
}
|
||||
// 스캔 현황 갱신(최근 2건 + 남은 목록 + 포장량 + 열린 팝업)
|
||||
function renderPacking(order) {
|
||||
const scans = Array.isArray(order.scans) ? order.scans : [];
|
||||
const full = scanFullRows(order);
|
||||
if (packingTotal) packingTotal.textContent = nf(full.total);
|
||||
if (btnPackingModal) btnPackingModal.disabled = false;
|
||||
if (recentScansEl) {
|
||||
const last = scans.slice(-2).reverse();
|
||||
recentScansEl.innerHTML = last.length
|
||||
? last.map((s, i) => '<div class="flex items-center justify-between py-0.5 ' + (i === 0 ? 'font-semibold text-emerald-700' : 'text-gray-500') + '">'
|
||||
+ '<span class="truncate mr-2 font-mono">' + (i === 0 ? '▶ ' : '') + packEsc(s.code || '') + '</span>'
|
||||
+ '<span class="whitespace-nowrap text-gray-500">' + packEsc(s.bag_name || '') + ' · ' + nf(parseInt(s.qty || 0, 10) || 0) + '</span></div>').join('')
|
||||
: '<span class="text-gray-400">스캔 내역이 없습니다.</span>';
|
||||
}
|
||||
renderRemaining(order);
|
||||
if (packingModalOverlay && !packingModalOverlay.classList.contains('hidden') && String(packingModalOrderId) === String(order.so_idx)) {
|
||||
fillPackingModal(order);
|
||||
}
|
||||
}
|
||||
// 포장 명세 팝업 열기/닫기
|
||||
function openPackingModal() {
|
||||
const order = orderMap.get(String(selectedId));
|
||||
if (!order) return;
|
||||
packingModalOrderId = selectedId;
|
||||
if (packingModalTitle) packingModalTitle.textContent = '접수 #' + displayNo(order) + ' · ' + (order.so_ds_name || '');
|
||||
fillPackingModal(order);
|
||||
packingModalOverlay.classList.remove('hidden');
|
||||
packingModalOverlay.classList.add('flex');
|
||||
}
|
||||
function closePackingModal() { packingModalOverlay.classList.add('hidden'); packingModalOverlay.classList.remove('flex'); }
|
||||
if (btnPackingModal) btnPackingModal.addEventListener('click', openPackingModal);
|
||||
document.getElementById('packing-modal-close')?.addEventListener('click', closePackingModal);
|
||||
document.getElementById('packing-modal-close2')?.addEventListener('click', closePackingModal);
|
||||
if (packingModalOverlay) packingModalOverlay.addEventListener('click', (e) => { if (e.target === packingModalOverlay) closePackingModal(); });
|
||||
|
||||
// ── 접수 리스트 렌더 + 헤더 클릭 정렬 ──────────────────────────
|
||||
let sortKey = 'so_idx', sortDir = 'desc', selectedId = null;
|
||||
@@ -668,6 +755,7 @@
|
||||
listBody.querySelectorAll('.order-list-row').forEach((row) => row.classList.remove('bg-blue-100'));
|
||||
tr.classList.add('bg-blue-100');
|
||||
renderDetail(selectedId);
|
||||
openPackingModal(); // 행 클릭 시 해당 주문 봉투코드 팝업 즉시 표시
|
||||
});
|
||||
|
||||
detailBody?.addEventListener('input', (e) => {
|
||||
|
||||
Reference in New Issue
Block a user