사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.
통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,74 +1,301 @@
|
||||
<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-4 mt-2 bg-white max-w-4xl">
|
||||
<div class="border border-gray-300 p-4 mt-2 bg-white">
|
||||
<form action="<?= mgmt_url('shop-orders/store') ?>" method="POST" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">판매소 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="so_ds_idx" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($shops as $shop): ?>
|
||||
<option value="<?= esc($shop->ds_idx) ?>" <?= (int) old('so_ds_idx') === (int) $shop->ds_idx ? 'selected' : '' ?>>
|
||||
<?= esc($shop->ds_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">판매소 검색 <span class="text-red-500">*</span></label>
|
||||
<input id="shop-search" class="border border-gray-300 rounded px-3 py-1.5 text-sm w-72" type="text" list="shop-search-list" placeholder="코드/사업자번호/대표자명/상호/전화/주소"/>
|
||||
<datalist id="shop-search-list">
|
||||
<?php foreach ($shops as $shop): ?>
|
||||
<option value="<?= esc(trim(($shop->ds_shop_no ?? '') . ' ' . ($shop->ds_name ?? '') . ' ' . ($shop->ds_rep_name ?? '') . ' ' . ($shop->ds_biz_no ?? '') . ' ' . ($shop->ds_tel ?? '') . ' ' . ($shop->ds_addr ?? ''))) ?>"></option>
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">판매소 선택 <span class="text-red-500">*</span></label>
|
||||
<select id="shop-select" class="border border-gray-300 rounded px-3 py-1.5 text-sm w-72" name="so_ds_idx" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($shops as $shop): ?>
|
||||
<option
|
||||
value="<?= esc($shop->ds_idx) ?>"
|
||||
data-shop-no="<?= esc((string) ($shop->ds_shop_no ?? '')) ?>"
|
||||
data-name="<?= esc((string) ($shop->ds_name ?? '')) ?>"
|
||||
data-rep-name="<?= esc((string) ($shop->ds_rep_name ?? '')) ?>"
|
||||
data-rep-phone="<?= esc((string) ($shop->ds_rep_phone ?? '')) ?>"
|
||||
data-tel="<?= esc((string) ($shop->ds_tel ?? '')) ?>"
|
||||
data-address="<?= esc(trim((string) ($shop->ds_addr ?? '') . ' ' . (string) ($shop->ds_addr_detail ?? ''))) ?>"
|
||||
data-va-bank="<?= esc((string) ($shop->ds_va_bank ?? '')) ?>"
|
||||
data-va-account="<?= esc((string) ($shop->ds_va_account ?? '')) ?>"
|
||||
<?= (int) old('so_ds_idx') === (int) $shop->ds_idx ? 'selected' : '' ?>
|
||||
>
|
||||
<?= esc(($shop->ds_shop_no ? '[' . $shop->ds_shop_no . '] ' : '') . $shop->ds_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-gray-300 p-2 bg-gray-50">
|
||||
<div class="text-sm font-bold text-gray-700 mb-2">지정판매소 정보</div>
|
||||
<table class="w-full text-sm">
|
||||
<tr><th class="text-left w-28 py-1">판매소 코드</th><td id="shop-info-code" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">상호</th><td id="shop-info-name" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">대표자명</th><td id="shop-info-rep" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">연락처</th><td id="shop-info-tel" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">주소</th><td id="shop-info-addr" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">가상계좌</th><td id="shop-info-va" class="py-1 text-gray-700">-</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">접수일</label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44 bg-gray-100" type="date" value="<?= esc(date('Y-m-d')) ?>" readonly/>
|
||||
<span class="text-xs text-gray-500">배달일 기본값은 접수일 다음날입니다.</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">배달일 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="so_delivery_date" type="date" value="<?= esc(old('so_delivery_date', date('Y-m-d', strtotime('+1 day')))) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">결제방법 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="so_payment_type" required>
|
||||
<select id="payment-type" class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="so_payment_type" required>
|
||||
<option value="">선택</option>
|
||||
<option value="이체" <?= old('so_payment_type') === '이체' ? 'selected' : '' ?>>이체</option>
|
||||
<option value="가상계좌" <?= old('so_payment_type') === '가상계좌' ? 'selected' : '' ?>>가상계좌</option>
|
||||
</select>
|
||||
<span id="payment-guide" class="text-xs text-gray-500"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">주문 품목</label>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-sm font-bold text-gray-700">전화 주문 접수표</label>
|
||||
<button type="button" id="add-order-row" class="border border-gray-300 bg-white px-3 py-1 rounded-sm text-xs text-gray-700 hover:bg-gray-50">행 추가</button>
|
||||
</div>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-16">순번</th>
|
||||
<th>봉투</th>
|
||||
<th class="w-32">수량</th>
|
||||
<th class="w-14">순번</th>
|
||||
<th class="w-48">품목</th>
|
||||
<th class="w-36">1박스(낱장/판매가)</th>
|
||||
<th class="w-36">1팩(낱장/판매가)</th>
|
||||
<th class="w-24">단가</th>
|
||||
<th class="w-28">주문수량</th>
|
||||
<th class="w-28">금액</th>
|
||||
<th class="w-32">포장(박스/팩/낱장)</th>
|
||||
<th class="w-20">행삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="order-rows">
|
||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<tr class="order-row">
|
||||
<td class="text-center row-no"><?= $i + 1 ?></td>
|
||||
<td>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full" name="item_bag_code[]">
|
||||
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full bag-code-select" name="item_bag_code[]">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $cd): ?>
|
||||
<option value="<?= esc($cd->cd_code) ?>">
|
||||
<?php $code = (string) $cd->cd_code; $price = $priceMap[$code] ?? null; $unit = $unitMap[$code] ?? null; ?>
|
||||
<option value="<?= esc($code) ?>" data-unit-price="<?= esc((string) (int) ($price->bp_consumer ?? 0)) ?>" data-box-sheets="<?= esc((string) (int) ($unit->pu_total_per_box ?? 0)) ?>" data-box-packs="<?= esc((string) (int) ($unit->pu_box_per_pack ?? 0)) ?>" data-pack-sheets="<?= esc((string) (int) ($unit->pu_pack_per_sheet ?? 0)) ?>">
|
||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right" name="item_qty[]" type="number" min="0" value="0"/>
|
||||
<td class="text-right px-2 box-info-cell">0 / 0</td>
|
||||
<td class="text-right px-2 pack-info-cell">0 / 0</td>
|
||||
<td class="text-right px-2 unit-price-cell">0</td>
|
||||
<td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-qty-input" name="item_qty[]" type="number" min="0" value="0"/></td>
|
||||
<td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-amount-input" type="number" min="0" step="1" value="0"/></td>
|
||||
<td class="text-right px-2 pack-result-cell">박스=0, 팩=0, 낱장=0</td>
|
||||
<td class="text-center px-2">
|
||||
<button type="button" class="remove-order-row border border-red-300 text-red-600 px-2 py-0.5 rounded text-xs hover:bg-red-50">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-semibold bg-gray-50">
|
||||
<td colspan="5" class="text-right px-2 py-1">합계</td>
|
||||
<td class="text-right px-2 py-1" id="sum-qty">0</td>
|
||||
<td class="text-right px-2 py-1" id="sum-amount">0</td>
|
||||
<td class="text-right px-2 py-1" id="sum-pack">박스=0, 팩=0, 낱장=0</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">저장</button>
|
||||
<a href="<?= mgmt_url('shop-orders') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<template id="order-row-template">
|
||||
<tr class="order-row">
|
||||
<td class="text-center row-no">1</td>
|
||||
<td>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full bag-code-select" name="item_bag_code[]">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $cd): ?>
|
||||
<?php $code = (string) $cd->cd_code; $price = $priceMap[$code] ?? null; $unit = $unitMap[$code] ?? null; ?>
|
||||
<option value="<?= esc($code) ?>" data-unit-price="<?= esc((string) (int) ($price->bp_consumer ?? 0)) ?>" data-box-sheets="<?= esc((string) (int) ($unit->pu_total_per_box ?? 0)) ?>" data-box-packs="<?= esc((string) (int) ($unit->pu_box_per_pack ?? 0)) ?>" data-pack-sheets="<?= esc((string) (int) ($unit->pu_pack_per_sheet ?? 0)) ?>">
|
||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-right px-2 box-info-cell">0 / 0</td>
|
||||
<td class="text-right px-2 pack-info-cell">0 / 0</td>
|
||||
<td class="text-right px-2 unit-price-cell">0</td>
|
||||
<td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-qty-input" name="item_qty[]" type="number" min="0" value="0"/></td>
|
||||
<td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-amount-input" type="number" min="0" step="1" value="0"/></td>
|
||||
<td class="text-right px-2 pack-result-cell">박스=0, 팩=0, 낱장=0</td>
|
||||
<td class="text-center px-2">
|
||||
<button type="button" class="remove-order-row border border-red-300 text-red-600 px-2 py-0.5 rounded text-xs hover:bg-red-50">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<script>
|
||||
(function () {
|
||||
const shopSearch = document.getElementById('shop-search');
|
||||
const shopSelect = document.getElementById('shop-select');
|
||||
const paymentType = document.getElementById('payment-type');
|
||||
const paymentGuide = document.getElementById('payment-guide');
|
||||
const addRowButton = document.getElementById('add-order-row');
|
||||
const orderRows = document.getElementById('order-rows');
|
||||
const rowTemplate = document.getElementById('order-row-template');
|
||||
const form = shopSelect.closest('form');
|
||||
function nf(n) { return new Intl.NumberFormat('ko-KR').format(n || 0); }
|
||||
function updateShopInfo() {
|
||||
const opt = shopSelect.options[shopSelect.selectedIndex];
|
||||
const bank = opt?.dataset?.vaBank || '';
|
||||
const account = opt?.dataset?.vaAccount || '';
|
||||
const va = bank || account ? [bank, account].filter(Boolean).join(' ') : '-';
|
||||
document.getElementById('shop-info-code').textContent = opt?.dataset?.shopNo || '-';
|
||||
document.getElementById('shop-info-name').textContent = opt?.dataset?.name || '-';
|
||||
document.getElementById('shop-info-rep').textContent = opt?.dataset?.repName || '-';
|
||||
document.getElementById('shop-info-tel').textContent = opt?.dataset?.tel || opt?.dataset?.repPhone || '-';
|
||||
document.getElementById('shop-info-addr').textContent = opt?.dataset?.address || '-';
|
||||
document.getElementById('shop-info-va').textContent = va;
|
||||
paymentGuide.textContent = paymentType.value === '가상계좌' ? ('가상계좌 안내: ' + va) : '';
|
||||
}
|
||||
function matchShopByKeyword(keyword) {
|
||||
const q = (keyword || '').trim().toLowerCase();
|
||||
if (!q) { return; }
|
||||
for (let i = 0; i < shopSelect.options.length; i++) {
|
||||
const opt = shopSelect.options[i];
|
||||
const merged = [opt.dataset.shopNo || '', opt.dataset.name || '', opt.dataset.repName || '', opt.dataset.tel || '', opt.dataset.address || '', opt.text || ''].join(' ').toLowerCase();
|
||||
if (merged.includes(q)) { shopSelect.selectedIndex = i; updateShopInfo(); return; }
|
||||
}
|
||||
}
|
||||
function calcRow(row, source) {
|
||||
const select = row.querySelector('.bag-code-select');
|
||||
const qtyInput = row.querySelector('.item-qty-input');
|
||||
const amountInput = row.querySelector('.item-amount-input');
|
||||
const selected = select.options[select.selectedIndex];
|
||||
let qty = parseInt(qtyInput.value || '0', 10) || 0;
|
||||
const unitPrice = parseInt(selected?.dataset?.unitPrice || '0', 10) || 0;
|
||||
const boxSheets = parseInt(selected?.dataset?.boxSheets || '0', 10) || 0;
|
||||
const boxPacks = parseInt(selected?.dataset?.boxPacks || '0', 10) || 0;
|
||||
const packSheets = parseInt(selected?.dataset?.packSheets || '0', 10) || 0;
|
||||
const rawAmount = parseInt(amountInput?.value || '0', 10) || 0;
|
||||
if (source === 'amount' && unitPrice > 0) {
|
||||
qty = Math.max(0, Math.round(rawAmount / unitPrice));
|
||||
qtyInput.value = String(qty);
|
||||
}
|
||||
let box = 0, pack = 0, 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 = unitPrice * qty;
|
||||
const boxPrice = boxSheets * unitPrice;
|
||||
const packPrice = packSheets * unitPrice;
|
||||
row.querySelector('.box-info-cell').textContent = nf(boxSheets) + ' / ' + nf(boxPrice);
|
||||
row.querySelector('.pack-info-cell').textContent = nf(packSheets) + ' / ' + nf(packPrice);
|
||||
row.querySelector('.unit-price-cell').textContent = nf(unitPrice);
|
||||
if (amountInput && source !== 'amount') {
|
||||
amountInput.value = String(amount);
|
||||
}
|
||||
const innerPackCount = box * boxPacks;
|
||||
const innerSheetCount = box * boxSheets;
|
||||
row.querySelector('.pack-result-cell').textContent = '박스=' + nf(box) + '(내부 팩=' + nf(innerPackCount) + ', 내부 낱장=' + nf(innerSheetCount) + '), 잔여 팩=' + nf(pack) + ', 잔여 낱장=' + nf(sheet);
|
||||
return { qty, amount, box, pack, sheet };
|
||||
}
|
||||
function recalcAllRows(sourceRow, sourceType) {
|
||||
let sumQty = 0, sumAmount = 0, sumBox = 0, sumPack = 0, sumSheet = 0;
|
||||
document.querySelectorAll('.order-row').forEach((row, index) => {
|
||||
const noCell = row.querySelector('.row-no');
|
||||
if (noCell) {
|
||||
noCell.textContent = String(index + 1);
|
||||
}
|
||||
const source = row === sourceRow ? sourceType : 'qty';
|
||||
const r = calcRow(row, source);
|
||||
sumQty += r.qty; sumAmount += r.amount; sumBox += r.box; sumPack += r.pack; sumSheet += r.sheet;
|
||||
});
|
||||
document.getElementById('sum-qty').textContent = nf(sumQty);
|
||||
document.getElementById('sum-amount').textContent = nf(sumAmount);
|
||||
document.getElementById('sum-pack').textContent = '박스=' + nf(sumBox) + ', 팩=' + nf(sumPack) + ', 낱장=' + nf(sumSheet);
|
||||
}
|
||||
shopSearch?.addEventListener('change', (e) => matchShopByKeyword(e.target.value));
|
||||
shopSearch?.addEventListener('blur', (e) => matchShopByKeyword(e.target.value));
|
||||
shopSelect?.addEventListener('change', updateShopInfo);
|
||||
paymentType?.addEventListener('change', updateShopInfo);
|
||||
orderRows?.addEventListener('change', function (e) {
|
||||
const row = e.target.closest('.order-row');
|
||||
if (e.target.closest('.bag-code-select') || e.target.closest('.item-qty-input')) {
|
||||
recalcAllRows(row, 'qty');
|
||||
} else if (e.target.closest('.item-amount-input')) {
|
||||
recalcAllRows(row, 'amount');
|
||||
}
|
||||
});
|
||||
orderRows?.addEventListener('input', function (e) {
|
||||
const row = e.target.closest('.order-row');
|
||||
if (e.target.closest('.item-qty-input')) {
|
||||
recalcAllRows(row, 'qty');
|
||||
}
|
||||
});
|
||||
orderRows?.addEventListener('click', function (e) {
|
||||
const removeButton = e.target.closest('.remove-order-row');
|
||||
if (!removeButton) {
|
||||
return;
|
||||
}
|
||||
const row = removeButton.closest('.order-row');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
if (orderRows.querySelectorAll('.order-row').length <= 1) {
|
||||
alert('최소 1개 행은 유지해야 합니다.');
|
||||
return;
|
||||
}
|
||||
row.remove();
|
||||
recalcAllRows(null, 'qty');
|
||||
});
|
||||
addRowButton?.addEventListener('click', function () {
|
||||
if (!rowTemplate || !orderRows) {
|
||||
return;
|
||||
}
|
||||
const fragment = rowTemplate.content.cloneNode(true);
|
||||
orderRows.appendChild(fragment);
|
||||
recalcAllRows(null, 'qty');
|
||||
});
|
||||
form?.addEventListener('submit', function (e) {
|
||||
let hasItem = false;
|
||||
document.querySelectorAll('.order-row').forEach((row) => {
|
||||
const code = row.querySelector('.bag-code-select').value;
|
||||
const qty = parseInt(row.querySelector('.item-qty-input').value || '0', 10) || 0;
|
||||
if (code && qty > 0) { hasItem = true; }
|
||||
});
|
||||
if (!hasItem) { e.preventDefault(); alert('주문 품목과 수량을 1개 이상 입력해 주세요.'); }
|
||||
});
|
||||
updateShopInfo();
|
||||
recalcAllRows(null, 'qty');
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user