사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.

통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
taekyoungc
2026-06-01 16:15:15 +09:00
parent 21e7b91871
commit 0f1d414f37
129 changed files with 18068 additions and 1585 deletions

View File

@@ -0,0 +1,261 @@
<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>
<a href="<?= base_url('bag/order/phone') ?>" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm shadow hover:opacity-90">전화 주문 접수</a>
</div>
</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 bg-white">
<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-14">번호</th>
<th>판매소</th>
<th class="w-28">접수일</th>
<th class="w-24">상태</th>
</tr>
</thead>
<tbody id="order-list-body">
<?php foreach (($orders ?? []) as $row): ?>
<?php $isCancelled = (($row['so_status'] ?? 'normal') === 'cancelled'); ?>
<tr class="order-list-row cursor-pointer hover:bg-blue-50 <?= $isCancelled ? 'bg-gray-50 text-gray-400' : '' ?>" data-order-id="<?= esc((string) ($row['so_idx'] ?? 0), 'attr') ?>">
<td class="text-center"><?= esc((string) ($row['so_idx'] ?? 0)) ?></td>
<td class="text-left pl-2"><?= esc((string) ($row['so_ds_name'] ?? '')) ?></td>
<td class="text-center"><?= esc((string) ($row['so_order_date'] ?? '')) ?></td>
<td class="text-center"><?= $isCancelled ? '취소' : '정상' ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<tr>
<td colspan="4" class="text-center py-8 text-gray-400">전화 주문 데이터가 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<section class="xl:col-span-3 border border-gray-300 bg-white">
<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="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
<div class="border border-gray-200 p-2 bg-gray-50">
<div><span class="font-semibold text-gray-700">접수번호:</span> <span id="detail-so-no">-</span></div>
<div><span class="font-semibold text-gray-700">판매소:</span> <span id="detail-shop-name">-</span></div>
<div><span class="font-semibold text-gray-700">결제구분:</span> <span id="detail-payment">-</span></div>
</div>
<div class="border border-gray-200 p-2 bg-gray-50">
<div><span class="font-semibold text-gray-700">접수일:</span> <span id="detail-order-date">-</span></div>
<div><span class="font-semibold text-gray-700">배달일:</span> <span id="detail-delivery-date">-</span></div>
<div><span class="font-semibold text-gray-700">상태:</span> <span id="detail-status">-</span></div>
</div>
</div>
<div class="border border-gray-300 overflow-auto">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-14">번호</th>
<th>품목</th>
<th class="w-24">단가</th>
<th class="w-24">접수량</th>
<th class="w-28">접수금액</th>
<th class="w-40">포장단위(박스/팩/낱장)</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) {
document.getElementById('detail-so-no').textContent = order ? String(order.so_idx || '-') : '-';
document.getElementById('detail-shop-name').textContent = order ? (order.so_ds_name || '-') : '-';
document.getElementById('detail-payment').textContent = order ? (order.so_payment_type || '-') : '-';
document.getElementById('detail-order-date').textContent = order ? (order.so_order_date || '-') : '-';
document.getElementById('detail-delivery-date').textContent = order ? (order.so_delivery_date || '-') : '-';
document.getElementById('detail-status').textContent = 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();
}
listBody?.addEventListener('click', (e) => {
const tr = e.target.closest('.order-list-row');
if (!tr) return;
listBody.querySelectorAll('.order-list-row').forEach((row) => row.classList.remove('bg-blue-100'));
tr.classList.add('bg-blue-100');
renderDetail(tr.dataset.orderId);
});
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('수정할 주문을 먼저 선택해 주세요.');
}
});
const firstRow = listBody?.querySelector('.order-list-row');
if (firstRow) {
firstRow.classList.add('bg-blue-100');
renderDetail(firstRow.dataset.orderId);
}
})();
</script>
<?= view('bag/_dev_all_sales_panel') ?>