종량제3 세션 작업. - 워크스페이스 탭 계정별 저장·자동복원(member_workspace_tabs 자동생성, pref/workspace-tabs) - 주문접수 관리 입금확인(so_paid) 수동 토글(phoneOrderMarkPaid, order/phone/manage/paid) - 메뉴 '전화 접수 관리' → '주문접수관리' 개명(피드백 #35) + 시드/개명 SQL Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
840 lines
46 KiB
PHP
840 lines
46 KiB
PHP
<?php $startDate = (string) ($startDate ?? ''); $endDate = (string) ($endDate ?? ''); ?>
|
||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||
<span class="text-sm font-bold text-gray-700">주문접수 관리</span>
|
||
</div>
|
||
<form method="get" action="<?= base_url('bag/order/phone/manage') ?>" class="mt-2 flex flex-wrap items-center gap-2 text-sm">
|
||
<label class="font-bold text-gray-700">접수일</label>
|
||
<input type="date" lang="ko" name="start_date" value="<?= esc($startDate, 'attr') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||
<span class="text-gray-400">~</span>
|
||
<input type="date" lang="ko" name="end_date" value="<?= esc($endDate, 'attr') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||
<button type="submit" class="bg-btn-search text-white px-3 py-1 rounded-sm text-sm">조회</button>
|
||
<?php if ($startDate !== '' || $endDate !== ''): ?>
|
||
<a href="<?= base_url('bag/order/phone/manage') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">초기화</a>
|
||
<?php endif; ?>
|
||
</form>
|
||
</section>
|
||
|
||
<div id="order-split" class="flex flex-col xl:flex-row gap-3 mt-2 items-stretch">
|
||
<section id="list-card" class="border border-gray-300 rounded-lg bg-white overflow-hidden flex flex-col min-w-0 xl:shrink-0">
|
||
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700">접수 리스트(전화)</div>
|
||
<div id="list-scroll" class="overflow-auto">
|
||
<table class="w-full data-table text-sm">
|
||
<thead>
|
||
<tr>
|
||
<th class="w-12 text-center sort-th cursor-pointer select-none" data-sort="so_idx" data-type="num">번호<span class="sort-ind"></span></th>
|
||
<th class="w-24 text-center sort-th cursor-pointer select-none" data-sort="so_order_date" data-type="str">접수일<span class="sort-ind"></span></th>
|
||
<th class="w-24 text-center sort-th cursor-pointer select-none" data-sort="so_delivery_date" data-type="str">배달일<span class="sort-ind"></span></th>
|
||
<th class="w-14 text-center sort-th cursor-pointer select-none" data-sort="so_channel" data-type="str">구분<span class="sort-ind"></span></th>
|
||
<th class="text-left sort-th cursor-pointer select-none" data-sort="so_ds_name" data-type="str">판매소<span class="sort-ind"></span></th>
|
||
<th class="w-16 text-center sort-th cursor-pointer select-none" data-sort="so_payment_type" data-type="str">결제<span class="sort-ind"></span></th>
|
||
<th class="w-14 text-center sort-th cursor-pointer select-none" data-sort="so_received" data-type="num">포장<span class="sort-ind"></span></th>
|
||
<th class="w-14 text-center sort-th cursor-pointer select-none" data-sort="so_paid" data-type="num">입금<span class="sort-ind"></span></th>
|
||
<th class="w-24 text-right sort-th cursor-pointer select-none" data-sort="so_total_amount" data-type="num">총금액<span class="sort-ind"></span></th>
|
||
<th class="w-14 text-center sort-th cursor-pointer select-none" data-sort="so_status" data-type="str">취소<span class="sort-ind"></span></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="order-list-body"></tbody>
|
||
</table>
|
||
</div>
|
||
<div id="list-summary" class="px-3 py-2 border-t border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700 flex flex-wrap items-center justify-between gap-2">
|
||
<span>건수 <span id="list-sum-count" class="text-blue-700">0</span>건</span>
|
||
<span>금액 <span id="list-sum-amount" class="text-blue-700">0</span>원</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 드래그 가능한 너비 조절 손잡이 (xl 이상 좌우 배치일 때만 표시) -->
|
||
<div id="split-handle" class="hidden xl:flex items-center justify-center cursor-col-resize select-none shrink-0" style="width:8px;touch-action:none;" role="separator" aria-orientation="vertical" title="드래그하여 표 너비를 조절합니다">
|
||
<div class="w-1.5 h-16 rounded-full bg-gray-300 hover:bg-blue-400 transition-colors"></div>
|
||
</div>
|
||
|
||
<section id="detail-card" class="border border-gray-300 rounded-lg bg-white overflow-hidden min-w-0 xl:flex-1">
|
||
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700">지정판매소 정보</div>
|
||
|
||
<form id="order-detail-form" action="<?= base_url('bag/order/phone/manage/update') ?>" method="POST" class="p-3 space-y-3">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="so_idx" id="detail-so-idx" value=""/>
|
||
|
||
<div class="border border-gray-200 rounded-lg p-3 bg-gray-50 text-sm">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-1">
|
||
<div><span class="font-semibold text-gray-700 inline-block w-20">접수번호</span> <span id="detail-so-no">-</span></div>
|
||
<div><span class="font-semibold text-gray-700 inline-block w-20">판매소코드</span> <span id="detail-shop-code">-</span></div>
|
||
<div><span class="font-semibold text-gray-700 inline-block w-20">판매소명</span> <span id="detail-shop-name">-</span></div>
|
||
<div><span class="font-semibold text-gray-700 inline-block w-20">담당자</span> <span id="detail-shop-rep">-</span></div>
|
||
<div><span class="font-semibold text-gray-700 inline-block w-20">전화번호</span> <span id="detail-shop-tel">-</span></div>
|
||
<div><span class="font-semibold text-gray-700 inline-block w-20">결제수단</span> <span id="detail-payment">-</span></div>
|
||
<div class="md:col-span-2"><span class="font-semibold text-gray-700 inline-block w-20">주소</span> <span id="detail-shop-addr">-</span></div>
|
||
<div><span class="font-semibold text-gray-700 inline-block w-20">접수일</span> <span id="detail-order-date">-</span></div>
|
||
<div class="flex items-center"><span class="font-semibold text-gray-700 inline-block w-20">배달일</span>
|
||
<input type="date" lang="ko" name="so_delivery_date" id="detail-delivery-date-input" class="border border-gray-300 rounded px-2 py-0.5 text-sm"/>
|
||
</div>
|
||
<div><span class="font-semibold text-gray-700 inline-block w-20">상태</span> <span id="detail-status">-</span></div>
|
||
<div><span class="font-semibold text-gray-700 inline-block w-20">입금상태</span> <span id="detail-paid-status" class="font-semibold">-</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap items-center justify-between gap-2 px-1">
|
||
<span class="text-sm font-semibold text-gray-700">접수 품목 내역</span>
|
||
<div class="flex items-center gap-2">
|
||
<button type="button" id="btn-toggle-paid" class="border border-emerald-300 text-emerald-700 px-3 py-1 rounded-sm text-sm hover:bg-emerald-50" disabled>입금확인</button>
|
||
<button type="submit" id="btn-save" class="bg-btn-search text-white px-3 py-1 rounded-sm text-sm shadow hover:opacity-90" disabled>주문 수정 저장</button>
|
||
<button type="button" id="btn-receipt" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50" disabled>입금자 영수증 출력</button>
|
||
<button type="submit" id="btn-cancel-order" form="order-cancel-form" class="border border-red-300 text-red-600 px-3 py-1 rounded-sm text-sm hover:bg-red-50" disabled>주문 취소</button>
|
||
</div>
|
||
</div>
|
||
<div 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-14 text-center 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-24 text-right sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">단가</th>
|
||
<th class="w-24 text-right sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">접수량</th>
|
||
<th class="w-24 text-right sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">포장량</th>
|
||
<th class="w-28 text-right sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">접수금액</th>
|
||
<th class="w-40 text-right sticky top-0 z-10" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">포장단위(박스/팩/낱장)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="detail-items-body">
|
||
<tr>
|
||
<td colspan="7" class="text-center py-6 text-gray-400">왼쪽 리스트에서 주문을 선택해 주세요.</td>
|
||
</tr>
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="font-semibold bg-gray-50">
|
||
<td colspan="3" class="text-right px-2 py-1">합계</td>
|
||
<td class="text-right px-2 py-1" id="detail-sum-qty">0</td>
|
||
<td class="text-right px-2 py-1" id="detail-sum-packed">0</td>
|
||
<td class="text-right px-2 py-1" id="detail-sum-amount">0</td>
|
||
<td class="text-right px-2 py-1" id="detail-sum-pack">박스=0, 팩=0, 낱장=0</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 봉투코드 스캔 → 즉시 판매·포장 처리 (선택된 주문의 판매소로 판매) -->
|
||
<div class="mt-3 border border-gray-300 rounded-lg p-2 bg-gray-50">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<span class="text-sm font-semibold text-gray-700 whitespace-nowrap"><i class="fa-solid fa-barcode text-emerald-600 mr-1"></i>봉투코드 스캔</span>
|
||
<input type="text" id="scan-input" autocomplete="off" placeholder="봉투 바코드를 스캔/입력 후 Enter" class="border border-gray-300 rounded px-2 py-1 text-sm flex-1 min-w-[12rem]" disabled/>
|
||
<span id="scan-msg" class="text-xs text-gray-500"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<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="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 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><!-- /스캔 현황 -->
|
||
</form>
|
||
|
||
<!-- 주문 취소 폼 — 버튼은 접수 품목 내역 타이틀 옆(form 속성으로 연결) -->
|
||
<form id="order-cancel-form" method="POST" class="hidden">
|
||
<?= csrf_field() ?>
|
||
</form>
|
||
</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">
|
||
<strong class="text-amber-950">[개발용 임시]</strong> 선택 주문의 판매소 기준 판매 테스트용 바코드 후보. <strong>바코드를 클릭하면 봉투코드 스캔칸에 입력</strong>됩니다. 개발 완료 후 이 블록과 API를 제거하세요.
|
||
</p>
|
||
<div class="max-h-52 overflow-auto border border-amber-300 bg-white">
|
||
<table class="w-full data-table text-xs">
|
||
<thead>
|
||
<tr>
|
||
<th class="w-28 text-left">출처</th>
|
||
<th class="w-44 text-center">바코드(대표)</th>
|
||
<th class="text-left">봉투 종류</th>
|
||
<th class="w-20 text-center">포장</th>
|
||
<th class="w-12 text-right">수량</th>
|
||
<th class="w-14 text-center">상태</th>
|
||
<th class="text-left">비고(낱장범위 등)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="dev-saleable-tbody">
|
||
<tr><td colspan="7" class="text-center text-gray-400 py-4">주문을 선택하면 목록이 표시됩니다.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(() => {
|
||
const orders = <?= json_encode($orders ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||
const orderMap = new Map(orders.map((o) => [String(o.so_idx), o]));
|
||
// 표시용 접수번호: 접수일(so_order_date)별로 매일 1번부터 순차 부여(그 날의 가장 오래된 건 = 1).
|
||
// 정렬과 무관하게 고정.
|
||
const dateKey = (o) => String(o && o.so_order_date ? o.so_order_date : '').slice(0, 10);
|
||
const displayNoMap = new Map();
|
||
(() => {
|
||
const perDate = {};
|
||
orders.slice().sort((a, b) => Number(a.so_idx || 0) - Number(b.so_idx || 0)).forEach((o) => {
|
||
const k = dateKey(o);
|
||
perDate[k] = (perDate[k] || 0) + 1;
|
||
displayNoMap.set(String(o.so_idx), perDate[k]);
|
||
});
|
||
})();
|
||
const displayNo = (o) => displayNoMap.get(String(o && o.so_idx)) || '';
|
||
|
||
// 봉투코드 스캔(판매/포장) — CSRF 토큰(쿠키 방식, 매 요청 갱신)
|
||
// base_url() 절대경로를 현재 문서(동일 출처) 기준 경로로 변환 → 워크스페이스 iframe/다른 호스트 접근에도 안전
|
||
const scanApi = new URL('<?= base_url('bag/order/phone/manage/scan') ?>', window.location.href).pathname;
|
||
// [개발용 임시] 판매 가능 바코드 후보 API (지정판매소 판매의 dev API 재사용)
|
||
const devSaleableApi = new URL('<?= base_url('bag/sale/designated/dev-saleable-barcodes') ?>', window.location.href).pathname;
|
||
const csrfName = '<?= csrf_token() ?>';
|
||
let csrfHash = '<?= csrf_hash() ?>';
|
||
|
||
const listBody = document.getElementById('order-list-body');
|
||
const form = document.getElementById('order-detail-form');
|
||
const cancelForm = document.getElementById('order-cancel-form');
|
||
const detailBody = document.getElementById('detail-items-body');
|
||
const inputSoIdx = document.getElementById('detail-so-idx');
|
||
const btnSave = document.getElementById('btn-save');
|
||
const btnCancel = document.getElementById('btn-cancel-order');
|
||
const btnReceipt = document.getElementById('btn-receipt');
|
||
const btnTogglePaid = document.getElementById('btn-toggle-paid');
|
||
const paidApi = new URL('<?= base_url('bag/order/phone/manage/paid') ?>', window.location.href).pathname;
|
||
const receiptBase = '<?= base_url('bag/order/phone/receipt') ?>';
|
||
btnReceipt?.addEventListener('click', () => {
|
||
const id = inputSoIdx.value;
|
||
if (!id) return;
|
||
const o = orderMap.get(String(id));
|
||
// 이미 포장(수령)이 완료된 건의 영수증 재발행 시 안내
|
||
if (o && Number(o.so_received) === 1) {
|
||
if (!confirm('포장이 완료된 건입니다.\n그래도 영수증을 다시 출력할까요?')) return;
|
||
}
|
||
window.open(receiptBase + '/' + encodeURIComponent(id), '_blank');
|
||
});
|
||
|
||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||
|
||
function paintPaidButton(order) {
|
||
if (!btnTogglePaid) return;
|
||
const paid = order && Number(order.so_paid) === 1;
|
||
btnTogglePaid.textContent = paid ? '입금확인 취소' : '입금확인';
|
||
btnTogglePaid.className = paid
|
||
? 'border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50'
|
||
: 'border border-emerald-300 text-emerald-700 px-3 py-1 rounded-sm text-sm hover:bg-emerald-50';
|
||
}
|
||
|
||
btnTogglePaid?.addEventListener('click', async () => {
|
||
const id = inputSoIdx.value;
|
||
if (!id) return;
|
||
const o = orderMap.get(String(id));
|
||
if (!o) return;
|
||
const nextPaid = Number(o.so_paid) === 1 ? 0 : 1;
|
||
if (nextPaid === 0 && !confirm('입금확인을 취소하면 이 주문은 포장(스캔)이 차단됩니다.\n계속할까요?')) return;
|
||
const body = new URLSearchParams();
|
||
body.set(csrfName, csrfHash);
|
||
body.set('so_idx', String(id));
|
||
body.set('paid', String(nextPaid));
|
||
btnTogglePaid.disabled = true;
|
||
try {
|
||
const res = await fetch(paidApi, { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body });
|
||
const data = await res.json();
|
||
if (data && data.csrf) csrfHash = data.csrf;
|
||
if (!data || !data.ok) { alert((data && data.message) || '처리에 실패했습니다.'); btnTogglePaid.disabled = false; return; }
|
||
o.so_paid = nextPaid;
|
||
setHeader(o);
|
||
paintPaidButton(o);
|
||
btnTogglePaid.disabled = false;
|
||
renderList(); // 리스트의 입금 표시 갱신
|
||
} catch (e) {
|
||
alert('네트워크 오류로 처리하지 못했습니다.');
|
||
btnTogglePaid.disabled = false;
|
||
}
|
||
});
|
||
|
||
function setHeader(order) {
|
||
var t = function (id, v) { document.getElementById(id).textContent = (v === undefined || v === null || v === '') ? '-' : v; };
|
||
t('detail-so-no', order ? displayNo(order) : '');
|
||
t('detail-shop-code', order ? order.ds_shop_no : '');
|
||
t('detail-shop-name', order ? order.so_ds_name : '');
|
||
t('detail-shop-rep', order ? order.ds_rep_name : '');
|
||
t('detail-shop-tel', order ? order.ds_tel : '');
|
||
t('detail-payment', order ? order.so_payment_type : '');
|
||
t('detail-shop-addr', order ? order.ds_addr : '');
|
||
t('detail-order-date', order ? order.so_order_date : '');
|
||
t('detail-status', order ? ((order.so_status === 'cancelled') ? '취소' : '정상') : '');
|
||
var ps = document.getElementById('detail-paid-status');
|
||
if (ps) {
|
||
var paid = order && Number(order.so_paid) === 1;
|
||
ps.textContent = order ? (paid ? '입금완료' : '미입금') : '-';
|
||
ps.style.color = order ? (paid ? '#059669' : '#e11d48') : '';
|
||
}
|
||
// 배달일 — 수정 가능한 date 입력
|
||
var dd = document.getElementById('detail-delivery-date-input');
|
||
if (dd) {
|
||
var v = order && order.so_delivery_date ? String(order.so_delivery_date) : '';
|
||
dd.value = /^\d{4}-\d{2}-\d{2}/.test(v) ? v.slice(0, 10) : '';
|
||
dd.disabled = !order || order.so_status === 'cancelled';
|
||
// 배달일은 접수일 이후로만 선택 가능
|
||
var od = order && order.so_order_date ? String(order.so_order_date).slice(0, 10) : '';
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(od)) { dd.min = od; } else { dd.removeAttribute('min'); }
|
||
}
|
||
}
|
||
|
||
function calcRow(tr) {
|
||
const qtyInput = tr.querySelector('.item-qty-input');
|
||
const qty = Math.max(0, parseInt(qtyInput.value || '0', 10) || 0);
|
||
qtyInput.value = String(qty);
|
||
|
||
const unitPrice = parseInt(tr.dataset.unitPrice || '0', 10) || 0;
|
||
const boxSheets = parseInt(tr.dataset.boxSheets || '0', 10) || 0;
|
||
const packSheets = parseInt(tr.dataset.packSheets || '0', 10) || 0;
|
||
|
||
let box = 0;
|
||
let pack = 0;
|
||
let sheet = qty;
|
||
if (boxSheets > 0) {
|
||
box = Math.floor(qty / boxSheets);
|
||
const remain = qty % boxSheets;
|
||
if (packSheets > 0) {
|
||
pack = Math.floor(remain / packSheets);
|
||
sheet = remain % packSheets;
|
||
} else {
|
||
sheet = remain;
|
||
}
|
||
} else if (packSheets > 0) {
|
||
pack = Math.floor(qty / packSheets);
|
||
sheet = qty % packSheets;
|
||
}
|
||
|
||
const packedCell = tr.querySelector('.item-packed-cell');
|
||
const packed = packedCell ? Math.max(0, parseInt((packedCell.textContent || '0').replace(/[^0-9]/g, '') || '0', 10) || 0) : 0;
|
||
|
||
const amount = qty * unitPrice;
|
||
tr.querySelector('.item-amount-cell').textContent = nf(amount);
|
||
tr.querySelector('.item-pack-cell').textContent = `박스=${nf(box)}, 팩=${nf(pack)}, 낱장=${nf(sheet)}`;
|
||
return { qty, packed, amount, box, pack, sheet };
|
||
}
|
||
|
||
function recalcTotals() {
|
||
let sumQty = 0;
|
||
let sumPacked = 0;
|
||
let sumAmount = 0;
|
||
let sumBox = 0;
|
||
let sumPack = 0;
|
||
let sumSheet = 0;
|
||
|
||
detailBody.querySelectorAll('tr.order-item-row').forEach((tr) => {
|
||
const r = calcRow(tr);
|
||
sumQty += r.qty;
|
||
sumPacked += r.packed;
|
||
sumAmount += r.amount;
|
||
sumBox += r.box;
|
||
sumPack += r.pack;
|
||
sumSheet += r.sheet;
|
||
});
|
||
|
||
document.getElementById('detail-sum-qty').textContent = nf(sumQty);
|
||
document.getElementById('detail-sum-packed').textContent = nf(sumPacked);
|
||
document.getElementById('detail-sum-amount').textContent = nf(sumAmount);
|
||
document.getElementById('detail-sum-pack').textContent = `박스=${nf(sumBox)}, 팩=${nf(sumPack)}, 낱장=${nf(sumSheet)}`;
|
||
}
|
||
|
||
function renderDetail(orderId) {
|
||
const order = orderMap.get(String(orderId));
|
||
if (!order) return;
|
||
|
||
inputSoIdx.value = String(order.so_idx || '');
|
||
setHeader(order);
|
||
cancelForm.action = `<?= base_url('bag/order/phone/manage/cancel') ?>/${order.so_idx}`;
|
||
|
||
const isCancelled = order.so_status === 'cancelled';
|
||
btnSave.disabled = isCancelled;
|
||
btnCancel.disabled = isCancelled;
|
||
if (btnReceipt) btnReceipt.disabled = false;
|
||
if (btnTogglePaid) { btnTogglePaid.disabled = isCancelled; paintPaidButton(order); }
|
||
|
||
renderPacking(order); // 포장 명세 탭 동기화
|
||
|
||
const items = Array.isArray(order.items) ? order.items : [];
|
||
if (items.length === 0) {
|
||
detailBody.innerHTML = '<tr><td colspan="7" class="text-center py-6 text-gray-400">품목 정보가 없습니다.</td></tr>';
|
||
recalcTotals();
|
||
return;
|
||
}
|
||
|
||
detailBody.innerHTML = items.map((item, idx) => {
|
||
const itemId = String(item.soi_idx || '');
|
||
const bagName = `${item.soi_bag_code || ''} ${item.soi_bag_name || ''}`.trim();
|
||
const qty = parseInt(item.soi_qty || 0, 10) || 0;
|
||
const packedQty = parseInt(item.soi_packed_qty || 0, 10) || 0;
|
||
const unitPrice = parseInt(item.soi_unit_price || 0, 10) || 0;
|
||
const amount = parseInt(item.soi_amount || 0, 10) || 0;
|
||
const box = parseInt(item.soi_box_count || 0, 10) || 0;
|
||
const pack = parseInt(item.soi_pack_count || 0, 10) || 0;
|
||
const sheet = parseInt(item.soi_sheet_count || 0, 10) || 0;
|
||
const boxSheets = parseInt(item.box_sheets || 0, 10) || 0;
|
||
const packSheets = parseInt(item.pack_sheets || 0, 10) || 0;
|
||
|
||
return `
|
||
<tr class="order-item-row" data-unit-price="${unitPrice}" data-box-sheets="${boxSheets}" data-pack-sheets="${packSheets}">
|
||
<td class="text-center">${idx + 1}</td>
|
||
<td class="text-left pl-2">${bagName}</td>
|
||
<td class="text-right pr-2">${nf(unitPrice)}</td>
|
||
<td class="text-right pr-2">
|
||
<input type="number" min="0" class="item-qty-input border border-gray-300 rounded px-2 py-1 w-24 text-right" name="item_qty[${itemId}]" value="${qty}" ${isCancelled ? 'disabled' : ''}/>
|
||
</td>
|
||
<td class="text-right pr-2 item-packed-cell font-semibold ${packedQty >= qty && qty > 0 ? 'text-emerald-600' : 'text-gray-700'}" data-bag-code="${esc(item.soi_bag_code || '')}">${nf(packedQty)}</td>
|
||
<td class="text-right pr-2 item-amount-cell">${nf(amount)}</td>
|
||
<td class="text-right pr-2 item-pack-cell">박스=${nf(box)}, 팩=${nf(pack)}, 낱장=${nf(sheet)}</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
|
||
recalcTotals();
|
||
|
||
// 봉투코드 스캔 입력: 정상 주문일 때만 활성화
|
||
const scanInput = document.getElementById('scan-input');
|
||
if (scanInput) {
|
||
scanInput.disabled = isCancelled;
|
||
const msg = document.getElementById('scan-msg');
|
||
if (msg) msg.textContent = '';
|
||
}
|
||
// [개발용 임시] 선택 주문 판매소 기준 후보 바코드 표
|
||
loadDevSaleable(order.so_ds_idx, order.so_idx);
|
||
|
||
syncListHeight(); // 상세 카드 높이 확정 후 접수 리스트 높이를 맞춘다
|
||
}
|
||
|
||
// [개발용 임시] 스캔 가능 바코드 후보 로딩(선택 주문 판매소·주문 기준)
|
||
const devPanelEl = document.getElementById('dev-saleable-panel');
|
||
const devTbodyEl = document.getElementById('dev-saleable-tbody');
|
||
async function loadDevSaleable(dsIdx, soIdx) {
|
||
if (!devPanelEl || !devTbodyEl) return;
|
||
const idx = Number(dsIdx || 0);
|
||
if (!idx) { devPanelEl.classList.add('hidden'); return; }
|
||
devPanelEl.classList.remove('hidden');
|
||
devTbodyEl.innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-4">불러오는 중…</td></tr>';
|
||
try {
|
||
let url = devSaleableApi + '?ds_idx=' + encodeURIComponent(String(idx));
|
||
if (Number(soIdx || 0) > 0) url += '&so_idx=' + encodeURIComponent(String(soIdx));
|
||
const res = await fetch(url, { credentials: 'same-origin' });
|
||
const data = await res.json();
|
||
if (!data.ok) { devTbodyEl.innerHTML = '<tr><td colspan="7" class="text-center text-red-600 py-4">' + esc(data.message || '조회 실패') + '</td></tr>'; return; }
|
||
const rows = Array.isArray(data.rows) ? data.rows : [];
|
||
if (!rows.length) { devTbodyEl.innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-4">표시할 바코드가 없습니다.</td></tr>'; return; }
|
||
devTbodyEl.innerHTML = rows.map((r) => {
|
||
const code = String(r.code || '');
|
||
const codeCell = (code && code !== '-')
|
||
? '<button type="button" class="dev-pick text-blue-600 hover:underline font-mono" data-code="' + esc(code) + '">' + esc(code) + '</button>'
|
||
: '<span class="text-gray-400">' + esc(code) + '</span>';
|
||
return '<tr>'
|
||
+ '<td class="text-left pl-1">' + esc(r.source || '') + '</td>'
|
||
+ '<td class="text-center">' + codeCell + '</td>'
|
||
+ '<td class="text-left pl-1">' + esc((r.bag_code || '') + ' ' + (r.bag_name || '')) + '</td>'
|
||
+ '<td class="text-center">' + esc(r.unit || '') + '</td>'
|
||
+ '<td class="text-right pr-1">' + nf(r.qty || 0) + '</td>'
|
||
+ '<td class="text-center">' + esc(r.state || '') + '</td>'
|
||
+ '<td class="text-left pl-1">' + esc(r.extra || '') + '</td>'
|
||
+ '</tr>';
|
||
}).join('');
|
||
} catch (e) {
|
||
devTbodyEl.innerHTML = '<tr><td colspan="7" class="text-center text-red-600 py-4">네트워크 오류</td></tr>';
|
||
}
|
||
}
|
||
// 후보 바코드 클릭 → 스캔칸에 입력(개발 편의)
|
||
devTbodyEl?.addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.dev-pick');
|
||
if (!btn) return;
|
||
const inp = document.getElementById('scan-input');
|
||
if (inp && !inp.disabled) { inp.value = btn.getAttribute('data-code') || ''; inp.focus(); }
|
||
});
|
||
|
||
// 봉투코드 스캔 → 즉시 판매·포장 처리 후 화면 갱신
|
||
const scanInputEl = document.getElementById('scan-input');
|
||
const scanMsgEl = document.getElementById('scan-msg');
|
||
function setScanMsg(text, isErr) {
|
||
if (!scanMsgEl) return;
|
||
scanMsgEl.textContent = text || '';
|
||
scanMsgEl.className = 'text-xs ' + (isErr ? 'text-red-600 font-semibold' : 'text-emerald-600');
|
||
}
|
||
async function submitScan() {
|
||
if (!scanInputEl) return;
|
||
const code = (scanInputEl.value || '').trim();
|
||
if (!code) return;
|
||
if (!selectedId) { setScanMsg('먼저 접수 리스트에서 주문을 선택해 주세요.', true); return; }
|
||
|
||
const body = new URLSearchParams();
|
||
body.set(csrfName, csrfHash);
|
||
body.set('so_idx', String(selectedId));
|
||
body.set('barcode', code);
|
||
|
||
let data;
|
||
try {
|
||
const res = await fetch(scanApi, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||
credentials: 'same-origin',
|
||
body: body.toString(),
|
||
});
|
||
data = await res.json();
|
||
} catch (e) {
|
||
setScanMsg('통신 오류가 발생했습니다.', true);
|
||
return;
|
||
}
|
||
if (data && data.csrf) csrfHash = data.csrf; // 다음 스캔용 토큰 갱신
|
||
if (!data || !data.ok) {
|
||
setScanMsg((data && data.message) || '스캔 처리 실패', true);
|
||
scanInputEl.select();
|
||
return;
|
||
}
|
||
|
||
// 선택된 주문 객체에 스캔 반영(재선택 시 유지)
|
||
const order = orderMap.get(String(data.so_idx));
|
||
if (order) {
|
||
if (!Array.isArray(order.scans)) order.scans = [];
|
||
order.scans.push({ bag_code: data.bag_code, bag_name: data.bag_name, code: data.code, unit: data.unit, qty: Number(data.qty || 0) });
|
||
const it = (order.items || []).find((x) => String(x.soi_bag_code) === String(data.bag_code));
|
||
if (it) it.soi_packed_qty = Number(data.packed_qty || 0);
|
||
order.so_received = Number(data.so_received || 0);
|
||
// 현재 이 주문을 보고 있으면 상세·포장명세 즉시 갱신
|
||
if (String(selectedId) === String(data.so_idx)) {
|
||
renderPacking(order);
|
||
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));
|
||
const row = cell.closest('tr');
|
||
const qtyInp = row ? row.querySelector('.item-qty-input') : null;
|
||
const rq = qtyInp ? (parseInt(qtyInp.value || '0', 10) || 0) : 0;
|
||
cell.classList.toggle('text-emerald-600', Number(data.packed_qty || 0) >= rq && rq > 0);
|
||
cell.classList.toggle('text-gray-700', !(Number(data.packed_qty || 0) >= rq && rq > 0));
|
||
}
|
||
recalcTotals();
|
||
}
|
||
// 접수 리스트의 '포장' 표시(O/X) 갱신
|
||
renderList();
|
||
// [개발용 임시] 후보 목록 갱신(방금 판매된 팩 제외)
|
||
loadDevSaleable(order.so_ds_idx, order.so_idx);
|
||
}
|
||
scanInputEl.value = '';
|
||
scanInputEl.focus();
|
||
setScanMsg('등록: ' + data.bag_code + ' / ' + data.unit + ' / ' + nf(data.qty) + ' (잔량 ' + nf(data.remain) + ')', false);
|
||
}
|
||
scanInputEl?.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') { e.preventDefault(); submitScan(); }
|
||
});
|
||
|
||
// 접수 리스트 스크롤 높이를 지정판매소 정보(상세) 카드 높이에 맞춘다.
|
||
// offsetHeight는 화면 zoom(텍스트 줄이기)과 무관한 레이아웃 px라, 배율과 상관없이 두 카드가 같은 높이가 되고 하단 공백이 생기지 않는다.
|
||
const listScrollEl = document.getElementById('list-scroll');
|
||
const detailCardEl = document.getElementById('detail-card');
|
||
function syncListHeight() {
|
||
if (!listScrollEl || !detailCardEl) return;
|
||
const listCard = listScrollEl.closest('section');
|
||
const header = listCard ? listCard.firstElementChild : null;
|
||
const summary = document.getElementById('list-summary');
|
||
listScrollEl.style.height = '0px'; // 리스트를 접어 상세 카드의 본래 높이를 측정
|
||
const target = detailCardEl.offsetHeight
|
||
- (header ? header.offsetHeight : 0)
|
||
- (summary ? summary.offsetHeight : 0);
|
||
listScrollEl.style.height = Math.max(160, target) + 'px';
|
||
}
|
||
window.addEventListener('resize', syncListHeight);
|
||
window.addEventListener('storage', function (e) { if (e.key === 'jrj_font_scale') syncListHeight(); });
|
||
if ('ResizeObserver' in window && detailCardEl) {
|
||
new ResizeObserver(function () { syncListHeight(); }).observe(detailCardEl);
|
||
}
|
||
|
||
// ── 접수 리스트 ↔ 지정판매소 정보 사이 너비 조절(드래그 스플리터) ──────
|
||
// 손잡이를 접수 리스트 쪽(왼쪽)으로 끌면 리스트 폭이 줄고 지정판매소 정보 폭이 넓어진다.
|
||
const splitEl = document.getElementById('order-split');
|
||
const listCardEl = document.getElementById('list-card');
|
||
const splitHandleEl = document.getElementById('split-handle');
|
||
const SPLIT_KEY = 'jrj_order_split_pct';
|
||
const SPLIT_MIN = 25, SPLIT_MAX = 75, SPLIT_DEFAULT = 55.5; // 기본값 ≈ 기존 5:4 비율
|
||
const xlMedia = window.matchMedia('(min-width: 1280px)');
|
||
function clampPct(p) { return Math.min(SPLIT_MAX, Math.max(SPLIT_MIN, p)); }
|
||
function currentPct() {
|
||
const saved = parseFloat(localStorage.getItem(SPLIT_KEY) || '');
|
||
return clampPct(isFinite(saved) ? saved : SPLIT_DEFAULT);
|
||
}
|
||
function applySplit(pct) {
|
||
if (!listCardEl) return;
|
||
if (xlMedia.matches) {
|
||
// 좌우 배치일 때만 폭 지정(세로로 쌓이는 좁은 화면에서는 전체폭 사용)
|
||
listCardEl.style.flex = '0 0 ' + pct + '%';
|
||
listCardEl.style.maxWidth = pct + '%';
|
||
} else {
|
||
listCardEl.style.flex = '';
|
||
listCardEl.style.maxWidth = '';
|
||
}
|
||
syncListHeight();
|
||
}
|
||
if (splitHandleEl && splitEl && listCardEl) {
|
||
let dragging = false, rafId = 0, pendingPct = null;
|
||
function onMove(clientX) {
|
||
const rect = splitEl.getBoundingClientRect();
|
||
if (rect.width <= 0) return;
|
||
pendingPct = clampPct((clientX - rect.left) / rect.width * 100);
|
||
if (!rafId) {
|
||
rafId = requestAnimationFrame(function () {
|
||
rafId = 0;
|
||
if (pendingPct != null) applySplit(pendingPct);
|
||
});
|
||
}
|
||
}
|
||
function pointerMove(e) { if (dragging) { e.preventDefault(); onMove(e.clientX); } }
|
||
function pointerUp() {
|
||
if (!dragging) return;
|
||
dragging = false;
|
||
document.body.style.userSelect = '';
|
||
document.body.style.cursor = '';
|
||
window.removeEventListener('pointermove', pointerMove);
|
||
window.removeEventListener('pointerup', pointerUp);
|
||
if (pendingPct != null) localStorage.setItem(SPLIT_KEY, String(Math.round(pendingPct * 10) / 10));
|
||
}
|
||
splitHandleEl.addEventListener('pointerdown', function (e) {
|
||
if (!xlMedia.matches) return; // 좁은 화면에서는 비활성
|
||
e.preventDefault();
|
||
dragging = true;
|
||
document.body.style.userSelect = 'none';
|
||
document.body.style.cursor = 'col-resize';
|
||
window.addEventListener('pointermove', pointerMove);
|
||
window.addEventListener('pointerup', pointerUp);
|
||
});
|
||
// 더블클릭 시 기본 비율로 초기화
|
||
splitHandleEl.addEventListener('dblclick', function () {
|
||
localStorage.removeItem(SPLIT_KEY);
|
||
applySplit(SPLIT_DEFAULT);
|
||
});
|
||
// 브레이크포인트 전환 시 폭 지정 적용/해제
|
||
(xlMedia.addEventListener ? xlMedia.addEventListener('change', function () { applySplit(currentPct()); })
|
||
: xlMedia.addListener(function () { applySplit(currentPct()); }));
|
||
applySplit(currentPct());
|
||
}
|
||
|
||
// ── 스캔 현황(최근 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])); }
|
||
|
||
// 스캔 코드 전체 → 표 행(HTML) + 건수/합계 (팝업용). 최근이 위로.
|
||
function scanFullRows(order) {
|
||
const scans = Array.isArray(order.scans) ? order.scans.slice().reverse() : [];
|
||
let no = 0, total = 0;
|
||
const rows = scans.map((s) => {
|
||
no++;
|
||
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>';
|
||
});
|
||
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;
|
||
const esc = (s) => String(s == null ? '' : s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
||
|
||
function rowHtml(o) {
|
||
const cancelled = o.so_status === 'cancelled';
|
||
const dDate = o.so_delivery_date ? String(o.so_delivery_date).slice(0, 10) : '';
|
||
const channel = (o.so_channel && o.so_channel !== 'phone') ? o.so_channel : '전화';
|
||
const packed = Number(o.so_received) === 1;
|
||
return `<tr class="order-list-row cursor-pointer hover:bg-blue-50 ${cancelled ? 'bg-gray-50 text-gray-400' : ''} ${String(o.so_idx) === String(selectedId) ? 'bg-blue-100' : ''}" data-order-id="${o.so_idx}">
|
||
<td class="text-center">${displayNo(o)}</td>
|
||
<td class="text-center">${esc(o.so_order_date)}</td>
|
||
<td class="text-center">${esc(dDate)}</td>
|
||
<td class="text-center">${esc(channel)}</td>
|
||
<td class="text-left pl-2">${esc(o.so_ds_name)}</td>
|
||
<td class="text-center">${esc(o.so_payment_type || '-')}</td>
|
||
<td class="text-center" style="text-align:center;">${packed ? '<span style="color:#2563eb;font-weight:700;">O</span>' : '<span class="text-gray-400">X</span>'}</td>
|
||
<td class="text-center" style="text-align:center;">${Number(o.so_paid) === 1 ? '<span style="color:#e11d48;font-size:11px;" title="입금">●</span>' : ''}</td>
|
||
<td class="text-right pr-2">${nf(o.so_total_amount || 0)}</td>
|
||
<td class="text-center">${cancelled ? '취소' : '정상'}</td>
|
||
</tr>`;
|
||
}
|
||
|
||
function renderList() {
|
||
const th = document.querySelector('.sort-th[data-sort="' + sortKey + '"]');
|
||
const type = th ? th.dataset.type : 'str';
|
||
const arr = orders.slice().sort((a, b) => {
|
||
let va = a[sortKey], vb = b[sortKey];
|
||
if (type === 'num') { va = Number(va || 0); vb = Number(vb || 0); return sortDir === 'asc' ? va - vb : vb - va; }
|
||
va = String(va || ''); vb = String(vb || '');
|
||
return sortDir === 'asc' ? va.localeCompare(vb, 'ko') : vb.localeCompare(va, 'ko');
|
||
});
|
||
listBody.innerHTML = arr.length
|
||
? arr.map(rowHtml).join('')
|
||
: '<tr><td colspan="10" class="text-center py-8 text-gray-400">전화 주문 데이터가 없습니다.</td></tr>';
|
||
document.querySelectorAll('.sort-th .sort-ind').forEach((s) => { s.textContent = ''; });
|
||
if (th) th.querySelector('.sort-ind').textContent = sortDir === 'asc' ? ' ▲' : ' ▼';
|
||
|
||
// 하단 합계 — 취소 주문은 제외
|
||
const normals = arr.filter((o) => o.so_status !== 'cancelled');
|
||
const sumCountEl = document.getElementById('list-sum-count');
|
||
const sumAmountEl = document.getElementById('list-sum-amount');
|
||
if (sumCountEl) sumCountEl.textContent = nf(normals.length);
|
||
if (sumAmountEl) sumAmountEl.textContent = nf(normals.reduce((s, o) => s + (Number(o.so_total_amount) || 0), 0));
|
||
}
|
||
|
||
document.querySelectorAll('.sort-th').forEach((th) => {
|
||
th.addEventListener('click', () => {
|
||
const k = th.dataset.sort;
|
||
if (sortKey === k) { sortDir = sortDir === 'asc' ? 'desc' : 'asc'; } else { sortKey = k; sortDir = 'asc'; }
|
||
renderList();
|
||
});
|
||
});
|
||
|
||
listBody?.addEventListener('click', (e) => {
|
||
const tr = e.target.closest('.order-list-row');
|
||
if (!tr) return;
|
||
selectedId = tr.dataset.orderId;
|
||
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) => {
|
||
if (e.target.closest('.item-qty-input') || e.target.closest('.item-packed-input')) {
|
||
recalcTotals();
|
||
}
|
||
});
|
||
|
||
cancelForm?.addEventListener('submit', (e) => {
|
||
if (!confirm('해당 주문을 취소 처리하시겠습니까? (삭제되지 않고 상태만 취소로 변경됩니다)')) {
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
|
||
form?.addEventListener('submit', (e) => {
|
||
if (!inputSoIdx.value) {
|
||
e.preventDefault();
|
||
alert('수정할 주문을 먼저 선택해 주세요.');
|
||
}
|
||
});
|
||
|
||
renderList();
|
||
const firstRow = listBody?.querySelector('.order-list-row');
|
||
if (firstRow) {
|
||
selectedId = firstRow.dataset.orderId;
|
||
firstRow.classList.add('bg-blue-100');
|
||
renderDetail(selectedId);
|
||
}
|
||
syncListHeight();
|
||
})();
|
||
</script>
|
||
|
||
<?= view('bag/_dev_all_sales_panel') ?>
|