통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
521 lines
21 KiB
PHP
521 lines
21 KiB
PHP
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
|
<span class="text-sm font-bold text-gray-700">지정판매소 판매</span>
|
|
</section>
|
|
|
|
<div class="border border-gray-300 p-3 mt-2 bg-white space-y-3">
|
|
<form id="sale-save-form" action="<?= base_url('bag/sale/designated/save') ?>" method="POST">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="so_idx" id="save-so-idx" value=""/>
|
|
<input type="hidden" name="ds_idx" id="save-ds-idx" value=""/>
|
|
<input type="hidden" name="scans_json" id="save-scans-json" value="[]"/>
|
|
|
|
<div class="flex flex-wrap items-end gap-2">
|
|
<div class="relative">
|
|
<label class="block text-xs font-bold text-gray-700 mb-1">판매소 검색</label>
|
|
<input id="shop-search" type="text" class="border border-gray-300 rounded px-2 py-1.5 text-sm w-[36rem] max-w-full" placeholder="코드/상호/대표자/전화/주소"/>
|
|
<datalist id="shop-search-list" class="hidden">
|
|
<?php foreach (($shops ?? []) as $shop): ?>
|
|
<option value="<?= esc(trim(($shop->ds_shop_no ?? '') . ' ' . ($shop->ds_name ?? '') . ' ' . ($shop->ds_rep_name ?? '') . ' ' . ($shop->ds_tel ?? '') . ' ' . ($shop->ds_addr ?? ''))) ?>"></option>
|
|
<?php endforeach; ?>
|
|
</datalist>
|
|
<div id="shop-search-suggest" class="hidden absolute left-0 top-full mt-1 w-[36rem] max-w-full max-h-56 overflow-auto border border-gray-300 bg-white shadow-lg z-20"></div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-700 mb-1">봉투코드 입력(스캔)</label>
|
|
<input id="barcode-input" type="text" class="border border-gray-300 rounded px-2 py-1.5 text-sm w-80" placeholder="박스/팩/낱장 바코드"/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-gray-700 mb-1">판매일</label>
|
|
<div class="border border-gray-300 bg-gray-100 rounded px-2 py-1.5 text-sm w-36"><?= esc(date('Y-m-d')) ?></div>
|
|
</div>
|
|
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">판매 저장</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div id="scan-message" class="text-sm text-gray-600"></div>
|
|
|
|
<div class="grid grid-cols-1 xl:grid-cols-5 gap-3">
|
|
<section class="xl:col-span-2 border border-gray-300">
|
|
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">지정판매소 정보</div>
|
|
<table class="w-full text-sm">
|
|
<tr><th class="text-left px-3 py-1.5 w-28">판매소 코드</th><td id="shop-info-code" class="px-3 py-1.5">-</td></tr>
|
|
<tr><th class="text-left px-3 py-1.5">상호</th><td id="shop-info-name" class="px-3 py-1.5">-</td></tr>
|
|
<tr><th class="text-left px-3 py-1.5">대표자명</th><td id="shop-info-rep" class="px-3 py-1.5">-</td></tr>
|
|
<tr><th class="text-left px-3 py-1.5">대표전화</th><td id="shop-info-tel" class="px-3 py-1.5">-</td></tr>
|
|
<tr><th class="text-left px-3 py-1.5">주소</th><td id="shop-info-addr" class="px-3 py-1.5">-</td></tr>
|
|
</table>
|
|
</section>
|
|
|
|
<section class="xl:col-span-3 border border-gray-300">
|
|
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">주문 접수 리스트</div>
|
|
<div class="max-h-56 overflow-auto">
|
|
<table class="w-full data-table text-sm">
|
|
<thead>
|
|
<tr>
|
|
<th class="w-14">번호</th>
|
|
<th>판매소</th>
|
|
<th class="w-28">접수일</th>
|
|
<th class="w-28">배달일</th>
|
|
<th class="w-16">상태</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="order-list-body"></tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<section id="dev-saleable-panel" class="hidden border border-amber-400 bg-amber-50/50 p-3 rounded-sm">
|
|
<p class="text-xs text-amber-900 mb-2 leading-relaxed">
|
|
<strong class="text-amber-950">[개발용 임시]</strong>
|
|
<strong>주문 접수 리스트</strong>에서 주문을 선택하면, 그 주문의 지정판매소(<code class="bg-amber-100 px-1 rounded">so_ds_idx</code>) 기준으로
|
|
판매 테스트에 쓸 수 있는 바코드 후보를 표시합니다.
|
|
(① 해당 판매소에 연결된 <code class="bg-amber-100 px-1 rounded">bag_sale_scan_code</code> 중 <code class="bg-amber-100 px-1 rounded">in_stock</code>,
|
|
② 같은 판매소의 <strong>전화·정상 주문</strong> 품목 봉투코드(수령완료 포함, 주문 리스트와 동일 범위) 및 <strong>선택한 주문</strong> 품목에 맞는 <code class="bg-amber-100 px-1 rounded">bag_receiving_pack_code</code> <code class="bg-amber-100 px-1 rounded">in_stock</code> 팩 코드.
|
|
입고 행의 <strong>수량</strong>은 팩에 담긴 <code class="bg-amber-100 px-1 rounded">brpc_sheet_qty</code>(낱장 수)입니다.)
|
|
<strong>개발 완료 후 이 블록과 API 라우트를 제거</strong>해 주세요.
|
|
</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">출처</th>
|
|
<th class="w-40">바코드(대표)</th>
|
|
<th>봉투 종류</th>
|
|
<th class="w-16">포장</th>
|
|
<th class="w-10">수량</th>
|
|
<th class="w-12">주문</th>
|
|
<th class="w-14">상태</th>
|
|
<th>비고(낱장범위 등)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="dev-saleable-tbody">
|
|
<tr><td colspan="8" class="text-center text-gray-400 py-4">주문을 선택하면 목록이 표시됩니다.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-3">
|
|
<section class="border border-gray-300">
|
|
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">판매 내역</div>
|
|
<div class="max-h-72 overflow-auto">
|
|
<table class="w-full data-table text-sm">
|
|
<thead>
|
|
<tr>
|
|
<th class="w-14">선택</th>
|
|
<th>봉투 종류</th>
|
|
<th class="w-20">접수량</th>
|
|
<th class="w-20">판매량</th>
|
|
<th class="w-20">단가</th>
|
|
<th class="w-24">판매금액</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="sale-items-body">
|
|
<tr><td colspan="6" class="text-center text-gray-400 py-6">주문을 선택해 주세요.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="border border-gray-300">
|
|
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">판매 상세 내역</div>
|
|
<div class="max-h-72 overflow-auto">
|
|
<table class="w-full data-table text-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>봉투 종류</th>
|
|
<th class="w-28">봉투 코드</th>
|
|
<th class="w-16">수량</th>
|
|
<th class="w-16">포장단위</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="scan-detail-body">
|
|
<tr><td colspan="4" class="text-center text-gray-400 py-6">바코드를 스캔해 주세요.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(() => {
|
|
const shops = <?= json_encode(array_map(static function ($s): array {
|
|
return [
|
|
'ds_idx' => (int) ($s->ds_idx ?? 0),
|
|
'ds_shop_no' => (string) ($s->ds_shop_no ?? ''),
|
|
'ds_name' => (string) ($s->ds_name ?? ''),
|
|
'ds_rep_name' => (string) ($s->ds_rep_name ?? ''),
|
|
'ds_tel' => (string) ($s->ds_tel ?? ''),
|
|
'ds_addr' => trim((string) ($s->ds_addr ?? '') . ' ' . (string) ($s->ds_addr_detail ?? '')),
|
|
];
|
|
}, $shops ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
const orders = <?= json_encode($orders ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
const scanApi = '<?= base_url('bag/sale/designated/scan') ?>';
|
|
const devSaleableApi = '<?= base_url('bag/sale/designated/dev-saleable-barcodes') ?>';
|
|
const csrfName = '<?= csrf_token() ?>';
|
|
const csrfHash = '<?= csrf_hash() ?>';
|
|
|
|
const shopSearch = document.getElementById('shop-search');
|
|
const shopSuggest = document.getElementById('shop-search-suggest');
|
|
const barcodeInput = document.getElementById('barcode-input');
|
|
const orderListBody = document.getElementById('order-list-body');
|
|
const saleItemsBody = document.getElementById('sale-items-body');
|
|
const scanDetailBody = document.getElementById('scan-detail-body');
|
|
const saveForm = document.getElementById('sale-save-form');
|
|
const saveSoIdx = document.getElementById('save-so-idx');
|
|
const saveDsIdx = document.getElementById('save-ds-idx');
|
|
const saveScansJson = document.getElementById('save-scans-json');
|
|
const scanMessage = document.getElementById('scan-message');
|
|
const devSaleablePanel = document.getElementById('dev-saleable-panel');
|
|
const devSaleableTbody = document.getElementById('dev-saleable-tbody');
|
|
|
|
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
|
let selectedShop = null;
|
|
let selectedOrder = null;
|
|
let selectedBagCode = '';
|
|
const pendingScans = [];
|
|
|
|
const shopMap = new Map(shops.map((s) => [String(s.ds_idx), s]));
|
|
|
|
function setMessage(msg, isError = false) {
|
|
scanMessage.textContent = msg || '';
|
|
scanMessage.className = isError ? 'text-sm text-red-600' : 'text-sm text-emerald-700';
|
|
}
|
|
|
|
function mergedShopText(shop) {
|
|
return [shop.ds_shop_no, shop.ds_name, shop.ds_rep_name, shop.ds_tel, shop.ds_addr]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
}
|
|
|
|
function hideSuggest() {
|
|
if (shopSuggest) {
|
|
shopSuggest.classList.add('hidden');
|
|
shopSuggest.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
function renderSuggest(query) {
|
|
if (!shopSuggest) return;
|
|
const q = String(query || '').trim().toLowerCase();
|
|
const matched = (q
|
|
? shops.filter((s) => mergedShopText(s).toLowerCase().includes(q))
|
|
: shops
|
|
).slice(0, 30);
|
|
if (matched.length === 0) {
|
|
hideSuggest();
|
|
return;
|
|
}
|
|
shopSuggest.innerHTML = matched.map((s) => `
|
|
<button type="button" class="shop-suggest-item w-full text-left px-2 py-1.5 hover:bg-blue-50 text-xs border-b border-gray-100 whitespace-normal break-all" data-ds-idx="${s.ds_idx}">
|
|
${mergedShopText(s)}
|
|
</button>
|
|
`).join('');
|
|
shopSuggest.classList.remove('hidden');
|
|
}
|
|
|
|
function hideDevSaleablePanel() {
|
|
if (devSaleablePanel) devSaleablePanel.classList.add('hidden');
|
|
if (devSaleableTbody) {
|
|
devSaleableTbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">주문을 선택하면 목록이 표시됩니다.</td></tr>';
|
|
}
|
|
}
|
|
|
|
async function loadDevSaleableBarcodes(dsIdx) {
|
|
if (!devSaleablePanel || !devSaleableTbody) return;
|
|
const idx = Number(dsIdx || 0);
|
|
if (!idx) {
|
|
hideDevSaleablePanel();
|
|
return;
|
|
}
|
|
devSaleablePanel.classList.remove('hidden');
|
|
devSaleableTbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">불러오는 중…</td></tr>';
|
|
try {
|
|
let devUrl = `${devSaleableApi}?ds_idx=${encodeURIComponent(String(idx))}`;
|
|
if (selectedOrder && Number(selectedOrder.so_idx || 0) > 0) {
|
|
devUrl += `&so_idx=${encodeURIComponent(String(selectedOrder.so_idx))}`;
|
|
}
|
|
const res = await fetch(devUrl, { credentials: 'same-origin' });
|
|
const data = await res.json();
|
|
if (!data.ok) {
|
|
devSaleableTbody.innerHTML = `<tr><td colspan="8" class="text-center text-red-600 py-4">${data.message || '조회 실패'}</td></tr>`;
|
|
return;
|
|
}
|
|
const rows = Array.isArray(data.rows) ? data.rows : [];
|
|
if (rows.length === 0) {
|
|
devSaleableTbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">표시할 바코드가 없습니다.</td></tr>';
|
|
return;
|
|
}
|
|
devSaleableTbody.innerHTML = rows.map((r) => `
|
|
<tr>
|
|
<td class="text-left pl-1">${r.source || ''}</td>
|
|
<td class="text-center font-mono">${r.code || ''}</td>
|
|
<td class="text-left pl-1">${r.bag_code || ''} ${r.bag_name || ''}</td>
|
|
<td class="text-center">${r.unit || ''}</td>
|
|
<td class="text-right pr-1">${nf(r.qty || 0)}</td>
|
|
<td class="text-center">${r.so_idx ? String(r.so_idx) : '-'}</td>
|
|
<td class="text-center">${r.state || ''}</td>
|
|
<td class="text-left pl-1">${r.extra || ''}</td>
|
|
</tr>
|
|
`).join('');
|
|
} catch {
|
|
devSaleableTbody.innerHTML = '<tr><td colspan="8" class="text-center text-red-600 py-4">네트워크 오류</td></tr>';
|
|
}
|
|
}
|
|
|
|
function updateShopInfo(shop) {
|
|
document.getElementById('shop-info-code').textContent = shop?.ds_shop_no || '-';
|
|
document.getElementById('shop-info-name').textContent = shop?.ds_name || '-';
|
|
document.getElementById('shop-info-rep').textContent = shop?.ds_rep_name || '-';
|
|
document.getElementById('shop-info-tel').textContent = shop?.ds_tel || '-';
|
|
document.getElementById('shop-info-addr').textContent = shop?.ds_addr || '-';
|
|
}
|
|
|
|
function renderOrderList() {
|
|
const rows = (selectedShop ? orders.filter((o) => Number(o.so_ds_idx) === Number(selectedShop.ds_idx)) : orders);
|
|
if (rows.length === 0) {
|
|
orderListBody.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-gray-400">조건에 맞는 주문이 없습니다.</td></tr>';
|
|
return;
|
|
}
|
|
orderListBody.innerHTML = rows.map((o) => `
|
|
<tr class="order-row cursor-pointer hover:bg-blue-50 ${selectedOrder && Number(selectedOrder.so_idx) === Number(o.so_idx) ? 'bg-blue-100' : ''}" data-order-id="${o.so_idx}">
|
|
<td class="text-center">${o.so_idx}</td>
|
|
<td class="text-left pl-2">${o.so_ds_name || ''}</td>
|
|
<td class="text-center">${o.so_order_date || ''}</td>
|
|
<td class="text-center">${o.so_delivery_date || ''}</td>
|
|
<td class="text-center">${o.so_status === 'cancelled' ? '주문 취소' : (Number(o.so_received || 0) === 1 ? '판매 완료' : '판매 진행')}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function pendingQtyForBag(code) {
|
|
return pendingScans.filter((s) => s.bag_code === code).reduce((sum, s) => sum + (Number(s.qty) || 0), 0);
|
|
}
|
|
|
|
function renderSaleItems() {
|
|
if (!selectedOrder) {
|
|
saleItemsBody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-400 py-6">주문을 선택해 주세요.</td></tr>';
|
|
return;
|
|
}
|
|
const items = Array.isArray(selectedOrder.items) ? selectedOrder.items : [];
|
|
if (items.length === 0) {
|
|
saleItemsBody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-400 py-6">품목 정보가 없습니다.</td></tr>';
|
|
return;
|
|
}
|
|
saleItemsBody.innerHTML = items.map((it) => {
|
|
const bagCode = String(it.soi_bag_code || '');
|
|
const sold = Number(it.sold_qty || 0) + pendingQtyForBag(bagCode);
|
|
const unitPrice = Number(it.soi_unit_price || 0);
|
|
const amount = sold * unitPrice;
|
|
const checked = selectedBagCode === bagCode ? 'checked' : '';
|
|
return `
|
|
<tr>
|
|
<td class="text-center"><input type="radio" name="pick-bag" value="${bagCode}" ${checked}></td>
|
|
<td class="text-left pl-2">${bagCode} ${it.soi_bag_name || ''}</td>
|
|
<td class="text-right pr-2">${nf(it.soi_qty || 0)}</td>
|
|
<td class="text-right pr-2">${nf(sold)}</td>
|
|
<td class="text-right pr-2">${nf(unitPrice)}</td>
|
|
<td class="text-right pr-2">${nf(amount)}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderScanDetails() {
|
|
const rows = pendingScans.filter((s) => !selectedBagCode || s.bag_code === selectedBagCode);
|
|
if (rows.length === 0) {
|
|
scanDetailBody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-400 py-6">바코드를 스캔해 주세요.</td></tr>';
|
|
return;
|
|
}
|
|
scanDetailBody.innerHTML = rows.map((r) => `
|
|
<tr>
|
|
<td class="text-left pl-2">${r.bag_code} ${r.bag_name || ''}</td>
|
|
<td class="text-center">${r.barcode}</td>
|
|
<td class="text-right pr-2">${nf(r.qty)}</td>
|
|
<td class="text-center">${r.unit}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function selectOrder(orderId) {
|
|
selectedOrder = orders.find((o) => Number(o.so_idx) === Number(orderId)) || null;
|
|
selectedBagCode = '';
|
|
pendingScans.length = 0;
|
|
saveSoIdx.value = selectedOrder ? String(selectedOrder.so_idx) : '';
|
|
saveDsIdx.value = selectedOrder ? String(selectedOrder.so_ds_idx || '') : (selectedShop ? String(selectedShop.ds_idx) : '');
|
|
saveScansJson.value = '[]';
|
|
|
|
// 주문이 선택되면 그 주문의 지정판매소를 「지정판매소 정보」 표·검색 input 에도 자동 반영한다.
|
|
if (selectedOrder) {
|
|
const matchedShop = shopMap.get(String(selectedOrder.so_ds_idx)) || null;
|
|
if (matchedShop) {
|
|
selectedShop = matchedShop;
|
|
if (shopSearch) shopSearch.value = mergedShopText(matchedShop);
|
|
updateShopInfo(matchedShop);
|
|
}
|
|
}
|
|
|
|
renderOrderList();
|
|
renderSaleItems();
|
|
renderScanDetails();
|
|
if (selectedOrder && Number(selectedOrder.so_ds_idx || 0) > 0) {
|
|
loadDevSaleableBarcodes(selectedOrder.so_ds_idx);
|
|
} else {
|
|
hideDevSaleablePanel();
|
|
}
|
|
}
|
|
|
|
async function submitScan() {
|
|
const code = (barcodeInput.value || '').trim();
|
|
if (!code) return;
|
|
if (!selectedOrder) {
|
|
setMessage('주문 접수 리스트에서 주문을 먼저 선택해 주세요.', true);
|
|
return;
|
|
}
|
|
|
|
const payload = new URLSearchParams();
|
|
payload.set(csrfName, csrfHash);
|
|
payload.set('so_idx', String(selectedOrder.so_idx));
|
|
payload.set('barcode', code);
|
|
|
|
const pendingByBag = {};
|
|
pendingScans.forEach((s) => {
|
|
const k = String(s.bag_code || '');
|
|
pendingByBag[k] = (pendingByBag[k] || 0) + (Number(s.qty || 0));
|
|
});
|
|
payload.set('pending_by_bag', JSON.stringify(pendingByBag));
|
|
|
|
const res = await fetch(scanApi, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
|
body: payload.toString(),
|
|
});
|
|
const data = await res.json();
|
|
if (!data.ok) {
|
|
setMessage(data.message || '스캔 처리 실패', true);
|
|
return;
|
|
}
|
|
|
|
if (!selectedBagCode) {
|
|
selectedBagCode = String(data.bag_code || '');
|
|
}
|
|
pendingScans.push({
|
|
barcode: code,
|
|
bag_code: data.bag_code,
|
|
bag_name: data.bag_name,
|
|
qty: Number(data.qty || 0),
|
|
unit: data.unit,
|
|
pack_ids: Array.isArray(data.pack_ids) ? data.pack_ids : [],
|
|
});
|
|
saveScansJson.value = JSON.stringify(pendingScans);
|
|
barcodeInput.value = '';
|
|
setMessage(`등록 완료: ${data.bag_code} / ${data.unit} / 수량 ${nf(data.qty)}`);
|
|
renderSaleItems();
|
|
renderScanDetails();
|
|
}
|
|
|
|
shopSearch?.addEventListener('change', (e) => {
|
|
const q = String(e.target.value || '').trim().toLowerCase();
|
|
selectedShop = shops.find((s) => {
|
|
const merged = [s.ds_shop_no, s.ds_name, s.ds_rep_name, s.ds_tel, s.ds_addr].join(' ').toLowerCase();
|
|
return merged.includes(q);
|
|
}) || null;
|
|
selectedOrder = null;
|
|
selectedBagCode = '';
|
|
pendingScans.length = 0;
|
|
saveSoIdx.value = '';
|
|
saveDsIdx.value = selectedShop ? String(selectedShop.ds_idx) : '';
|
|
saveScansJson.value = '[]';
|
|
updateShopInfo(selectedShop);
|
|
hideDevSaleablePanel();
|
|
renderOrderList();
|
|
renderSaleItems();
|
|
renderScanDetails();
|
|
setMessage('');
|
|
hideSuggest();
|
|
});
|
|
|
|
shopSearch?.addEventListener('input', (e) => {
|
|
renderSuggest(e.target.value || '');
|
|
});
|
|
|
|
shopSearch?.addEventListener('focus', (e) => {
|
|
renderSuggest(e.target.value || '');
|
|
});
|
|
|
|
shopSearch?.addEventListener('click', (e) => {
|
|
renderSuggest(e.target.value || '');
|
|
});
|
|
|
|
shopSuggest?.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('.shop-suggest-item');
|
|
if (!btn) return;
|
|
const dsIdx = Number(btn.dataset.dsIdx || 0);
|
|
const shop = shops.find((s) => Number(s.ds_idx) === dsIdx) || null;
|
|
if (!shop) return;
|
|
shopSearch.value = mergedShopText(shop);
|
|
selectedShop = shop;
|
|
selectedOrder = null;
|
|
selectedBagCode = '';
|
|
pendingScans.length = 0;
|
|
saveSoIdx.value = '';
|
|
saveDsIdx.value = selectedShop ? String(selectedShop.ds_idx) : '';
|
|
saveScansJson.value = '[]';
|
|
updateShopInfo(selectedShop);
|
|
hideDevSaleablePanel();
|
|
renderOrderList();
|
|
renderSaleItems();
|
|
renderScanDetails();
|
|
setMessage('');
|
|
hideSuggest();
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (!shopSuggest || !shopSearch) return;
|
|
if (e.target === shopSearch || shopSuggest.contains(e.target)) return;
|
|
hideSuggest();
|
|
});
|
|
|
|
barcodeInput?.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
submitScan();
|
|
}
|
|
});
|
|
|
|
orderListBody?.addEventListener('click', (e) => {
|
|
const tr = e.target.closest('.order-row');
|
|
if (!tr) return;
|
|
selectOrder(tr.dataset.orderId);
|
|
});
|
|
|
|
saleItemsBody?.addEventListener('change', (e) => {
|
|
const radio = e.target.closest('input[name="pick-bag"]');
|
|
if (!radio) return;
|
|
selectedBagCode = radio.value || '';
|
|
renderScanDetails();
|
|
});
|
|
|
|
saveForm?.addEventListener('submit', (e) => {
|
|
if (!selectedOrder) {
|
|
e.preventDefault();
|
|
alert('주문을 먼저 선택해 주세요.');
|
|
return;
|
|
}
|
|
if (pendingScans.length === 0) {
|
|
e.preventDefault();
|
|
alert('없는 바코드이거나 유효한 스캔 내역이 없습니다.');
|
|
return;
|
|
}
|
|
});
|
|
|
|
renderOrderList();
|
|
renderSaleItems();
|
|
renderScanDetails();
|
|
})();
|
|
</script>
|
|
|
|
<?= view('bag/_dev_all_sales_panel') ?>
|