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:
@@ -77,6 +77,7 @@ $routes->get('bag/order/phone', 'Bag::phoneOrderCreate');
|
|||||||
$routes->get('bag/order/phone/manage', 'Bag::phoneOrderManage');
|
$routes->get('bag/order/phone/manage', 'Bag::phoneOrderManage');
|
||||||
$routes->post('bag/order/phone/manage/update', 'Bag::phoneOrderUpdate');
|
$routes->post('bag/order/phone/manage/update', 'Bag::phoneOrderUpdate');
|
||||||
$routes->post('bag/order/phone/manage/cancel/(:num)', 'Bag::phoneOrderCancel/$1');
|
$routes->post('bag/order/phone/manage/cancel/(:num)', 'Bag::phoneOrderCancel/$1');
|
||||||
|
$routes->get('bag/order/phone/receipt/(:num)', 'Bag::phoneOrderReceipt/$1');
|
||||||
$routes->get('bag/order/change', 'Bag::orderChange');
|
$routes->get('bag/order/change', 'Bag::orderChange');
|
||||||
$routes->get('bag/order/revise/(:num)', 'Bag::orderRevise/$1');
|
$routes->get('bag/order/revise/(:num)', 'Bag::orderRevise/$1');
|
||||||
$routes->get('bag/order/lot-seed', 'Bag::orderLotSeed');
|
$routes->get('bag/order/lot-seed', 'Bag::orderLotSeed');
|
||||||
|
|||||||
@@ -3614,11 +3614,36 @@ SQL);
|
|||||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('매뉴얼 문서를 찾을 수 없습니다.');
|
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('매뉴얼 문서를 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 이 매뉴얼 페이지가 설명하는 화면 → 바로가기 링크(screenHelp 역방향).
|
||||||
|
// screenHelp 에는 "현재경로 매칭"용 비(非)라우트 prefix(bag/sale, bag/order 등)도 섞여 있어
|
||||||
|
// 그대로 쓰면 404가 난다. 같은 소제목(hint)의 후보 중 "실제 GET 라우트"인 첫 경로만 채택.
|
||||||
|
$routeCollection = service('routes');
|
||||||
|
$routeCollection->loadRoutes();
|
||||||
|
$validGet = $routeCollection->getRoutes('GET'); // [경로 => 핸들러] (정적 경로는 키가 곧 경로)
|
||||||
|
|
||||||
|
$screenHelp = config(\Config\Manual::class)->screenHelp ?? [];
|
||||||
|
$jumpByHint = [];
|
||||||
|
foreach ($screenHelp as $path => $val) {
|
||||||
|
[$pSlug, $hint] = array_pad(explode('#', (string) $val, 2), 2, null);
|
||||||
|
if ($pSlug !== $slug || $hint === null || trim($hint) === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isset($jumpByHint[$hint]) || ! isset($validGet[(string) $path])) {
|
||||||
|
continue; // 이미 채택했거나, 실제 라우트가 아니면 건너뜀
|
||||||
|
}
|
||||||
|
$jumpByHint[$hint] = (string) $path;
|
||||||
|
}
|
||||||
|
$jumps = [];
|
||||||
|
foreach ($jumpByHint as $hint => $path) {
|
||||||
|
$jumps[] = ['hint' => $hint, 'url' => base_url($path)];
|
||||||
|
}
|
||||||
|
|
||||||
return $this->render('사용자 매뉴얼 · ' . $page['title'], 'bag/manual', [
|
return $this->render('사용자 매뉴얼 · ' . $page['title'], 'bag/manual', [
|
||||||
'pages' => $renderer->pages(),
|
'pages' => $renderer->pages(),
|
||||||
'current' => $slug,
|
'current' => $slug,
|
||||||
'title' => $page['title'],
|
'title' => $page['title'],
|
||||||
'body' => $body,
|
'body' => $body,
|
||||||
|
'jumps' => $jumps,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7022,7 +7047,7 @@ SQL;
|
|||||||
return redirect()->to(site_url('bag/sales'))->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(site_url('bag/sales'))->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일자별·기간별 조회 (접수일 so_order_date 기준)
|
// 일자별·기간별 조회 (배달일 so_delivery_date 기준)
|
||||||
$startDate = trim((string) ($this->request->getGet('start_date') ?? ''));
|
$startDate = trim((string) ($this->request->getGet('start_date') ?? ''));
|
||||||
$endDate = trim((string) ($this->request->getGet('end_date') ?? ''));
|
$endDate = trim((string) ($this->request->getGet('end_date') ?? ''));
|
||||||
$isYmd = static fn (string $d): bool => preg_match('/^\d{4}-\d{2}-\d{2}$/', $d) === 1;
|
$isYmd = static fn (string $d): bool => preg_match('/^\d{4}-\d{2}-\d{2}$/', $d) === 1;
|
||||||
@@ -7037,10 +7062,10 @@ SQL;
|
|||||||
$builder->where('so_channel', 'phone');
|
$builder->where('so_channel', 'phone');
|
||||||
}
|
}
|
||||||
if ($isYmd($startDate)) {
|
if ($isYmd($startDate)) {
|
||||||
$builder->where('so_order_date >=', $startDate);
|
$builder->where('so_delivery_date >=', $startDate);
|
||||||
}
|
}
|
||||||
if ($isYmd($endDate)) {
|
if ($isYmd($endDate)) {
|
||||||
$builder->where('so_order_date <=', $endDate . ' 23:59:59');
|
$builder->where('so_delivery_date <=', $endDate . ' 23:59:59');
|
||||||
}
|
}
|
||||||
$orders = $builder->orderBy('so_idx', 'DESC')->limit(200)->findAll();
|
$orders = $builder->orderBy('so_idx', 'DESC')->limit(200)->findAll();
|
||||||
|
|
||||||
@@ -7100,6 +7125,7 @@ SQL;
|
|||||||
'soi_bag_name' => (string) ($item->soi_bag_name ?? ''),
|
'soi_bag_name' => (string) ($item->soi_bag_name ?? ''),
|
||||||
'soi_unit_price' => (int) ($item->soi_unit_price ?? 0),
|
'soi_unit_price' => (int) ($item->soi_unit_price ?? 0),
|
||||||
'soi_qty' => (int) ($item->soi_qty ?? 0),
|
'soi_qty' => (int) ($item->soi_qty ?? 0),
|
||||||
|
'soi_packed_qty' => (int) ($item->soi_packed_qty ?? 0),
|
||||||
'soi_amount' => (int) ($item->soi_amount ?? 0),
|
'soi_amount' => (int) ($item->soi_amount ?? 0),
|
||||||
'soi_box_count' => (int) ($item->soi_box_count ?? 0),
|
'soi_box_count' => (int) ($item->soi_box_count ?? 0),
|
||||||
'soi_pack_count' => (int) ($item->soi_pack_count ?? 0),
|
'soi_pack_count' => (int) ($item->soi_pack_count ?? 0),
|
||||||
@@ -7124,7 +7150,10 @@ SQL;
|
|||||||
'ds_addr' => $shop['addr'],
|
'ds_addr' => $shop['addr'],
|
||||||
'so_order_date' => (string) ($order->so_order_date ?? ''),
|
'so_order_date' => (string) ($order->so_order_date ?? ''),
|
||||||
'so_delivery_date' => (string) ($order->so_delivery_date ?? ''),
|
'so_delivery_date' => (string) ($order->so_delivery_date ?? ''),
|
||||||
|
'so_channel' => (string) ($order->so_channel ?? ''),
|
||||||
'so_payment_type' => (string) ($order->so_payment_type ?? ''),
|
'so_payment_type' => (string) ($order->so_payment_type ?? ''),
|
||||||
|
'so_received' => (int) ($order->so_received ?? 0),
|
||||||
|
'so_paid' => (int) ($order->so_paid ?? 0),
|
||||||
'so_total_qty' => (int) ($order->so_total_qty ?? 0),
|
'so_total_qty' => (int) ($order->so_total_qty ?? 0),
|
||||||
'so_total_amount' => (int) ($order->so_total_amount ?? 0),
|
'so_total_amount' => (int) ($order->so_total_amount ?? 0),
|
||||||
'so_status' => (string) ($order->so_status ?? 'normal'),
|
'so_status' => (string) ($order->so_status ?? 'normal'),
|
||||||
@@ -7139,6 +7168,47 @@ SQL;
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 입금자 영수증 출력용 — 단일 접수건의 인쇄용 영수증(독립 HTML, 자동 인쇄).
|
||||||
|
*/
|
||||||
|
public function phoneOrderReceipt(int $id): string|RedirectResponse
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(site_url('bag/order/phone/manage'))->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = model(ShopOrderModel::class)->where('so_idx', $id)->where('so_lg_idx', $lgIdx)->first();
|
||||||
|
if (! $order) {
|
||||||
|
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = model(ShopOrderItemModel::class)
|
||||||
|
->where('soi_so_idx', $id)
|
||||||
|
->orderBy('soi_idx', 'ASC')
|
||||||
|
->findAll();
|
||||||
|
|
||||||
|
$shop = null;
|
||||||
|
$dsIdx = (int) ($order->so_ds_idx ?? 0);
|
||||||
|
if ($dsIdx > 0) {
|
||||||
|
$shop = \Config\Database::connect()->table('designated_shop')
|
||||||
|
->select('ds_shop_no, ds_name, ds_rep_name, ds_tel, ds_addr, ds_addr_detail')
|
||||||
|
->where('ds_idx', $dsIdx)
|
||||||
|
->get()->getRowArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($lgIdx);
|
||||||
|
$lgName = $lgRow ? (string) $lgRow->lg_name : '';
|
||||||
|
|
||||||
|
return view('bag/order_phone_receipt', [
|
||||||
|
'order' => $order,
|
||||||
|
'items' => $items,
|
||||||
|
'shop' => $shop,
|
||||||
|
'lgName' => $lgName,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 전화 주문 상세 품목 수량 수정 저장.
|
* 전화 주문 상세 품목 수량 수정 저장.
|
||||||
*/
|
*/
|
||||||
@@ -7174,6 +7244,10 @@ SQL;
|
|||||||
if (! is_array($qtyInput)) {
|
if (! is_array($qtyInput)) {
|
||||||
$qtyInput = [];
|
$qtyInput = [];
|
||||||
}
|
}
|
||||||
|
$packedInput = $this->request->getPost('item_packed') ?? [];
|
||||||
|
if (! is_array($packedInput)) {
|
||||||
|
$packedInput = [];
|
||||||
|
}
|
||||||
|
|
||||||
$codeSet = [];
|
$codeSet = [];
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
@@ -7230,8 +7304,10 @@ SQL;
|
|||||||
$sheetCount = $qty % $packSheets;
|
$sheetCount = $qty % $packSheets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$packedQty = isset($packedInput[$itemId]) ? max(0, (int) $packedInput[$itemId]) : (int) ($item->soi_packed_qty ?? 0);
|
||||||
$itemModel->update($itemId, [
|
$itemModel->update($itemId, [
|
||||||
'soi_qty' => $qty,
|
'soi_qty' => $qty,
|
||||||
|
'soi_packed_qty' => $packedQty,
|
||||||
'soi_amount' => $amount,
|
'soi_amount' => $amount,
|
||||||
'soi_box_count' => $boxCount,
|
'soi_box_count' => $boxCount,
|
||||||
'soi_pack_count' => $packCount,
|
'soi_pack_count' => $packCount,
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ class ShopOrderItemModel extends Model
|
|||||||
protected $useTimestamps = false;
|
protected $useTimestamps = false;
|
||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'soi_so_idx', 'soi_bag_code', 'soi_bag_name', 'soi_unit_price',
|
'soi_so_idx', 'soi_bag_code', 'soi_bag_name', 'soi_unit_price',
|
||||||
'soi_qty', 'soi_amount', 'soi_box_count', 'soi_pack_count', 'soi_sheet_count',
|
'soi_qty', 'soi_packed_qty', 'soi_amount', 'soi_box_count', 'soi_pack_count', 'soi_sheet_count',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,11 +44,15 @@ $searchQ = (string) (service('request')->getGet('q') ?? '');
|
|||||||
.manual-prose tbody tr:nth-child(even) td { background: #f9fafb; }
|
.manual-prose tbody tr:nth-child(even) td { background: #f9fafb; }
|
||||||
.manual-prose hr { margin: 1.6rem 0; border: 0; border-top: 1px solid #e5e7eb; }
|
.manual-prose hr { margin: 1.6rem 0; border: 0; border-top: 1px solid #e5e7eb; }
|
||||||
.manual-toc a.active { background: #1a2b4b; color: #fff; font-weight: 700; }
|
.manual-toc a.active { background: #1a2b4b; color: #fff; font-weight: 700; }
|
||||||
|
/* 소제목 옆 "화면 열기" 바로가기 */
|
||||||
|
.manual-jump { display: inline-flex; align-items: center; gap: 4px; margin-left: 8px; vertical-align: middle; font-size: 0.72rem; font-weight: 700; color: #1d4ed8; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 9999px; padding: 2px 9px; text-decoration: none; cursor: pointer; white-space: nowrap; }
|
||||||
|
.manual-jump:hover { background: #1d4ed8; color: #fff; border-color: #1d4ed8; }
|
||||||
|
.manual-jump i { font-size: 0.65rem; }
|
||||||
/* "이 화면 설명"으로 들어왔을 때 해당 소제목 강조 */
|
/* "이 화면 설명"으로 들어왔을 때 해당 소제목 강조 */
|
||||||
.manual-prose h2.hl-flash, .manual-prose h3.hl-flash { background: #fef9c3; box-shadow: -6px 0 0 #fef9c3, 6px 0 0 #fef9c3; border-radius: 4px; animation: hlFlash 2.6s ease-out 1; }
|
.manual-prose h2.hl-flash, .manual-prose h3.hl-flash { background: #fef9c3; box-shadow: -6px 0 0 #fef9c3, 6px 0 0 #fef9c3; border-radius: 4px; animation: hlFlash 2.6s ease-out 1; }
|
||||||
@keyframes hlFlash { 0%, 45% { background: #fde047; box-shadow: -6px 0 0 #fde047, 6px 0 0 #fde047; } 100% { background: transparent; box-shadow: -6px 0 0 transparent, 6px 0 0 transparent; } }
|
@keyframes hlFlash { 0%, 45% { background: #fde047; box-shadow: -6px 0 0 #fde047, 6px 0 0 #fde047; } 100% { background: transparent; box-shadow: -6px 0 0 transparent, 6px 0 0 transparent; } }
|
||||||
@media print {
|
@media print {
|
||||||
.manual-toc, .manual-actions, .manual-nav { display: none !important; }
|
.manual-toc, .manual-actions, .manual-nav, .manual-jump { display: none !important; }
|
||||||
.manual-layout { display: block !important; }
|
.manual-layout { display: block !important; }
|
||||||
.manual-prose { max-width: none; }
|
.manual-prose { max-width: none; }
|
||||||
}
|
}
|
||||||
@@ -111,6 +115,8 @@ $searchQ = (string) (service('request')->getGet('q') ?? '');
|
|||||||
var SEARCH_URL = location.origin + <?= json_encode((string) parse_url(base_url('bag/manual/search'), PHP_URL_PATH), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
var SEARCH_URL = location.origin + <?= json_encode((string) parse_url(base_url('bag/manual/search'), PHP_URL_PATH), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
var BASE = location.origin + <?= json_encode((string) parse_url(base_url('bag/manual/'), PHP_URL_PATH), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
var BASE = location.origin + <?= json_encode((string) parse_url(base_url('bag/manual/'), PHP_URL_PATH), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||||
var Q0 = <?= json_encode($searchQ, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>;
|
var Q0 = <?= json_encode($searchQ, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>;
|
||||||
|
// 이 매뉴얼 페이지가 설명하는 화면 바로가기 목록 [{hint, url}, ...]
|
||||||
|
var JUMPS = <?= json_encode($jumps ?? [], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>;
|
||||||
// "이 화면 설명"으로 들어온 경우: 해당 소제목으로 스크롤 + 강조.
|
// "이 화면 설명"으로 들어온 경우: 해당 소제목으로 스크롤 + 강조.
|
||||||
// 드로어를 껐다 다시 켜도(같은 URL이라 iframe 재로드 안 됨) 부모가 보낸 메시지로 다시 강조한다.
|
// 드로어를 껐다 다시 켜도(같은 URL이라 iframe 재로드 안 됨) 부모가 보낸 메시지로 다시 강조한다.
|
||||||
var hlFlashTimer = null;
|
var hlFlashTimer = null;
|
||||||
@@ -140,6 +146,41 @@ $searchQ = (string) (service('request')->getGet('q') ?? '');
|
|||||||
if (e.origin !== location.origin) return;
|
if (e.origin !== location.origin) return;
|
||||||
if (e.data && e.data.type === 'manual-hl') runHl();
|
if (e.data && e.data.type === 'manual-hl') runHl();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 해당 화면으로 이동: 워크스페이스(탭) 안이면 새 탭으로, 아니면 최상위 창을 이동.
|
||||||
|
function openScreen(url, title) {
|
||||||
|
try { if (window.top && typeof window.top.wsOpenTab === 'function') { window.top.wsOpenTab(url, title); return; } } catch (e) {}
|
||||||
|
try { if (window.parent && typeof window.parent.wsOpenTab === 'function') { window.parent.wsOpenTab(url, title); return; } } catch (e) {}
|
||||||
|
try { window.top.location.href = url; } catch (e) { window.location.href = url; }
|
||||||
|
}
|
||||||
|
// 각 소제목(h2/h3) 옆에 "화면 열기" 바로가기 버튼을 붙인다(hint 매칭).
|
||||||
|
(function attachJumps() {
|
||||||
|
if (!JUMPS || !JUMPS.length) return;
|
||||||
|
var prose = document.querySelector('.manual-prose');
|
||||||
|
if (!prose) return;
|
||||||
|
var norm = function (s) { return String(s || '').replace(/\s+/g, ' ').trim().toLowerCase(); };
|
||||||
|
var heads = prose.querySelectorAll('h2, h3');
|
||||||
|
JUMPS.forEach(function (j) {
|
||||||
|
var needle = norm(j.hint);
|
||||||
|
if (!needle || !j.url) return;
|
||||||
|
for (var i = 0; i < heads.length; i++) {
|
||||||
|
var h = heads[i];
|
||||||
|
if (h.getAttribute('data-jump')) continue; // 한 제목엔 하나만
|
||||||
|
if (norm(h.textContent).indexOf(needle) < 0) continue;
|
||||||
|
var label = (h.textContent || '화면').trim(); // 버튼 추가 전 제목 확보
|
||||||
|
var url = j.url;
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.className = 'manual-jump';
|
||||||
|
a.href = url;
|
||||||
|
a.title = '이 화면으로 이동';
|
||||||
|
a.innerHTML = '<i class="fa-solid fa-arrow-up-right-from-square"></i> 화면 열기';
|
||||||
|
a.addEventListener('click', function (ev) { ev.preventDefault(); openScreen(url, label); });
|
||||||
|
h.appendChild(a);
|
||||||
|
h.setAttribute('data-jump', '1');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
var input = document.getElementById('manualSearchInput');
|
var input = document.getElementById('manualSearchInput');
|
||||||
var box = document.getElementById('manualSearchResults');
|
var box = document.getElementById('manualSearchResults');
|
||||||
|
|
||||||
|
|||||||
@@ -96,69 +96,118 @@
|
|||||||
|
|
||||||
<input type="hidden" name="so_delivery_date" value="<?= esc(date('Y-m-d', strtotime('+1 day'))) ?>"/>
|
<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="mt-4">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<label class="block text-sm font-bold text-gray-700 mb-2">전화 주문접수표 (일괄)</label>
|
||||||
<label class="block text-sm font-bold text-gray-700">전화 주문접수표</label>
|
<style>
|
||||||
<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>
|
/* 그룹 헤더(rowspan/colspan) 경계선이 단가까지 이어지는 현상 방지 — 헤더 맨 아래 한 줄만 */
|
||||||
</div>
|
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">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-14 text-center">구분</th>
|
<th class="text-center" rowspan="2">구분</th>
|
||||||
<th class="w-56">품목</th>
|
<th rowspan="2">봉투종류</th>
|
||||||
<th class="w-40 text-right">1박스(낱장/판매가)</th>
|
<th class="text-center" colspan="2">Pack</th>
|
||||||
<th class="w-40 text-right">1팩(낱장/판매가)</th>
|
<th class="text-center" colspan="2">Box</th>
|
||||||
<th class="w-24 text-right">단가</th>
|
<th class="text-right" rowspan="2">단가</th>
|
||||||
<th class="w-28 text-right">주문수량</th>
|
<th class="text-center" colspan="2">주문내용</th>
|
||||||
<th class="w-28 text-right">금액</th>
|
<th class="text-center" colspan="3">포장</th>
|
||||||
<th class="w-44 text-right">포장(박스/팩/낱장)</th>
|
</tr>
|
||||||
<th class="w-20 text-center">삭제</th>
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="order-rows">
|
<tbody id="order-rows">
|
||||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
<?php if (empty($orderGroups)): ?>
|
||||||
<tr class="order-row">
|
<tr><td colspan="12" class="text-center text-gray-400 py-6">등록된 봉투 품목이 없습니다.</td></tr>
|
||||||
<td class="text-center item-kind-cell">-</td>
|
<?php else: ?>
|
||||||
<td>
|
<?php foreach ($orderGroups as $g): ?>
|
||||||
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full bag-code-select" name="item_bag_code[]">
|
<?php $rc = count($g['rows']); $firstRow = true; ?>
|
||||||
<option value="">선택</option>
|
<?php foreach ($g['rows'] as $r): ?>
|
||||||
<?php foreach ($bagCodes as $cd): ?>
|
<?php $boxPrice = $r['box'] * $r['price']; $packPrice = $r['pack'] * $r['price']; ?>
|
||||||
<?php
|
<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') ?>">
|
||||||
$code = (string) $cd->cd_code;
|
<?php if ($firstRow): ?>
|
||||||
$name = (string) ($cd->cd_name ?? '');
|
<td class="text-center align-top font-semibold text-gray-700" rowspan="<?= (int) $rc ?>"><?= esc($g['label']) ?></td>
|
||||||
$price = $priceMap[$code] ?? null;
|
<?php endif; ?>
|
||||||
$unit = $unitMap[$code] ?? null;
|
<td class="text-left pl-2"><?= esc($r['name']) ?><input type="hidden" name="item_bag_code[]" value="<?= esc($r['code'], 'attr') ?>"/></td>
|
||||||
$unitPrice = (int) ($price->bp_consumer ?? 0);
|
<td class="text-right px-2"><?= number_format($r['pack']) ?></td>
|
||||||
$boxSheets = (int) ($unit->pu_total_per_box ?? 0);
|
<td class="text-right px-2"><?= number_format($packPrice) ?></td>
|
||||||
$packSheets = (int) ($unit->pu_pack_per_sheet ?? 0);
|
<td class="text-right px-2"><?= number_format($r['box']) ?></td>
|
||||||
$kindLabel = (mb_strpos($name, '스티커') !== false) ? '스티커' : '봉투';
|
<td class="text-right px-2"><?= number_format($boxPrice) ?></td>
|
||||||
?>
|
<td class="text-right px-2"><?= number_format($r['price']) ?></td>
|
||||||
<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><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 item-amount-cell">0</td>
|
||||||
<td class="text-right px-2 pack-result-cell">박스=0, 팩=0, 낱장=0</td>
|
<td class="text-center item-box-cell">0</td>
|
||||||
<td class="text-center px-2">
|
<td class="text-center item-pack-cell">0</td>
|
||||||
<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 class="text-center item-sheet-cell">0</td>
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endfor; ?>
|
<?php $firstRow = false; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="font-semibold bg-gray-50">
|
<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-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-amount">0</td>
|
||||||
<td class="text-right px-2 py-1" id="sum-pack">박스=0, 팩=0, 낱장=0</td>
|
<td class="text-center px-2 py-1" id="sum-box">0</td>
|
||||||
<td></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>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
@@ -172,40 +221,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
@@ -214,9 +229,7 @@
|
|||||||
const shopSuggest = document.getElementById('shop-search-suggest');
|
const shopSuggest = document.getElementById('shop-search-suggest');
|
||||||
const paymentType = document.getElementById('payment-type');
|
const paymentType = document.getElementById('payment-type');
|
||||||
const paymentGuide = document.getElementById('payment-guide');
|
const paymentGuide = document.getElementById('payment-guide');
|
||||||
const addRowButton = document.getElementById('add-order-row');
|
|
||||||
const orderRows = document.getElementById('order-rows');
|
const orderRows = document.getElementById('order-rows');
|
||||||
const rowTemplate = document.getElementById('order-row-template');
|
|
||||||
const form = document.getElementById('phone-order-form');
|
const form = document.getElementById('phone-order-form');
|
||||||
|
|
||||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||||
@@ -293,15 +306,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function calcRow(row) {
|
function calcRow(row) {
|
||||||
const select = row.querySelector('.bag-code-select');
|
|
||||||
const qtyInput = row.querySelector('.item-qty-input');
|
const qtyInput = row.querySelector('.item-qty-input');
|
||||||
const selected = select.options[select.selectedIndex];
|
|
||||||
|
|
||||||
const qty = parseInt(qtyInput.value || '0', 10) || 0;
|
const qty = parseInt(qtyInput.value || '0', 10) || 0;
|
||||||
const unitPrice = parseInt(selected?.dataset?.unitPrice || '0', 10) || 0;
|
const unitPrice = parseInt(row.dataset.unitPrice || '0', 10) || 0;
|
||||||
const boxSheets = parseInt(selected?.dataset?.boxSheets || '0', 10) || 0;
|
const boxSheets = parseInt(row.dataset.boxSheets || '0', 10) || 0;
|
||||||
const packSheets = parseInt(selected?.dataset?.packSheets || '0', 10) || 0;
|
const packSheets = parseInt(row.dataset.packSheets || '0', 10) || 0;
|
||||||
const kindLabel = selected?.dataset?.kindLabel || '-';
|
|
||||||
|
|
||||||
let box = 0;
|
let box = 0;
|
||||||
let pack = 0;
|
let pack = 0;
|
||||||
@@ -321,15 +330,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const amount = unitPrice * qty;
|
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('.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 };
|
return { qty, amount, box, pack, sheet };
|
||||||
}
|
}
|
||||||
@@ -346,7 +350,9 @@
|
|||||||
});
|
});
|
||||||
document.getElementById('sum-qty').textContent = nf(sumQty);
|
document.getElementById('sum-qty').textContent = nf(sumQty);
|
||||||
document.getElementById('sum-amount').textContent = nf(sumAmount);
|
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() {
|
function clearSearchInputOnly() {
|
||||||
@@ -394,46 +400,21 @@
|
|||||||
shopSelect?.addEventListener('change', updateShopInfo);
|
shopSelect?.addEventListener('change', updateShopInfo);
|
||||||
paymentType?.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) {
|
orderRows?.addEventListener('input', function (e) {
|
||||||
if (e.target.closest('.item-qty-input')) {
|
if (e.target.closest('.item-qty-input')) {
|
||||||
recalcAllRows();
|
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) {
|
form?.addEventListener('submit', function (e) {
|
||||||
let hasItem = false;
|
let hasItem = false;
|
||||||
document.querySelectorAll('.order-row').forEach((row) => {
|
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;
|
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) {
|
if (!hasItem) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('주문 품목과 수량을 1개 이상 입력해 주세요.');
|
alert('주문 수량을 1개 이상 입력해 주세요.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<span class="text-sm font-bold text-gray-700">전화 주문 접수 관리</span>
|
<span class="text-sm font-bold text-gray-700">전화 주문 접수 관리</span>
|
||||||
</div>
|
</div>
|
||||||
<form method="get" action="<?= base_url('bag/order/phone/manage') ?>" class="mt-2 flex flex-wrap items-center gap-2 text-sm">
|
<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>
|
<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"/>
|
<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>
|
<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"/>
|
<input type="date" lang="ko" name="end_date" value="<?= esc($endDate, 'attr') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
@@ -25,9 +25,13 @@
|
|||||||
<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-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_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="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="w-14 text-center sort-th cursor-pointer select-none" data-sort="so_channel" 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="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_payment_type" data-type="str">결제<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>
|
<th class="w-14 text-center sort-th cursor-pointer select-none" data-sort="so_received" data-type="num">포장<span class="sort-ind"></span></th>
|
||||||
|
<th class="w-14 text-center sort-th cursor-pointer select-none" data-sort="so_paid" data-type="num">입금<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-14 text-center sort-th cursor-pointer select-none" data-sort="so_status" data-type="str">취소<span class="sort-ind"></span></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="order-list-body"></tbody>
|
<tbody id="order-list-body"></tbody>
|
||||||
@@ -68,19 +72,21 @@
|
|||||||
<th class="text-left">품목</th>
|
<th class="text-left">품목</th>
|
||||||
<th class="w-24 text-right">단가</th>
|
<th class="w-24 text-right">단가</th>
|
||||||
<th class="w-24 text-right">접수량</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-28 text-right">접수금액</th>
|
||||||
<th class="w-40 text-right">포장단위(박스/팩/낱장)</th>
|
<th class="w-40 text-right">포장단위(박스/팩/낱장)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="detail-items-body">
|
<tbody id="detail-items-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center py-6 text-gray-400">왼쪽 리스트에서 주문을 선택해 주세요.</td>
|
<td colspan="7" class="text-center py-6 text-gray-400">왼쪽 리스트에서 주문을 선택해 주세요.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="font-semibold bg-gray-50">
|
<tr class="font-semibold bg-gray-50">
|
||||||
<td colspan="3" class="text-right px-2 py-1">합계</td>
|
<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-qty">0</td>
|
||||||
|
<td class="text-right px-2 py-1" id="detail-sum-packed">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-amount">0</td>
|
||||||
<td class="text-right px-2 py-1" id="detail-sum-pack">박스=0, 팩=0, 낱장=0</td>
|
<td class="text-right px-2 py-1" id="detail-sum-pack">박스=0, 팩=0, 낱장=0</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -88,8 +94,32 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="flex items-center justify-between px-1 mb-1 text-sm">
|
||||||
|
<span class="font-semibold text-gray-700">포장 명세</span>
|
||||||
|
<span class="text-gray-600">포장량: <span id="packing-total" class="font-bold text-blue-700">0</span> 개</span>
|
||||||
|
</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-12 text-center">No</th>
|
||||||
|
<th class="text-left">봉투종류</th>
|
||||||
|
<th class="w-28 text-center">봉투코드</th>
|
||||||
|
<th class="w-20 text-right">수량</th>
|
||||||
|
<th class="w-16 text-center">포장</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="packing-body">
|
||||||
|
<tr><td colspan="5" class="text-center py-6 text-gray-400">주문을 선택해 주세요.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div><!-- /포장 명세 -->
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<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>
|
<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>
|
||||||
|
<button type="button" id="btn-receipt" class="border border-gray-300 text-gray-700 px-5 py-1.5 rounded-sm text-sm hover:bg-gray-50" disabled>입금자 영수증 출력</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -112,6 +142,18 @@
|
|||||||
const inputSoIdx = document.getElementById('detail-so-idx');
|
const inputSoIdx = document.getElementById('detail-so-idx');
|
||||||
const btnSave = document.getElementById('btn-save');
|
const btnSave = document.getElementById('btn-save');
|
||||||
const btnCancel = document.getElementById('btn-cancel-order');
|
const btnCancel = document.getElementById('btn-cancel-order');
|
||||||
|
const btnReceipt = document.getElementById('btn-receipt');
|
||||||
|
const receiptBase = '<?= base_url('bag/order/phone/receipt') ?>';
|
||||||
|
btnReceipt?.addEventListener('click', () => {
|
||||||
|
const id = inputSoIdx.value;
|
||||||
|
if (!id) return;
|
||||||
|
const o = orderMap.get(String(id));
|
||||||
|
// 이미 포장(수령)이 완료된 건의 영수증 재발행 시 안내
|
||||||
|
if (o && Number(o.so_received) === 1) {
|
||||||
|
if (!confirm('포장이 완료된 건입니다.\n그래도 영수증을 다시 출력할까요?')) return;
|
||||||
|
}
|
||||||
|
window.open(receiptBase + '/' + encodeURIComponent(id), '_blank');
|
||||||
|
});
|
||||||
|
|
||||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||||
|
|
||||||
@@ -161,14 +203,18 @@
|
|||||||
sheet = qty % packSheets;
|
sheet = qty % packSheets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const packedInput = tr.querySelector('.item-packed-input');
|
||||||
|
const packed = packedInput ? Math.max(0, parseInt(packedInput.value || '0', 10) || 0) : 0;
|
||||||
|
|
||||||
const amount = qty * unitPrice;
|
const amount = qty * unitPrice;
|
||||||
tr.querySelector('.item-amount-cell').textContent = nf(amount);
|
tr.querySelector('.item-amount-cell').textContent = nf(amount);
|
||||||
tr.querySelector('.item-pack-cell').textContent = `박스=${nf(box)}, 팩=${nf(pack)}, 낱장=${nf(sheet)}`;
|
tr.querySelector('.item-pack-cell').textContent = `박스=${nf(box)}, 팩=${nf(pack)}, 낱장=${nf(sheet)}`;
|
||||||
return { qty, amount, box, pack, sheet };
|
return { qty, packed, amount, box, pack, sheet };
|
||||||
}
|
}
|
||||||
|
|
||||||
function recalcTotals() {
|
function recalcTotals() {
|
||||||
let sumQty = 0;
|
let sumQty = 0;
|
||||||
|
let sumPacked = 0;
|
||||||
let sumAmount = 0;
|
let sumAmount = 0;
|
||||||
let sumBox = 0;
|
let sumBox = 0;
|
||||||
let sumPack = 0;
|
let sumPack = 0;
|
||||||
@@ -177,6 +223,7 @@
|
|||||||
detailBody.querySelectorAll('tr.order-item-row').forEach((tr) => {
|
detailBody.querySelectorAll('tr.order-item-row').forEach((tr) => {
|
||||||
const r = calcRow(tr);
|
const r = calcRow(tr);
|
||||||
sumQty += r.qty;
|
sumQty += r.qty;
|
||||||
|
sumPacked += r.packed;
|
||||||
sumAmount += r.amount;
|
sumAmount += r.amount;
|
||||||
sumBox += r.box;
|
sumBox += r.box;
|
||||||
sumPack += r.pack;
|
sumPack += r.pack;
|
||||||
@@ -184,6 +231,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('detail-sum-qty').textContent = nf(sumQty);
|
document.getElementById('detail-sum-qty').textContent = nf(sumQty);
|
||||||
|
document.getElementById('detail-sum-packed').textContent = nf(sumPacked);
|
||||||
document.getElementById('detail-sum-amount').textContent = nf(sumAmount);
|
document.getElementById('detail-sum-amount').textContent = nf(sumAmount);
|
||||||
document.getElementById('detail-sum-pack').textContent = `박스=${nf(sumBox)}, 팩=${nf(sumPack)}, 낱장=${nf(sumSheet)}`;
|
document.getElementById('detail-sum-pack').textContent = `박스=${nf(sumBox)}, 팩=${nf(sumPack)}, 낱장=${nf(sumSheet)}`;
|
||||||
}
|
}
|
||||||
@@ -199,10 +247,13 @@
|
|||||||
const isCancelled = order.so_status === 'cancelled';
|
const isCancelled = order.so_status === 'cancelled';
|
||||||
btnSave.disabled = isCancelled;
|
btnSave.disabled = isCancelled;
|
||||||
btnCancel.disabled = isCancelled;
|
btnCancel.disabled = isCancelled;
|
||||||
|
if (btnReceipt) btnReceipt.disabled = false;
|
||||||
|
|
||||||
|
renderPacking(order); // 포장 명세 탭 동기화
|
||||||
|
|
||||||
const items = Array.isArray(order.items) ? order.items : [];
|
const items = Array.isArray(order.items) ? order.items : [];
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
detailBody.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-gray-400">품목 정보가 없습니다.</td></tr>';
|
detailBody.innerHTML = '<tr><td colspan="7" class="text-center py-6 text-gray-400">품목 정보가 없습니다.</td></tr>';
|
||||||
recalcTotals();
|
recalcTotals();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -211,6 +262,7 @@
|
|||||||
const itemId = String(item.soi_idx || '');
|
const itemId = String(item.soi_idx || '');
|
||||||
const bagName = `${item.soi_bag_code || ''} ${item.soi_bag_name || ''}`.trim();
|
const bagName = `${item.soi_bag_code || ''} ${item.soi_bag_name || ''}`.trim();
|
||||||
const qty = parseInt(item.soi_qty || 0, 10) || 0;
|
const qty = parseInt(item.soi_qty || 0, 10) || 0;
|
||||||
|
const packedQty = parseInt(item.soi_packed_qty || 0, 10) || 0;
|
||||||
const unitPrice = parseInt(item.soi_unit_price || 0, 10) || 0;
|
const unitPrice = parseInt(item.soi_unit_price || 0, 10) || 0;
|
||||||
const amount = parseInt(item.soi_amount || 0, 10) || 0;
|
const amount = parseInt(item.soi_amount || 0, 10) || 0;
|
||||||
const box = parseInt(item.soi_box_count || 0, 10) || 0;
|
const box = parseInt(item.soi_box_count || 0, 10) || 0;
|
||||||
@@ -227,6 +279,9 @@
|
|||||||
<td class="text-right pr-2">
|
<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' : ''}/>
|
<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>
|
||||||
|
<td class="text-right pr-2">
|
||||||
|
<input type="number" min="0" class="item-packed-input border border-gray-300 rounded px-2 py-1 w-24 text-right" name="item_packed[${itemId}]" value="${packedQty}" ${isCancelled ? 'disabled' : ''}/>
|
||||||
|
</td>
|
||||||
<td class="text-right pr-2 item-amount-cell">${nf(amount)}</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>
|
<td class="text-right pr-2 item-pack-cell">박스=${nf(box)}, 팩=${nf(pack)}, 낱장=${nf(sheet)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -236,6 +291,47 @@
|
|||||||
recalcTotals();
|
recalcTotals();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 포장 명세 탭: 주문 품목을 박스/팩/낱장 단위 행으로 펼침 ──────────
|
||||||
|
const packingBody = document.getElementById('packing-body');
|
||||||
|
const packingTotal = document.getElementById('packing-total');
|
||||||
|
function packEsc(s) { return String(s == null ? '' : s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); }
|
||||||
|
function renderPacking(order) {
|
||||||
|
if (!packingBody) return;
|
||||||
|
const items = Array.isArray(order.items) ? order.items : [];
|
||||||
|
const rows = [];
|
||||||
|
let no = 0;
|
||||||
|
const addRow = (code, name, qty, unit) => {
|
||||||
|
no++;
|
||||||
|
rows.push('<tr><td class="text-center">' + no + '</td>'
|
||||||
|
+ '<td class="text-left pl-2">' + packEsc(code) + ' ' + packEsc(name) + '</td>'
|
||||||
|
+ '<td class="text-center text-gray-400">-</td>'
|
||||||
|
+ '<td class="text-right pr-2">' + nf(qty) + '</td>'
|
||||||
|
+ '<td class="text-center">' + unit + '</td></tr>');
|
||||||
|
};
|
||||||
|
items.forEach((it) => {
|
||||||
|
const code = it.soi_bag_code || '';
|
||||||
|
const name = it.soi_bag_name || '';
|
||||||
|
let qty = parseInt(it.soi_qty || 0, 10) || 0;
|
||||||
|
const boxSheets = parseInt(it.box_sheets || 0, 10) || 0;
|
||||||
|
const packSheets = parseInt(it.pack_sheets || 0, 10) || 0;
|
||||||
|
let box = 0, pack = 0, sheet = qty;
|
||||||
|
if (boxSheets > 0) {
|
||||||
|
box = Math.floor(qty / boxSheets);
|
||||||
|
const rem = qty % boxSheets;
|
||||||
|
if (packSheets > 0) { pack = Math.floor(rem / packSheets); sheet = rem % packSheets; }
|
||||||
|
else { sheet = rem; }
|
||||||
|
} else if (packSheets > 0) {
|
||||||
|
pack = Math.floor(qty / packSheets); sheet = qty % packSheets;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < box; i++) { addRow(code, name, boxSheets, '박스'); }
|
||||||
|
for (let i = 0; i < pack; i++) { addRow(code, name, packSheets, '팩'); }
|
||||||
|
if (sheet > 0) { addRow(code, name, sheet, '낱장'); }
|
||||||
|
});
|
||||||
|
packingBody.innerHTML = rows.length ? rows.join('')
|
||||||
|
: '<tr><td colspan="5" class="text-center py-6 text-gray-400">포장할 품목이 없습니다.</td></tr>';
|
||||||
|
if (packingTotal) packingTotal.textContent = String(no);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 접수 리스트 렌더 + 헤더 클릭 정렬 ──────────────────────────
|
// ── 접수 리스트 렌더 + 헤더 클릭 정렬 ──────────────────────────
|
||||||
let sortKey = 'so_idx', sortDir = 'desc', selectedId = null;
|
let sortKey = 'so_idx', sortDir = 'desc', selectedId = null;
|
||||||
const esc = (s) => String(s == null ? '' : s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
const esc = (s) => String(s == null ? '' : s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
||||||
@@ -243,11 +339,17 @@
|
|||||||
function rowHtml(o) {
|
function rowHtml(o) {
|
||||||
const cancelled = o.so_status === 'cancelled';
|
const cancelled = o.so_status === 'cancelled';
|
||||||
const dDate = o.so_delivery_date ? String(o.so_delivery_date).slice(0, 10) : '';
|
const dDate = o.so_delivery_date ? String(o.so_delivery_date).slice(0, 10) : '';
|
||||||
|
const channel = (o.so_channel && o.so_channel !== 'phone') ? o.so_channel : '전화';
|
||||||
|
const packed = Number(o.so_received) === 1;
|
||||||
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}">
|
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">${o.so_idx}</td>
|
||||||
<td class="text-center">${esc(o.so_order_date)}</td>
|
<td class="text-center">${esc(o.so_order_date)}</td>
|
||||||
<td class="text-center">${esc(dDate)}</td>
|
<td class="text-center">${esc(dDate)}</td>
|
||||||
|
<td class="text-center">${esc(channel)}</td>
|
||||||
<td class="text-left pl-2">${esc(o.so_ds_name)}</td>
|
<td class="text-left pl-2">${esc(o.so_ds_name)}</td>
|
||||||
|
<td class="text-center">${esc(o.so_payment_type || '-')}</td>
|
||||||
|
<td class="text-center" style="text-align:center;">${packed ? '<span style="color:#2563eb;font-weight:700;">O</span>' : '<span class="text-gray-400">X</span>'}</td>
|
||||||
|
<td class="text-center" style="text-align:center;">${Number(o.so_paid) === 1 ? '<span style="color:#e11d48;font-size:11px;" title="입금">●</span>' : ''}</td>
|
||||||
<td class="text-right pr-2">${nf(o.so_total_amount || 0)}</td>
|
<td class="text-right pr-2">${nf(o.so_total_amount || 0)}</td>
|
||||||
<td class="text-center">${cancelled ? '취소' : '정상'}</td>
|
<td class="text-center">${cancelled ? '취소' : '정상'}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
@@ -264,7 +366,7 @@
|
|||||||
});
|
});
|
||||||
listBody.innerHTML = arr.length
|
listBody.innerHTML = arr.length
|
||||||
? arr.map(rowHtml).join('')
|
? arr.map(rowHtml).join('')
|
||||||
: '<tr><td colspan="6" class="text-center py-8 text-gray-400">전화 주문 데이터가 없습니다.</td></tr>';
|
: '<tr><td colspan="10" class="text-center py-8 text-gray-400">전화 주문 데이터가 없습니다.</td></tr>';
|
||||||
document.querySelectorAll('.sort-th .sort-ind').forEach((s) => { s.textContent = ''; });
|
document.querySelectorAll('.sort-th .sort-ind').forEach((s) => { s.textContent = ''; });
|
||||||
if (th) th.querySelector('.sort-ind').textContent = sortDir === 'asc' ? ' ▲' : ' ▼';
|
if (th) th.querySelector('.sort-ind').textContent = sortDir === 'asc' ? ' ▲' : ' ▼';
|
||||||
}
|
}
|
||||||
@@ -287,7 +389,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
detailBody?.addEventListener('input', (e) => {
|
detailBody?.addEventListener('input', (e) => {
|
||||||
if (e.target.closest('.item-qty-input')) {
|
if (e.target.closest('.item-qty-input') || e.target.closest('.item-packed-input')) {
|
||||||
recalcTotals();
|
recalcTotals();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
89
app/Views/bag/order_phone_receipt.php
Normal file
89
app/Views/bag/order_phone_receipt.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* 입금자 영수증 출력용 (독립 인쇄 화면).
|
||||||
|
* @var object $order
|
||||||
|
* @var list<object> $items
|
||||||
|
* @var array|null $shop
|
||||||
|
* @var string $lgName
|
||||||
|
*/
|
||||||
|
$nf = static fn ($n): string => number_format((int) $n);
|
||||||
|
$paid = (int) ($order->so_paid ?? 0) === 1;
|
||||||
|
$addr = $shop ? trim((string) ($shop['ds_addr'] ?? '') . ' ' . (string) ($shop['ds_addr_detail'] ?? '')) : '';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>입금 영수증 #<?= (int) $order->so_idx ?></title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: 'Malgun Gothic', 'Noto Sans KR', sans-serif; color: #111; margin: 0; padding: 24px; }
|
||||||
|
.receipt { max-width: 620px; margin: 0 auto; }
|
||||||
|
h1 { text-align: center; font-size: 1.5rem; letter-spacing: 8px; margin: 0 0 4px; }
|
||||||
|
.sub { text-align: center; color: #555; font-size: .85rem; margin-bottom: 18px; }
|
||||||
|
.meta { width: 100%; border-collapse: collapse; margin-bottom: 14px; font-size: .85rem; }
|
||||||
|
.meta th { text-align: left; width: 90px; color: #555; padding: 3px 6px; vertical-align: top; }
|
||||||
|
.meta td { padding: 3px 6px; }
|
||||||
|
table.items { width: 100%; border-collapse: collapse; font-size: .82rem; }
|
||||||
|
table.items th, table.items td { border: 1px solid #bbb; padding: 5px 7px; }
|
||||||
|
table.items th { background: #f1f3f5; }
|
||||||
|
table.items td.num { text-align: right; }
|
||||||
|
tfoot td { font-weight: bold; background: #fafafa; }
|
||||||
|
.paidbox { margin-top: 16px; text-align: center; font-size: 1rem; font-weight: bold; }
|
||||||
|
.paid { color: #c0392b; }
|
||||||
|
.unpaid { color: #888; }
|
||||||
|
.foot { margin-top: 26px; text-align: center; color: #666; font-size: .75rem; }
|
||||||
|
.noprint { text-align: center; margin: 18px 0; }
|
||||||
|
@media print { .noprint { display: none; } body { padding: 0; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="receipt">
|
||||||
|
<h1>영 수 증</h1>
|
||||||
|
<div class="sub"><?= esc($lgName) ?> 종량제 봉투 · 전화 접수 입금 영수증</div>
|
||||||
|
|
||||||
|
<table class="meta">
|
||||||
|
<tr><th>접수번호</th><td>#<?= (int) $order->so_idx ?></td><th>접수일</th><td><?= esc((string) ($order->so_order_date ?? '')) ?></td></tr>
|
||||||
|
<tr><th>판매소</th><td><?= esc($shop['ds_name'] ?? (string) ($order->so_ds_name ?? '')) ?> <?= $shop ? '(' . esc((string) $shop['ds_shop_no']) . ')' : '' ?></td><th>배달일</th><td><?= esc((string) ($order->so_delivery_date ?? '')) ?></td></tr>
|
||||||
|
<tr><th>담당자</th><td><?= esc($shop['ds_rep_name'] ?? '') ?></td><th>전화</th><td><?= esc($shop['ds_tel'] ?? '') ?></td></tr>
|
||||||
|
<tr><th>주소</th><td colspan="3"><?= esc($addr) ?></td></tr>
|
||||||
|
<tr><th>결제수단</th><td><?= esc((string) ($order->so_payment_type ?? '')) ?></td><th>입금</th><td><?= $paid ? '입금완료' : '미입금' ?></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="items">
|
||||||
|
<thead>
|
||||||
|
<tr><th>품목</th><th>단가</th><th>수량</th><th>금액</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php $sumQty = 0; $sumAmt = 0; foreach (($items ?? []) as $it): ?>
|
||||||
|
<?php $q = (int) ($it->soi_qty ?? 0); $a = (int) ($it->soi_amount ?? 0); $sumQty += $q; $sumAmt += $a; ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= esc((string) ($it->soi_bag_code ?? '')) ?> <?= esc((string) ($it->soi_bag_name ?? '')) ?></td>
|
||||||
|
<td class="num"><?= $nf($it->soi_unit_price ?? 0) ?></td>
|
||||||
|
<td class="num"><?= $nf($q) ?></td>
|
||||||
|
<td class="num"><?= $nf($a) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($items)): ?>
|
||||||
|
<tr><td colspan="4" style="text-align:center;color:#999;">품목 내역이 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr><td>합계</td><td></td><td class="num"><?= $nf($sumQty) ?></td><td class="num"><?= $nf($sumAmt) ?></td></tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="paidbox <?= $paid ? 'paid' : 'unpaid' ?>"><?= $paid ? '● 입금 완료' : '미입금' ?></div>
|
||||||
|
|
||||||
|
<div class="foot">출력일: <?= date('Y-m-d H:i') ?></div>
|
||||||
|
|
||||||
|
<div class="noprint">
|
||||||
|
<button type="button" onclick="window.print()" style="padding:8px 18px;font-size:.9rem;cursor:pointer;">인쇄</button>
|
||||||
|
<button type="button" onclick="window.close()" style="padding:8px 18px;font-size:.9rem;cursor:pointer;margin-left:6px;">닫기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>window.addEventListener('load', function () { setTimeout(function () { window.print(); }, 300); });</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
writable/database/shop_order_item_packed_qty_add.sql
Normal file
2
writable/database/shop_order_item_packed_qty_add.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- 전화접수 품목별 포장량(packed) 컬럼 추가
|
||||||
|
ALTER TABLE shop_order_item ADD COLUMN soi_packed_qty INT NOT NULL DEFAULT 0 AFTER soi_qty;
|
||||||
Reference in New Issue
Block a user