feat: 전화접수 관리·주문 개선 + 매뉴얼 화면 바로가기
- 전화접수 관리: 배달일 기준 조회, 컬럼 보강(구분/포장 O·X/결제/수령/입금/총금액/취소), 헤더 정렬, 배달일 수정 저장, 포장 명세·포장량 수정, 입금자 영수증 출력 - 전화 주문 접수: 행추가 → 일괄표(카테고리 그룹·포장 헤더) - 매뉴얼 소제목에 '화면 열기' 바로가기(실제 GET 라우트만 연결) - shop_order_item.soi_packed_qty 컬럼 추가(SQL) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -96,69 +96,118 @@
|
||||
|
||||
<input type="hidden" name="so_delivery_date" value="<?= esc(date('Y-m-d', strtotime('+1 day'))) ?>"/>
|
||||
|
||||
<?php
|
||||
// 일괄표: 봉투코드를 카테고리(일반용·재사용·음식물 스티커·대형폐기물 스티커)로 묶어
|
||||
// 봉투류는 리터 오름차순, 폐기물 스티커는 금액 오름차순으로 정렬한다.
|
||||
$catOf = static function (string $name): ?array {
|
||||
if (mb_strpos($name, '일반용') === 0) { return [1, '일반용', 'liter']; }
|
||||
if (mb_strpos($name, '재사용') === 0) { return [2, '재사용', 'liter']; }
|
||||
if (mb_strpos($name, '음식물 스티커') === 0) { return [3, '음식물 스티커', 'liter']; }
|
||||
if (mb_strpos($name, '폐기물 스티커') === 0) { return [4, '대형폐기물 스티커', 'amount']; }
|
||||
return null;
|
||||
};
|
||||
$orderGroups = [];
|
||||
foreach (($bagCodes ?? []) as $cd) {
|
||||
$name = (string) ($cd->cd_name ?? '');
|
||||
$cat = $catOf($name);
|
||||
if ($cat === null) { continue; }
|
||||
$code = (string) $cd->cd_code;
|
||||
$price = $priceMap[$code] ?? null;
|
||||
$unit = $unitMap[$code] ?? null;
|
||||
if ($cat[2] === 'amount') {
|
||||
preg_match('/([\d,]+)\s*원/u', $name, $m);
|
||||
$sortVal = (int) str_replace(',', '', $m[1] ?? '0');
|
||||
} else {
|
||||
preg_match('/(\d+)\s*[lL]/u', $name, $m);
|
||||
$sortVal = (int) ($m[1] ?? 0);
|
||||
}
|
||||
$orderGroups[$cat[0]]['label'] = $cat[1];
|
||||
$orderGroups[$cat[0]]['rows'][] = [
|
||||
'code' => $code,
|
||||
'name' => $name,
|
||||
'price' => (int) ($price->bp_consumer ?? 0),
|
||||
'box' => (int) ($unit->pu_total_per_box ?? 0),
|
||||
'pack' => (int) ($unit->pu_pack_per_sheet ?? 0),
|
||||
'sort' => $sortVal,
|
||||
];
|
||||
}
|
||||
ksort($orderGroups);
|
||||
foreach ($orderGroups as &$g) {
|
||||
usort($g['rows'], static fn ($a, $b) => $a['sort'] <=> $b['sort']);
|
||||
}
|
||||
unset($g);
|
||||
?>
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">전화 주문접수표 (일괄)</label>
|
||||
<style>
|
||||
/* 그룹 헤더(rowspan/colspan) 경계선이 단가까지 이어지는 현상 방지 — 헤더 맨 아래 한 줄만 */
|
||||
table.phone-batch-table { border-collapse: separate; border-spacing: 0; }
|
||||
table.phone-batch-table thead th { border-bottom: 0; }
|
||||
table.phone-batch-table thead tr:last-child th { border-bottom: 1px solid #e5e7eb; } /* 수량/금액 등 하위 헤더 */
|
||||
table.phone-batch-table thead tr:first-child th[rowspan] { border-bottom: 1px solid #e5e7eb; } /* 구분·봉투종류·단가 */
|
||||
table.phone-batch-table tbody td { border-bottom: 1px solid #f1f3f5; }
|
||||
</style>
|
||||
<div class="border border-gray-300 rounded-lg p-4 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<table class="w-full data-table text-sm phone-batch-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14 text-center">구분</th>
|
||||
<th class="w-56">품목</th>
|
||||
<th class="w-40 text-right">1박스(낱장/판매가)</th>
|
||||
<th class="w-40 text-right">1팩(낱장/판매가)</th>
|
||||
<th class="w-24 text-right">단가</th>
|
||||
<th class="w-28 text-right">주문수량</th>
|
||||
<th class="w-28 text-right">금액</th>
|
||||
<th class="w-44 text-right">포장(박스/팩/낱장)</th>
|
||||
<th class="w-20 text-center">삭제</th>
|
||||
<th class="text-center" rowspan="2">구분</th>
|
||||
<th rowspan="2">봉투종류</th>
|
||||
<th class="text-center" colspan="2">Pack</th>
|
||||
<th class="text-center" colspan="2">Box</th>
|
||||
<th class="text-right" rowspan="2">단가</th>
|
||||
<th class="text-center" colspan="2">주문내용</th>
|
||||
<th class="text-center" colspan="3">포장</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-right">수량</th>
|
||||
<th class="text-right">금액</th>
|
||||
<th class="text-right">수량</th>
|
||||
<th class="text-right">금액</th>
|
||||
<th class="text-right">판매수량</th>
|
||||
<th class="text-right">금액</th>
|
||||
<th class="text-center">B</th>
|
||||
<th class="text-center">P</th>
|
||||
<th class="text-center">E</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="order-rows">
|
||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||
<tr class="order-row">
|
||||
<td class="text-center item-kind-cell">-</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;
|
||||
$name = (string) ($cd->cd_name ?? '');
|
||||
$price = $priceMap[$code] ?? null;
|
||||
$unit = $unitMap[$code] ?? null;
|
||||
$unitPrice = (int) ($price->bp_consumer ?? 0);
|
||||
$boxSheets = (int) ($unit->pu_total_per_box ?? 0);
|
||||
$packSheets = (int) ($unit->pu_pack_per_sheet ?? 0);
|
||||
$kindLabel = (mb_strpos($name, '스티커') !== false) ? '스티커' : '봉투';
|
||||
?>
|
||||
<option value="<?= esc($code) ?>" data-name="<?= esc($name, 'attr') ?>" data-kind-label="<?= esc($kindLabel, 'attr') ?>" data-unit-price="<?= esc((string) $unitPrice, 'attr') ?>" data-box-sheets="<?= esc((string) $boxSheets, 'attr') ?>" data-pack-sheets="<?= esc((string) $packSheets, 'attr') ?>">
|
||||
<?= esc($code) ?> — <?= esc($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 class="text-right px-2 item-amount-cell">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; ?>
|
||||
<?php if (empty($orderGroups)): ?>
|
||||
<tr><td colspan="12" class="text-center text-gray-400 py-6">등록된 봉투 품목이 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($orderGroups as $g): ?>
|
||||
<?php $rc = count($g['rows']); $firstRow = true; ?>
|
||||
<?php foreach ($g['rows'] as $r): ?>
|
||||
<?php $boxPrice = $r['box'] * $r['price']; $packPrice = $r['pack'] * $r['price']; ?>
|
||||
<tr class="order-row" data-unit-price="<?= esc((string) $r['price'], 'attr') ?>" data-box-sheets="<?= esc((string) $r['box'], 'attr') ?>" data-pack-sheets="<?= esc((string) $r['pack'], 'attr') ?>">
|
||||
<?php if ($firstRow): ?>
|
||||
<td class="text-center align-top font-semibold text-gray-700" rowspan="<?= (int) $rc ?>"><?= esc($g['label']) ?></td>
|
||||
<?php endif; ?>
|
||||
<td class="text-left pl-2"><?= esc($r['name']) ?><input type="hidden" name="item_bag_code[]" value="<?= esc($r['code'], 'attr') ?>"/></td>
|
||||
<td class="text-right px-2"><?= number_format($r['pack']) ?></td>
|
||||
<td class="text-right px-2"><?= number_format($packPrice) ?></td>
|
||||
<td class="text-right px-2"><?= number_format($r['box']) ?></td>
|
||||
<td class="text-right px-2"><?= number_format($boxPrice) ?></td>
|
||||
<td class="text-right px-2"><?= number_format($r['price']) ?></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 class="text-right px-2 item-amount-cell">0</td>
|
||||
<td class="text-center item-box-cell">0</td>
|
||||
<td class="text-center item-pack-cell">0</td>
|
||||
<td class="text-center item-sheet-cell">0</td>
|
||||
</tr>
|
||||
<?php $firstRow = false; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-semibold bg-gray-50">
|
||||
<td colspan="5" class="text-right px-2 py-1">합계</td>
|
||||
<td colspan="7" 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>
|
||||
<td class="text-center px-2 py-1" id="sum-box">0</td>
|
||||
<td class="text-center px-2 py-1" id="sum-pack">0</td>
|
||||
<td class="text-center px-2 py-1" id="sum-sheet">0</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@@ -172,40 +221,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<template id="order-row-template">
|
||||
<tr class="order-row">
|
||||
<td class="text-center item-kind-cell">-</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;
|
||||
$name = (string) ($cd->cd_name ?? '');
|
||||
$price = $priceMap[$code] ?? null;
|
||||
$unit = $unitMap[$code] ?? null;
|
||||
$unitPrice = (int) ($price->bp_consumer ?? 0);
|
||||
$boxSheets = (int) ($unit->pu_total_per_box ?? 0);
|
||||
$packSheets = (int) ($unit->pu_pack_per_sheet ?? 0);
|
||||
$kindLabel = (mb_strpos($name, '스티커') !== false) ? '스티커' : '봉투';
|
||||
?>
|
||||
<option value="<?= esc($code) ?>" data-name="<?= esc($name, 'attr') ?>" data-kind-label="<?= esc($kindLabel, 'attr') ?>" data-unit-price="<?= esc((string) $unitPrice, 'attr') ?>" data-box-sheets="<?= esc((string) $boxSheets, 'attr') ?>" data-pack-sheets="<?= esc((string) $packSheets, 'attr') ?>">
|
||||
<?= esc($code) ?> — <?= esc($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 class="text-right px-2 item-amount-cell">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>
|
||||
(() => {
|
||||
@@ -214,9 +229,7 @@
|
||||
const shopSuggest = document.getElementById('shop-search-suggest');
|
||||
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 = document.getElementById('phone-order-form');
|
||||
|
||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||
@@ -293,15 +306,11 @@
|
||||
}
|
||||
|
||||
function calcRow(row) {
|
||||
const select = row.querySelector('.bag-code-select');
|
||||
const qtyInput = row.querySelector('.item-qty-input');
|
||||
const selected = select.options[select.selectedIndex];
|
||||
|
||||
const 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 packSheets = parseInt(selected?.dataset?.packSheets || '0', 10) || 0;
|
||||
const kindLabel = selected?.dataset?.kindLabel || '-';
|
||||
const unitPrice = parseInt(row.dataset.unitPrice || '0', 10) || 0;
|
||||
const boxSheets = parseInt(row.dataset.boxSheets || '0', 10) || 0;
|
||||
const packSheets = parseInt(row.dataset.packSheets || '0', 10) || 0;
|
||||
|
||||
let box = 0;
|
||||
let pack = 0;
|
||||
@@ -321,15 +330,10 @@
|
||||
}
|
||||
|
||||
const amount = unitPrice * qty;
|
||||
const boxPrice = boxSheets * unitPrice;
|
||||
const packPrice = packSheets * unitPrice;
|
||||
|
||||
row.querySelector('.item-kind-cell').textContent = kindLabel;
|
||||
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);
|
||||
row.querySelector('.item-amount-cell').textContent = nf(amount);
|
||||
row.querySelector('.pack-result-cell').textContent = '박스=' + nf(box) + ', 팩=' + nf(pack) + ', 낱장=' + nf(sheet);
|
||||
row.querySelector('.item-box-cell').textContent = box ? nf(box) : '';
|
||||
row.querySelector('.item-pack-cell').textContent = pack ? nf(pack) : '';
|
||||
row.querySelector('.item-sheet-cell').textContent = sheet ? nf(sheet) : '';
|
||||
|
||||
return { qty, amount, box, pack, sheet };
|
||||
}
|
||||
@@ -346,7 +350,9 @@
|
||||
});
|
||||
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);
|
||||
document.getElementById('sum-box').textContent = nf(sumBox);
|
||||
document.getElementById('sum-pack').textContent = nf(sumPack);
|
||||
document.getElementById('sum-sheet').textContent = nf(sumSheet);
|
||||
}
|
||||
|
||||
function clearSearchInputOnly() {
|
||||
@@ -394,46 +400,21 @@
|
||||
shopSelect?.addEventListener('change', updateShopInfo);
|
||||
paymentType?.addEventListener('change', updateShopInfo);
|
||||
|
||||
orderRows?.addEventListener('change', function (e) {
|
||||
if (e.target.closest('.bag-code-select') || e.target.closest('.item-qty-input')) {
|
||||
recalcAllRows();
|
||||
}
|
||||
});
|
||||
orderRows?.addEventListener('input', function (e) {
|
||||
if (e.target.closest('.item-qty-input')) {
|
||||
recalcAllRows();
|
||||
}
|
||||
});
|
||||
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();
|
||||
});
|
||||
|
||||
addRowButton?.addEventListener('click', function () {
|
||||
if (!rowTemplate || !orderRows) return;
|
||||
const fragment = rowTemplate.content.cloneNode(true);
|
||||
orderRows.appendChild(fragment);
|
||||
recalcAllRows();
|
||||
});
|
||||
|
||||
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 (qty > 0) hasItem = true;
|
||||
});
|
||||
if (!hasItem) {
|
||||
e.preventDefault();
|
||||
alert('주문 품목과 수량을 1개 이상 입력해 주세요.');
|
||||
alert('주문 수량을 1개 이상 입력해 주세요.');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user