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="mt-3">
|
||||||
<div class="flex items-center justify-between px-1 mb-1 text-sm">
|
<div class="flex items-center justify-between px-1 mb-1 text-sm">
|
||||||
<span class="font-semibold text-gray-700">포장 명세</span>
|
<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="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>
|
||||||
<div id="packing-scroll" class="border border-gray-300 rounded-lg overflow-auto max-h-[179px]">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
<table class="w-full data-table text-sm">
|
<!-- 최근 스캔 2건 -->
|
||||||
<thead>
|
<div class="border border-gray-300 rounded-lg overflow-hidden">
|
||||||
<tr>
|
<div class="px-2 py-1 bg-gray-50 border-b border-gray-200 text-xs font-semibold text-gray-600">최근 스캔</div>
|
||||||
<th class="w-10 text-center sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">No</th>
|
<div id="recent-scans" class="p-2 text-xs text-gray-400 min-h-[3.5rem]">스캔 내역이 없습니다.</div>
|
||||||
<th class="w-40 text-left sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투종류</th>
|
</div>
|
||||||
<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>
|
<div class="border border-gray-300 rounded-lg overflow-hidden">
|
||||||
<th class="w-16 text-center sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">포장</th>
|
<div class="px-2 py-1 bg-amber-50 border-b border-amber-200 text-xs font-semibold text-amber-800">앞으로 찍을 목록 (남은 수량)</div>
|
||||||
</tr>
|
<div id="remaining-list" class="p-2 text-xs text-gray-400 max-h-[9rem] overflow-auto min-h-[3.5rem]">주문을 선택해 주세요.</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody id="packing-body">
|
|
||||||
<tr><td colspan="5" class="text-center py-6 text-gray-400">주문을 선택해 주세요.</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /포장 명세 -->
|
</div><!-- /스캔 현황 -->
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- 주문 취소 폼 — 버튼은 접수 품목 내역 타이틀 옆(form 속성으로 연결) -->
|
<!-- 주문 취소 폼 — 버튼은 접수 품목 내역 타이틀 옆(form 속성으로 연결) -->
|
||||||
@@ -150,6 +149,36 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</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 제거) -->
|
<!-- [개발용 임시] 선택 주문 판매소 기준 스캔 가능 바코드 후보 — 페이지 맨 아래 배치 (개발 완료 후 이 블록과 API 제거) -->
|
||||||
<div id="dev-saleable-panel" class="mt-3 hidden border border-amber-400 bg-amber-50/50 p-2 rounded-sm">
|
<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">
|
<p class="text-[11px] text-amber-900 mb-1 leading-relaxed">
|
||||||
@@ -464,9 +493,6 @@
|
|||||||
// 현재 이 주문을 보고 있으면 상세·포장명세 즉시 갱신
|
// 현재 이 주문을 보고 있으면 상세·포장명세 즉시 갱신
|
||||||
if (String(selectedId) === String(data.so_idx)) {
|
if (String(selectedId) === String(data.so_idx)) {
|
||||||
renderPacking(order);
|
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)) + '"]');
|
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) {
|
if (cell) {
|
||||||
cell.textContent = nf(Number(data.packed_qty || 0));
|
cell.textContent = nf(Number(data.packed_qty || 0));
|
||||||
@@ -580,32 +606,93 @@
|
|||||||
applySplit(currentPct());
|
applySplit(currentPct());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 포장 명세 탭: 주문 품목을 박스/팩/낱장 단위 행으로 펼침 ──────────
|
// ── 스캔 현황(최근 2건 + 앞으로 찍을 목록) + 포장 명세 팝업 ──────────
|
||||||
const packingBody = document.getElementById('packing-body');
|
|
||||||
const packingTotal = document.getElementById('packing-total');
|
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 packEsc(s) { return String(s == null ? '' : s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); }
|
||||||
// 포장 명세 = 실제 스캔(=판매/포장)된 봉투코드 내역. 최근 스캔이 위로 오도록 역순 표시.
|
|
||||||
function renderPacking(order) {
|
// 스캔 코드 전체 → 표 행(HTML) + 건수/합계 (팝업용). 최근이 위로.
|
||||||
if (!packingBody) return;
|
function scanFullRows(order) {
|
||||||
const scans = Array.isArray(order.scans) ? order.scans.slice().reverse() : [];
|
const scans = Array.isArray(order.scans) ? order.scans.slice().reverse() : [];
|
||||||
let no = 0;
|
let no = 0, total = 0;
|
||||||
let totalQty = 0;
|
|
||||||
const rows = scans.map((s) => {
|
const rows = scans.map((s) => {
|
||||||
no++;
|
no++;
|
||||||
const code = s.bag_code || '';
|
const code = s.bag_code || '', name = s.bag_name || '', qty = parseInt(s.qty || 0, 10) || 0;
|
||||||
const name = s.bag_name || '';
|
total += qty;
|
||||||
const qty = parseInt(s.qty || 0, 10) || 0;
|
|
||||||
totalQty += qty;
|
|
||||||
return '<tr><td class="text-center">' + no + '</td>'
|
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 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-left pl-2 font-mono text-gray-600">' + packEsc(s.code || '') + '</td>'
|
||||||
+ '<td class="text-right pr-2">' + nf(qty) + '</td>'
|
+ '<td class="text-right pr-2">' + nf(qty) + '</td>'
|
||||||
+ '<td class="text-center">' + packEsc(s.unit || '') + '</td></tr>';
|
+ '<td class="text-center">' + packEsc(s.unit || '') + '</td></tr>';
|
||||||
});
|
});
|
||||||
packingBody.innerHTML = rows.length ? rows.join('')
|
return { html: rows.join(''), count: no, total: total };
|
||||||
: '<tr><td colspan="5" class="text-center py-6 text-gray-400">스캔된 포장 내역이 없습니다.</td></tr>';
|
|
||||||
if (packingTotal) packingTotal.textContent = nf(totalQty);
|
|
||||||
}
|
}
|
||||||
|
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;
|
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'));
|
listBody.querySelectorAll('.order-list-row').forEach((row) => row.classList.remove('bg-blue-100'));
|
||||||
tr.classList.add('bg-blue-100');
|
tr.classList.add('bg-blue-100');
|
||||||
renderDetail(selectedId);
|
renderDetail(selectedId);
|
||||||
|
openPackingModal(); // 행 클릭 시 해당 주문 봉투코드 팝업 즉시 표시
|
||||||
});
|
});
|
||||||
|
|
||||||
detailBody?.addEventListener('input', (e) => {
|
detailBody?.addEventListener('input', (e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user