- 접수 리스트에 접수일/배달일/금액 컬럼 추가, 헤더 클릭 정렬(번호·접수일·배달일·판매소·금액·상태, 오름/내림 토글) - 우측 상세에서 배달일을 date 입력으로 수정 가능, 저장 시 so_delivery_date 변경 반영 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
319 lines
15 KiB
PHP
319 lines
15 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 class="grid grid-cols-1 xl:grid-cols-5 gap-3 mt-2">
|
|
<section class="xl:col-span-2 border border-gray-300 rounded-lg bg-white overflow-hidden">
|
|
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700">접수 리스트(전화)</div>
|
|
<div class="max-h-[72vh] 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="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-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-16 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>
|
|
</section>
|
|
|
|
<section class="xl:col-span-3 border border-gray-300 rounded-lg bg-white overflow-hidden">
|
|
<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>
|
|
</div>
|
|
|
|
<div class="px-1 text-sm font-semibold text-gray-700">접수 품목 내역</div>
|
|
<div class="border border-gray-300 rounded-lg p-4 overflow-auto">
|
|
<table class="w-full data-table text-sm">
|
|
<thead>
|
|
<tr>
|
|
<th class="w-14 text-center">번호</th>
|
|
<th class="text-left">품목</th>
|
|
<th class="w-24 text-right">단가</th>
|
|
<th class="w-24 text-right">접수량</th>
|
|
<th class="w-28 text-right">접수금액</th>
|
|
<th class="w-40 text-right">포장단위(박스/팩/낱장)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="detail-items-body">
|
|
<tr>
|
|
<td colspan="6" 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-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="flex gap-2">
|
|
<button type="submit" id="btn-save" class="bg-btn-search text-white px-5 py-1.5 rounded-sm text-sm shadow hover:opacity-90" disabled>주문 수정 저장</button>
|
|
</div>
|
|
</form>
|
|
|
|
<form id="order-cancel-form" method="POST" class="px-3 pb-3">
|
|
<?= csrf_field() ?>
|
|
<button type="submit" id="btn-cancel-order" class="border border-red-300 text-red-600 px-5 py-1.5 rounded-sm text-sm hover:bg-red-50" disabled>주문 취소</button>
|
|
</form>
|
|
</section>
|
|
</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]));
|
|
|
|
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 nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
|
|
|
function setHeader(order) {
|
|
var t = function (id, v) { document.getElementById(id).textContent = (v === undefined || v === null || v === '') ? '-' : v; };
|
|
t('detail-so-no', order ? order.so_idx : '');
|
|
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') ? '취소' : '정상') : '');
|
|
// 배달일 — 수정 가능한 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';
|
|
}
|
|
}
|
|
|
|
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 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, amount, box, pack, sheet };
|
|
}
|
|
|
|
function recalcTotals() {
|
|
let sumQty = 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;
|
|
sumAmount += r.amount;
|
|
sumBox += r.box;
|
|
sumPack += r.pack;
|
|
sumSheet += r.sheet;
|
|
});
|
|
|
|
document.getElementById('detail-sum-qty').textContent = nf(sumQty);
|
|
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;
|
|
|
|
const items = Array.isArray(order.items) ? order.items : [];
|
|
if (items.length === 0) {
|
|
detailBody.innerHTML = '<tr><td colspan="6" 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 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-amount-cell">${nf(amount)}</td>
|
|
<td class="text-right pr-2 item-pack-cell">박스=${nf(box)}, 팩=${nf(pack)}, 낱장=${nf(sheet)}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
recalcTotals();
|
|
}
|
|
|
|
// ── 접수 리스트 렌더 + 헤더 클릭 정렬 ──────────────────────────
|
|
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) : '';
|
|
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">${o.so_idx}</td>
|
|
<td class="text-center">${esc(o.so_order_date)}</td>
|
|
<td class="text-center">${esc(dDate)}</td>
|
|
<td class="text-left pl-2">${esc(o.so_ds_name)}</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="6" 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' ? ' ▲' : ' ▼';
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
detailBody?.addEventListener('input', (e) => {
|
|
if (e.target.closest('.item-qty-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);
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<?= view('bag/_dev_all_sales_panel') ?>
|