diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 2c536d7..d8c2a93 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -77,6 +77,7 @@ $routes->get('bag/order/phone', 'Bag::phoneOrderCreate'); $routes->get('bag/order/phone/manage', 'Bag::phoneOrderManage'); $routes->post('bag/order/phone/manage/update', 'Bag::phoneOrderUpdate'); $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/revise/(:num)', 'Bag::orderRevise/$1'); $routes->get('bag/order/lot-seed', 'Bag::orderLotSeed'); diff --git a/app/Controllers/Bag.php b/app/Controllers/Bag.php index 08844a5..2d7ffc5 100644 --- a/app/Controllers/Bag.php +++ b/app/Controllers/Bag.php @@ -3614,11 +3614,36 @@ SQL); 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', [ 'pages' => $renderer->pages(), 'current' => $slug, 'title' => $page['title'], 'body' => $body, + 'jumps' => $jumps, ]); } @@ -7022,7 +7047,7 @@ SQL; return redirect()->to(site_url('bag/sales'))->with('error', '지자체를 선택해 주세요.'); } - // 일자별·기간별 조회 (접수일 so_order_date 기준) + // 일자별·기간별 조회 (배달일 so_delivery_date 기준) $startDate = trim((string) ($this->request->getGet('start_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; @@ -7037,10 +7062,10 @@ SQL; $builder->where('so_channel', 'phone'); } if ($isYmd($startDate)) { - $builder->where('so_order_date >=', $startDate); + $builder->where('so_delivery_date >=', $startDate); } 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(); @@ -7100,6 +7125,7 @@ SQL; 'soi_bag_name' => (string) ($item->soi_bag_name ?? ''), 'soi_unit_price' => (int) ($item->soi_unit_price ?? 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_box_count' => (int) ($item->soi_box_count ?? 0), 'soi_pack_count' => (int) ($item->soi_pack_count ?? 0), @@ -7124,7 +7150,10 @@ SQL; 'ds_addr' => $shop['addr'], 'so_order_date' => (string) ($order->so_order_date ?? ''), 'so_delivery_date' => (string) ($order->so_delivery_date ?? ''), + 'so_channel' => (string) ($order->so_channel ?? ''), '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_amount' => (int) ($order->so_total_amount ?? 0), '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)) { $qtyInput = []; } + $packedInput = $this->request->getPost('item_packed') ?? []; + if (! is_array($packedInput)) { + $packedInput = []; + } $codeSet = []; foreach ($items as $item) { @@ -7230,8 +7304,10 @@ SQL; $sheetCount = $qty % $packSheets; } + $packedQty = isset($packedInput[$itemId]) ? max(0, (int) $packedInput[$itemId]) : (int) ($item->soi_packed_qty ?? 0); $itemModel->update($itemId, [ 'soi_qty' => $qty, + 'soi_packed_qty' => $packedQty, 'soi_amount' => $amount, 'soi_box_count' => $boxCount, 'soi_pack_count' => $packCount, diff --git a/app/Models/ShopOrderItemModel.php b/app/Models/ShopOrderItemModel.php index f384e11..c0ae5eb 100644 --- a/app/Models/ShopOrderItemModel.php +++ b/app/Models/ShopOrderItemModel.php @@ -12,6 +12,6 @@ class ShopOrderItemModel extends Model protected $useTimestamps = false; protected $allowedFields = [ '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', ]; } diff --git a/app/Views/bag/manual.php b/app/Views/bag/manual.php index 11eb971..dd239b9 100644 --- a/app/Views/bag/manual.php +++ b/app/Views/bag/manual.php @@ -44,11 +44,15 @@ $searchQ = (string) (service('request')->getGet('q') ?? ''); .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-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; } @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 { - .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-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 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) ?>; + // 이 매뉴얼 페이지가 설명하는 화면 바로가기 목록 [{hint, url}, ...] + var JUMPS = = json_encode($jumps ?? [], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>; // "이 화면 설명"으로 들어온 경우: 해당 소제목으로 스크롤 + 강조. // 드로어를 껐다 다시 켜도(같은 URL이라 iframe 재로드 안 됨) 부모가 보낸 메시지로 다시 강조한다. var hlFlashTimer = null; @@ -140,6 +146,41 @@ $searchQ = (string) (service('request')->getGet('q') ?? ''); if (e.origin !== location.origin) return; 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 = ' 화면 열기'; + 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 box = document.getElementById('manualSearchResults'); diff --git a/app/Views/bag/order_phone.php b/app/Views/bag/order_phone.php index aff24ad..3ae8be2 100644 --- a/app/Views/bag/order_phone.php +++ b/app/Views/bag/order_phone.php @@ -96,69 +96,118 @@ +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); +?>
| 구분 | -품목 | -1박스(낱장/판매가) | -1팩(낱장/판매가) | -단가 | -주문수량 | -금액 | -포장(박스/팩/낱장) | -삭제 | +구분 | +봉투종류 | +Pack | +Box | +단가 | +주문내용 | +포장 | +|||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 수량 | +금액 | +수량 | +금액 | +판매수량 | +금액 | +B | +P | +E | ||||||||||||
| - | -- - | -0 / 0 | -0 / 0 | -0 | -- | 0 | -박스=0, 팩=0, 낱장=0 | -- - | -||||||||||||
| 등록된 봉투 품목이 없습니다. | ||||||||||||||||||||
| = esc($g['label']) ?> | + += esc($r['name']) ?> | += number_format($r['pack']) ?> | += number_format($packPrice) ?> | += number_format($r['box']) ?> | += number_format($boxPrice) ?> | += number_format($r['price']) ?> | ++ | 0 | +0 | +0 | +0 | +|||||||||
| 합계 | +합계 | 0 | 0 | -박스=0, 팩=0, 낱장=0 | -+ | 0 | +0 | +0 | ||||||||||||