Compare commits

...

4 Commits

Author SHA1 Message Date
taekyoungc
21e7b91871 실사/불출/lot-seed 관련 사이트 라우트를 운영 반영한다.
운영 404를 유발한 inspection-select 및 inspection-work 라우트 누락을 포함해 사이트 경로 변경분을 Routes.php에 반영한다.

Made-with: Cursor
2026-04-29 15:15:48 +09:00
taekyoungc
c708d30660 실사 저장 안정화와 메뉴 운영 정책을 일관되게 반영한다.
실사 저장값이 페이지 이동 후 원복되지 않도록 저장/조회 경로를 보강하고, 코드 범위 보정과 bis 간 동기화를 추가했다. 또한 메뉴 관리를 레벨4 이상으로 제한하고 메뉴 변경 사항을 모든 지자체에 일괄 반영하도록 동기화 로직을 도입했다.

Made-with: Cursor
2026-04-29 14:59:49 +09:00
taekyoungc
215d4d2c7c 발주 변경·입고 화면을 사이트 흐름에 맞게 반영한다.
발주 등록/변경 및 스캐너·일괄·입고현황 화면의 라우팅과 화면 구성을 운영과 동일한 최신 형태로 정리한다.

Made-with: Cursor
2026-04-23 15:53:33 +09:00
taekyoungc
6db9d119c1 발주 최신 헤드 조회 메서드 누락을 복구한다.
/bag/order/create 경로에서 발생한 undefined method(whereLatestHead) 오류를 막기 위해 BagOrderModel에 조회 스코프를 추가한다.

Made-with: Cursor
2026-04-23 15:43:19 +09:00
16 changed files with 5585 additions and 218 deletions

View File

@@ -26,8 +26,18 @@ $routes->get('bag/code-details/(:num)', 'Bag::codeDetails/$1');
// 옛 주소 호환: 세부 목록만 사이트로 이동
$routes->get('admin/code-details/(:num)', 'Admin\CodeDetail::index/$1');
$routes->get('bag/purchase-inbound', 'Bag::purchaseInbound');
$routes->get('bag/issue', 'Bag::issue');
$routes->get('bag/issue', 'Bag::issueLegacy');
$routes->get('bag/issue/cancel', 'Bag::issue');
$routes->get('bag/inventory', 'Bag::inventory');
$routes->get('bag/inventory/export', 'Bag::inventoryExport');
$routes->get('bag/inventory/inspection-select', 'Bag::inspectionSelect');
$routes->get('bag/inventory/inspection-work', 'Bag::inspectionWork');
$routes->post('bag/inventory/inspection-run', 'Bag::inspectionRun');
$routes->post('bag/inventory/inspection-select/save', 'Bag::inspectionSelectSave');
$routes->post('bag/inventory/inspection-select/confirm', 'Bag::inspectionSelectConfirm');
$routes->get('bag/inventory/inspection/(:num)', 'Bag::inspectionDetail/$1');
$routes->post('bag/inventory/inspection/(:num)/save', 'Bag::inspectionSave/$1');
$routes->post('bag/inventory/inspection/(:num)/apply', 'Bag::inspectionApply/$1');
$routes->get('bag/sales', 'Bag::sales');
$routes->get('bag/sales-stats', 'Bag::salesStats');
$routes->get('bag/flow', 'Bag::flow');
@@ -41,11 +51,24 @@ $routes->post('bag/inventory/adjust', 'Bag::inventoryAdjustStore');
$routes->get('bag/issue/create', 'Bag::issueCreate');
$routes->post('bag/issue/store', 'Bag::issueStore');
$routes->post('bag/issue/cancel/(:num)', 'Bag::issueCancel/$1');
$routes->post('bag/issue/cancel-save', 'Bag::issueCancelSave');
$routes->get('bag/order/create', 'Bag::orderCreate');
$routes->get('bag/order/change', 'Bag::orderChange');
$routes->get('bag/order/revise/(:num)', 'Bag::orderRevise/$1');
$routes->get('bag/order/lot-seed', 'Bag::orderLotSeed');
$routes->post('bag/order/lot-seed/generate', 'Bag::orderLotSeedGenerate');
$routes->post('bag/order/store', 'Bag::orderStore');
$routes->post('bag/order/cancel/(:num)', 'Bag::orderCancel/$1');
$routes->post('bag/order/delete', 'Bag::orderDeletePost');
$routes->post('bag/order/delete/(:num)', 'Bag::orderDelete/$1');
$routes->get('bag/receiving/create', 'Bag::receivingCreate');
$routes->post('bag/receiving/store', 'Bag::receivingStore');
$routes->get('bag/receiving/scanner', 'Bag::receivingScanner');
$routes->post('bag/receiving/scanner/store', 'Bag::receivingScannerStore');
$routes->get('bag/receiving/batch', 'Bag::receivingBatch');
$routes->post('bag/receiving/batch/store', 'Bag::receivingBatchStore');
$routes->get('bag/receiving/status', 'Bag::receivingStatus');
$routes->get('bag/receiving/status/export', 'Bag::receivingStatusExport');
$routes->get('bag/sale/create', 'Bag::saleCreate');
$routes->post('bag/sale/store', 'Bag::saleStore');
$routes->get('bag/shop-order/create', 'Bag::shopOrderCreate');
@@ -108,6 +131,7 @@ $routes->group('bag', ['filter' => 'adminAuth'], static function ($routes): void
$routes->get('bag-orders/export', 'Admin\BagOrder::export');
$routes->get('bag-orders', 'Admin\BagOrder::index');
$routes->get('bag-orders/create', 'Admin\BagOrder::create');
$routes->get('bag-orders/revise/(:num)', 'Admin\BagOrder::revise/$1');
$routes->post('bag-orders/store', 'Admin\BagOrder::store');
$routes->get('bag-orders/detail/(:num)', 'Admin\BagOrder::detail/$1');
$routes->post('bag-orders/cancel/(:num)', 'Admin\BagOrder::cancel/$1');

View File

@@ -50,7 +50,7 @@ class BagReceiving extends BaseController
return redirect()->to(mgmt_url('bag-receivings'))->with('error', '지자체를 선택해 주세요.');
}
$orders = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll();
$orders = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->whereLatestHead($lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll();
return $this->renderWorkPage('입고 처리', 'admin/bag_receiving/create', compact('orders'));
}

View File

@@ -23,6 +23,9 @@ class Menu extends BaseController
*/
public function index()
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
helper('admin');
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
@@ -93,6 +96,9 @@ class Menu extends BaseController
*/
public function list()
{
if ($deny = $this->denyUnlessLevel4Plus(true)) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return $this->response->setJSON(['status' => 0, 'msg' => '지자체를 선택하세요.']);
@@ -112,6 +118,9 @@ class Menu extends BaseController
*/
public function store()
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government'))
@@ -144,6 +153,7 @@ class Menu extends BaseController
if ($mmPidx > 0) {
$this->menuModel->updateCnode($mmPidx, 1);
}
$this->menuModel->syncTypeToAllLgs($mtIdx, $lgIdx);
return redirect()->back()->with('success', '메뉴가 등록되었습니다.');
}
@@ -152,6 +162,9 @@ class Menu extends BaseController
*/
public function update(int $id)
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government'))
@@ -171,6 +184,7 @@ class Menu extends BaseController
'mm_is_view' => $this->request->getPost('mm_is_view') ? 'Y' : 'N',
];
$this->menuModel->update($id, $data);
$this->menuModel->syncTypeToAllLgs((int) $row->mt_idx, $lgIdx);
return redirect()->back()->with('success', '메뉴가 수정되었습니다.');
}
@@ -179,6 +193,9 @@ class Menu extends BaseController
*/
public function delete(int $id)
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government'))
@@ -190,6 +207,7 @@ class Menu extends BaseController
}
$result = $this->menuModel->deleteSafe($id);
if ($result['ok']) {
$this->menuModel->syncTypeToAllLgs((int) $row->mt_idx, $lgIdx);
return redirect()->back()->with('success', '메뉴가 삭제되었습니다.');
}
return redirect()->back()->with('error', $result['msg']);
@@ -200,6 +218,9 @@ class Menu extends BaseController
*/
public function move()
{
if ($deny = $this->denyUnlessLevel4Plus()) {
return $deny;
}
$lgIdx = admin_effective_lg_idx();
if ($lgIdx === null) {
return redirect()->to(base_url('admin/select-local-government'))
@@ -209,7 +230,12 @@ class Menu extends BaseController
if (! is_array($ids) || empty($ids)) {
return redirect()->back()->with('error', '순서를 적용할 메뉴가 없습니다.');
}
$firstId = (int) ($ids[0] ?? 0);
$firstRow = $firstId > 0 ? $this->menuModel->find($firstId) : null;
$this->menuModel->setOrder($ids, $lgIdx);
if ($firstRow && (int) $firstRow->lg_idx === $lgIdx) {
$this->menuModel->syncTypeToAllLgs((int) $firstRow->mt_idx, $lgIdx);
}
return redirect()->back()->with('success', '순서가 적용되었습니다.');
}
@@ -266,4 +292,27 @@ class Menu extends BaseController
return (int) $types[0]->mt_idx;
}
/**
* 메뉴 관리는 레벨4 이상(슈퍼/본부 관리자)만 허용.
*
* @return \CodeIgniter\HTTP\RedirectResponse|\CodeIgniter\HTTP\ResponseInterface|null
*/
private function denyUnlessLevel4Plus(bool $json = false)
{
$level = (int) session()->get('mb_level');
if (Roles::isSuperAdminEquivalent($level)) {
return null;
}
if ($json) {
return $this->response->setJSON([
'status' => 0,
'msg' => '메뉴 관리는 레벨4 이상만 접근할 수 있습니다.',
]);
}
return redirect()->to(base_url('admin/dashboard'))
->with('error', '메뉴 관리는 레벨4 이상만 접근할 수 있습니다.');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,26 @@ class BagOrderModel extends Model
protected $primaryKey = 'bo_idx';
protected $returnType = 'object';
protected $useTimestamps = false;
/**
* 동일 발주 UUID에 대해 bo_version이 최대인 행만 (수정으로 생긴 이전 버전 행은 목록·이력에서 제외).
* DB에는 버전별 행이 그대로 남고, 조회 시에만 필터한다.
*/
public function whereLatestHead(int $lgIdx): self
{
$lg = (int) $lgIdx;
return $this->where(
"(bo_uuid, bo_version) IN (SELECT bo_uuid, MAX(bo_version) FROM {$this->table} WHERE bo_lg_idx = {$lg} GROUP BY bo_uuid)",
null,
false
);
}
protected $allowedFields = [
'bo_uuid', 'bo_version', 'bo_lg_idx', 'bo_gugun_code', 'bo_dong_code',
'bo_company_idx', 'bo_agency_idx', 'bo_fee_rate', 'bo_order_date',
'bo_bag_types', 'bo_unit_prices', 'bo_qty_boxes',
'bo_lot_no', 'bo_hash', 'bo_status', 'bo_orderer_idx',
'bo_regdate', 'bo_moddate',
];

View File

@@ -189,4 +189,67 @@ class MenuModel extends Model
}
}
/**
* 특정 메뉴 타입(mt_idx)을 source 지자체 기준으로 모든 지자체에 재배포.
* 기존 대상 지자체의 해당 타입 메뉴는 삭제 후 source 구조로 재생성한다.
*/
public function syncTypeToAllLgs(int $mtIdx, int $sourceLg): void
{
if ($mtIdx <= 0 || $sourceLg <= 0) {
return;
}
$source = $this->where('mt_idx', $mtIdx)
->where('lg_idx', $sourceLg)
->orderBy('mm_dep', 'ASC')
->orderBy('mm_num', 'ASC')
->findAll();
if (empty($source)) {
return;
}
$lgRows = $this->db->table('local_government')
->select('lg_idx')
->orderBy('lg_idx', 'ASC')
->get()
->getResultArray();
foreach ($lgRows as $lgRow) {
$destLg = (int) ($lgRow['lg_idx'] ?? 0);
if ($destLg <= 0 || $destLg === $sourceLg) {
continue;
}
$this->db->transStart();
$this->where('mt_idx', $mtIdx)
->where('lg_idx', $destLg)
->delete();
$idMap = [];
foreach ($source as $row) {
$oldId = (int) ($row->mm_idx ?? 0);
$oldP = (int) ($row->mm_pidx ?? 0);
$newPidx = 0;
if ($oldP > 0 && isset($idMap[$oldP])) {
$newPidx = (int) $idMap[$oldP];
}
$this->insert([
'mt_idx' => $mtIdx,
'lg_idx' => $destLg,
'mm_name' => (string) ($row->mm_name ?? ''),
'mm_link' => (string) ($row->mm_link ?? ''),
'mm_pidx' => $newPidx,
'mm_dep' => (int) ($row->mm_dep ?? 0),
'mm_num' => (int) ($row->mm_num ?? 0),
'mm_cnode' => (int) ($row->mm_cnode ?? 0),
'mm_level' => (string) ($row->mm_level ?? ''),
'mm_is_view' => (string) ($row->mm_is_view ?? 'Y'),
]);
$idMap[$oldId] = (int) $this->getInsertID();
}
$this->db->transComplete();
}
}
}

View File

@@ -1,83 +1,443 @@
<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">
<form action="<?= mgmt_url('bag-orders/store') ?>" method="POST" class="space-y-4">
<?php
$oldBagCodes = old('item_bag_code');
$oldQtyBoxes = old('item_qty_box');
$oldQtySheets = old('item_qty_sheet');
$oldBagCodes = is_array($oldBagCodes) ? $oldBagCodes : [];
$oldQtyBoxes = is_array($oldQtyBoxes) ? $oldQtyBoxes : [];
$oldQtySheets = is_array($oldQtySheets) ? $oldQtySheets : [];
$defaultOrderDate = old('bo_order_date', date('Y-m-d'));
$defaultOrderMonth = old('bo_order_month_ui', substr($defaultOrderDate, 0, 7));
$bagMeta = [];
foreach (($bagReferenceRows ?? []) as $row) {
$bagMeta[$row['code']] = [
'name' => $row['name'],
'orderPrice' => (float) $row['orderPrice'],
'boxPerPack' => (int) $row['boxPerPack'],
'packPerSheet' => (int) $row['packPerSheet'],
'totalPerBox' => max(1, (int) $row['totalPerBox']),
];
}
$initialSelectedItems = [];
$maxOldCount = max(count($oldBagCodes), count($oldQtySheets), count($oldQtyBoxes));
for ($i = 0; $i < $maxOldCount; $i++) {
$code = trim((string) ($oldBagCodes[$i] ?? ''));
if ($code === '' || ! isset($bagMeta[$code])) {
continue;
}
$fallbackQtyBox = (int) ($oldQtyBoxes[$i] ?? 0);
$rawQtySheet = (int) ($oldQtySheets[$i] ?? 0);
$fallbackTotalPerBox = (int) ($bagMeta[$code]['totalPerBox'] ?? 1);
if ($fallbackQtyBox <= 0 && $rawQtySheet > 0) {
$fallbackQtyBox = intdiv($rawQtySheet, max(1, $fallbackTotalPerBox));
}
$initialSelectedItems[] = [
'code' => $code,
'qtyBox' => max(0, $fallbackQtyBox),
];
}
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
?>
<form action="<?= mgmt_url('bag-orders/store') ?>" method="POST" class="mt-2 space-y-2">
<?= 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>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bo_order_date" type="date" value="<?= esc(old('bo_order_date', date('Y-m-d'))) ?>" required/>
<div class="border border-gray-300 bg-white p-2">
<div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex items-center gap-2">
<label for="bo_order_month_ui" class="font-bold text-gray-700">발주월</label>
<input id="bo_order_month_ui" name="bo_order_month_ui" type="month" value="<?= esc($defaultOrderMonth) ?>" class="border border-gray-300 rounded px-2 py-1" />
</div>
<div class="flex items-center gap-2">
<label for="bo_order_date" class="font-bold text-gray-700">발주일 <span class="text-red-500">*</span></label>
<input id="bo_order_date" name="bo_order_date" type="date" value="<?= esc($defaultOrderDate) ?>" required class="border border-gray-300 rounded px-2 py-1" />
</div>
<p class="text-blue-600 font-bold">※ 발주수량은 박스단위로 입력해 주세요. (발주일은 미래일도 선택 가능)</p>
</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-32 text-right" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', '0')) ?>"/>
<span class="text-sm 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">제작업체</label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_company_idx">
<option value="">선택</option>
<?php foreach ($companies as $cp): ?>
<option value="<?= esc($cp->cp_idx) ?>" <?= (int) old('bo_company_idx') === (int) $cp->cp_idx ? 'selected' : '' ?>>
<?= esc($cp->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">입고처</label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_agency_idx">
<option value="">선택</option>
<?php foreach ($agencies as $ag): ?>
<option value="<?= esc($ag->sa_idx) ?>" <?= (int) old('bo_agency_idx') === (int) $ag->sa_idx ? 'selected' : '' ?>>
[<?= esc($ag->sa_kind ?? '') ?>] <?= esc($ag->sa_code ?? '') ?> — <?= esc($ag->sa_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mt-4">
<label class="block text-sm font-bold text-gray-700 mb-2">발주 품목</label>
<div class="border border-gray-300 overflow-auto">
<table class="w-full data-table">
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2">
<section class="xl:col-span-5 border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 이력</div>
<div class="overflow-auto max-h-[410px]">
<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-28">발주일</th>
<th>제작업체</th>
<th>입고처</th>
<th class="w-16">상태</th>
</tr>
</thead>
<tbody>
<?php for ($i = 0; $i < 3; $i++): ?>
<?php foreach (($recentOrders ?? []) as $history): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td>
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full" name="item_bag_code[]">
<option value="">선택</option>
<?php foreach ($bagCodes as $cd): ?>
<option value="<?= esc($cd->cd_code) ?>">
<?= 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_box[]" type="number" min="0" value="0"/>
</td>
<td class="text-center"><?= esc((string) $history->bo_order_date) ?></td>
<td class="text-left pl-2"><?= esc((string) ($companyMap[(int) $history->bo_company_idx] ?? '-')) ?></td>
<td class="text-left pl-2"><?= esc((string) ($agencyMap[(int) $history->bo_agency_idx] ?? '-')) ?></td>
<td class="text-center"><?= esc((string) ($statusMap[(string) $history->bo_status] ?? $history->bo_status)) ?></td>
</tr>
<?php endfor; ?>
<?php endforeach; ?>
<?php if (empty($recentOrders)): ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">발주 이력이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<section class="xl:col-span-7 border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 Form</div>
<div class="p-2 space-y-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div class="flex items-center gap-2 text-sm">
<label for="bo_fee_rate" class="w-20 font-bold text-gray-700">수수료</label>
<input id="bo_fee_rate" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', '0')) ?>" class="border border-gray-300 rounded px-2 py-1 w-24 text-right" />
<span>%</span>
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_association_idx" class="w-20 font-bold text-gray-700">협회</label>
<select id="bo_association_idx" name="bo_association_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($associations ?? []) as $association): ?>
<option value="<?= esc((string) $association->cp_idx) ?>" <?= (int) old('bo_association_idx') === (int) $association->cp_idx ? 'selected' : '' ?>>
<?= esc((string) $association->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_company_idx" class="w-20 font-bold text-gray-700">제작업체</label>
<select id="bo_company_idx" name="bo_company_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($companies ?? []) as $company): ?>
<option value="<?= esc((string) $company->cp_idx) ?>" <?= (int) old('bo_company_idx') === (int) $company->cp_idx ? 'selected' : '' ?>>
<?= esc((string) $company->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_agency_idx" class="w-20 font-bold text-gray-700">입고처</label>
<select id="bo_agency_idx" name="bo_agency_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($agencies ?? []) as $agency): ?>
<option value="<?= esc((string) $agency->sa_idx) ?>" <?= (int) old('bo_agency_idx') === (int) $agency->sa_idx ? 'selected' : '' ?>>
[<?= esc((string) ($agency->sa_kind ?? '')) ?>] <?= esc((string) ($agency->sa_code ?? '')) ?> — <?= esc((string) $agency->sa_name) ?>
</option>
<?php endforeach; ?>
</select>
</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>
<div class="border border-gray-300 overflow-auto">
<table class="w-full data-table text-sm order-input-table" id="order-item-table">
<thead>
<tr>
<th class="w-12">번호</th>
<th class="w-16">선택</th>
<th>품명</th>
<th class="w-28">수량(BOX)</th>
<th class="w-24">단가</th>
<th class="w-24">환산수량</th>
<th class="w-28">금액</th>
</tr>
</thead>
<tbody id="selected-order-items-body"></tbody>
<tfoot>
<tr>
<th colspan="3" class="text-center">계</th>
<th class="text-right pr-2" id="sum-box-qty">0</th>
<th></th>
<th class="text-right pr-2" id="sum-sheet-qty">0</th>
<th class="text-right pr-2" id="sum-amount">0</th>
</tr>
</tfoot>
</table>
</div>
<div class="flex gap-2 pt-1">
<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('bag-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>
</div>
</section>
</div>
<section class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 등록 종류</div>
<p class="text-xs text-gray-600 px-2 py-1">아래 목록에서 봉투를 선택하면 발주 품목에 추가됩니다. (개수 제한 없음)</p>
<div class="overflow-auto">
<table class="w-full data-table text-sm order-reference-table">
<thead>
<tr>
<th class="w-12">번호</th>
<th class="w-20">선택</th>
<th>봉투 종류</th>
<th class="w-24">발주단가</th>
<th class="w-24">Box당 팩</th>
<th class="w-24">팩당 낱장</th>
<th class="w-28">1박스 총 낱장</th>
</tr>
</thead>
<tbody>
<?php foreach (($bagReferenceRows ?? []) as $idx => $row): ?>
<tr data-reference-row data-code="<?= esc((string) $row['code']) ?>" class="cursor-pointer">
<td class="text-center"><?= $idx + 1 ?></td>
<td class="text-center">
<button type="button" class="js-toggle-bag border border-gray-300 rounded px-2 py-0.5 text-xs hover:bg-gray-100" data-code="<?= esc((string) $row['code']) ?>">선택</button>
</td>
<td class="text-left pl-2"><?= esc((string) $row['name']) ?></td>
<td class="text-right pr-2"><?= number_format((float) $row['orderPrice'], 2) ?></td>
<td class="text-right pr-2"><?= number_format((int) $row['boxPerPack']) ?></td>
<td class="text-right pr-2"><?= number_format((int) $row['packPerSheet']) ?></td>
<td class="text-right pr-2"><?= number_format((int) $row['totalPerBox']) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($bagReferenceRows)): ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">표시할 봉투 기준 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</form>
<style>
.order-input-table tbody tr,
.order-reference-table tbody tr {
height: 34px;
}
.order-input-table tbody td,
.order-reference-table tbody td {
padding-top: 4px;
padding-bottom: 4px;
}
</style>
<script>
(() => {
const bagMeta = <?= json_encode($bagMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const initialSelectedItems = <?= json_encode($initialSelectedItems, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const selectedBody = document.getElementById('selected-order-items-body');
const referenceRows = Array.from(document.querySelectorAll('[data-reference-row]'));
const sumBoxQtyEl = document.getElementById('sum-box-qty');
const sumSheetQtyEl = document.getElementById('sum-sheet-qty');
const sumAmountEl = document.getElementById('sum-amount');
const monthInput = document.getElementById('bo_order_month_ui');
const orderDateInput = document.getElementById('bo_order_date');
const orderForm = document.querySelector('form[action*="bag-orders/store"]');
const selectedItems = new Map();
let activeCode = null;
const formatNumber = (value) => new Intl.NumberFormat('ko-KR').format(Number.isFinite(value) ? value : 0);
const escapeHtml = (value) => String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
const syncMonthFromDate = () => {
if (!orderDateInput || !monthInput || !orderDateInput.value) return;
monthInput.value = orderDateInput.value.substring(0, 7);
};
const syncDateFromMonth = () => {
if (!orderDateInput || !monthInput || !monthInput.value) return;
const parts = monthInput.value.split('-');
if (parts.length !== 2) return;
const year = Number(parts[0]);
const month = Number(parts[1]);
if (!Number.isFinite(year) || !Number.isFinite(month)) return;
const currentDay = orderDateInput.value ? Number(orderDateInput.value.split('-')[2]) : 1;
const lastDay = new Date(year, month, 0).getDate();
const day = String(Math.min(Math.max(currentDay, 1), lastDay)).padStart(2, '0');
orderDateInput.value = `${String(year)}-${String(month).padStart(2, '0')}-${day}`;
};
const updateReferenceSelectionUi = () => {
referenceRows.forEach((row) => {
const code = row.dataset.code || '';
const button = row.querySelector('.js-toggle-bag');
const isSelected = selectedItems.has(code);
row.classList.toggle('bg-blue-50', isSelected);
if (button) {
button.textContent = isSelected ? '선택됨' : '선택';
button.classList.toggle('bg-blue-600', isSelected);
button.classList.toggle('text-white', isSelected);
button.classList.toggle('border-blue-600', isSelected);
}
});
};
const updateTotals = () => {
let sumBoxQty = 0;
let sumSheetQty = 0;
let sumAmount = 0;
selectedBody.querySelectorAll('tr[data-item-row]').forEach((row) => {
const code = row.dataset.code || '';
const qtyInput = row.querySelector('.item-qty-box');
const qtyBox = Math.max(0, parseInt(qtyInput?.value || '0', 10));
const meta = bagMeta[code] || { orderPrice: 0, totalPerBox: 1 };
const unitPrice = Number(meta.orderPrice || 0);
const totalPerBox = Math.max(1, Number(meta.totalPerBox || 1));
const qtySheet = qtyBox * totalPerBox;
const amount = qtySheet * unitPrice;
const unitPriceEl = row.querySelector('.item-unit-price');
const qtySheetEl = row.querySelector('.item-qty-sheet');
const sheetHelpEl = row.querySelector('.item-sheet-help');
const amountEl = row.querySelector('.item-amount');
if (unitPriceEl) unitPriceEl.textContent = formatNumber(unitPrice);
if (qtySheetEl) qtySheetEl.textContent = formatNumber(qtySheet);
if (sheetHelpEl) sheetHelpEl.textContent = `낱장 ${formatNumber(qtySheet)}장`;
if (amountEl) amountEl.textContent = formatNumber(amount);
selectedItems.set(code, { qtyBox });
sumBoxQty += qtyBox;
sumSheetQty += qtySheet;
sumAmount += amount;
});
if (sumBoxQtyEl) sumBoxQtyEl.textContent = formatNumber(sumBoxQty);
if (sumSheetQtyEl) sumSheetQtyEl.textContent = formatNumber(sumSheetQty);
if (sumAmountEl) sumAmountEl.textContent = formatNumber(sumAmount);
};
const setActiveRow = (code) => {
activeCode = code || null;
selectedBody.querySelectorAll('tr[data-item-row]').forEach((row) => {
row.classList.toggle('bg-amber-50', row.dataset.code === activeCode);
});
};
const renderSelectedRows = () => {
const codes = Object.keys(bagMeta).filter((code) => selectedItems.has(code));
if (codes.length === 0) {
selectedBody.innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-4">아래 "발주 등록 종류"에서 봉투를 선택해 주세요.</td></tr>';
setActiveRow(null);
updateTotals();
updateReferenceSelectionUi();
return;
}
selectedBody.innerHTML = codes.map((code, idx) => {
const meta = bagMeta[code];
const qtyBox = Math.max(0, parseInt(String(selectedItems.get(code)?.qtyBox ?? 0), 10));
const name = meta?.name || code;
return `
<tr data-item-row data-code="${escapeHtml(code)}" class="cursor-pointer">
<td class="text-center">${idx + 1}</td>
<td class="text-center">
<button type="button" class="js-remove-selected text-xs text-red-600 hover:underline" data-code="${escapeHtml(code)}">해제</button>
</td>
<td class="text-left pl-2">
${escapeHtml(name)}
<input type="hidden" name="item_bag_code[]" value="${escapeHtml(code)}" />
</td>
<td>
<input name="item_qty_box[]" type="number" min="0" step="1" value="${qtyBox}" class="item-qty-box border border-gray-300 rounded px-2 py-1 text-sm w-full text-right leading-tight" />
<p class="text-[11px] text-gray-500 mt-1 item-sheet-help">낱장 0장</p>
</td>
<td class="text-right pr-2 item-unit-price">0</td>
<td class="text-right pr-2 item-qty-sheet">0</td>
<td class="text-right pr-2 item-amount">0</td>
</tr>
`;
}).join('');
if (!activeCode || !selectedItems.has(activeCode)) {
activeCode = codes[0];
}
setActiveRow(activeCode);
updateTotals();
updateReferenceSelectionUi();
};
const toggleSelection = (code) => {
if (!code || !bagMeta[code]) return;
if (selectedItems.has(code)) {
selectedItems.delete(code);
if (activeCode === code) activeCode = null;
} else {
selectedItems.set(code, { qtyBox: 0 });
activeCode = code;
}
renderSelectedRows();
};
initialSelectedItems.forEach((item) => {
if (!item || !item.code || !bagMeta[item.code]) return;
selectedItems.set(item.code, { qtyBox: Math.max(0, parseInt(String(item.qtyBox ?? 0), 10)) });
activeCode = item.code;
});
selectedBody.addEventListener('click', (event) => {
const removeButton = event.target.closest('.js-remove-selected');
if (removeButton) {
toggleSelection(removeButton.dataset.code || '');
return;
}
const row = event.target.closest('tr[data-item-row]');
if (!row) return;
const code = row.dataset.code || '';
setActiveRow(code);
const qtyInput = row.querySelector('.item-qty-box');
if (qtyInput) qtyInput.focus();
});
selectedBody.addEventListener('input', (event) => {
const qtyInput = event.target.closest('.item-qty-box');
if (!qtyInput) return;
const row = qtyInput.closest('tr[data-item-row]');
if (!row) return;
const code = row.dataset.code || '';
selectedItems.set(code, { qtyBox: Math.max(0, parseInt(qtyInput.value || '0', 10)) });
updateTotals();
});
referenceRows.forEach((row) => {
row.addEventListener('click', (event) => {
const button = event.target.closest('.js-toggle-bag');
if (button) {
toggleSelection(button.dataset.code || '');
return;
}
if (event.target.closest('td')) {
toggleSelection(row.dataset.code || '');
}
});
});
if (monthInput) monthInput.addEventListener('change', () => { syncDateFromMonth(); updateTotals(); });
if (orderDateInput) orderDateInput.addEventListener('change', syncMonthFromDate);
if (orderForm) {
orderForm.addEventListener('submit', (event) => {
const hasValidItem = Array.from(selectedBody.querySelectorAll('tr[data-item-row]')).some((row) => {
const qtyInput = row.querySelector('.item-qty-box');
return Math.max(0, parseInt(qtyInput?.value || '0', 10)) > 0;
});
if (!hasValidItem) {
event.preventDefault();
alert('봉투를 선택하고 수량을 1 이상 입력해 주세요.');
}
});
}
syncMonthFromDate();
renderSelectedRows();
})();
</script>

View File

@@ -1,81 +1,200 @@
<?= view('components/print_header', ['printTitle' => '발주 현황']) ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<?php
// 발주기간: native month 입력은 로케일에 따라 Jan 등 영문 표기될 수 있어 YYYY-MM select + 한글 라벨 사용
$bagOrderYmChoices = [];
$bagOrderYmCenterY = (int) date('Y');
for ($by = $bagOrderYmCenterY - 4; $by <= $bagOrderYmCenterY + 2; $by++) {
for ($bm = 1; $bm <= 12; $bm++) {
$bagOrderYmChoices[] = sprintf('%04d-%02d', $by, $bm);
}
}
foreach ([(string) ($startMonth ?? ''), (string) ($endMonth ?? '')] as $ymExtra) {
if (preg_match('/^\d{4}-\d{2}$/', $ymExtra) && ! in_array($ymExtra, $bagOrderYmChoices, true)) {
$bagOrderYmChoices[] = $ymExtra;
}
}
sort($bagOrderYmChoices);
$bagOrderYmLabel = static function (string $ym): string {
if (preg_match('/^(\d{4})-(\d{2})$/', $ym, $m)) {
return $m[1] . '년 ' . (int) $m[2] . '월';
}
return $ym;
};
?>
<?= view('components/print_header', ['printTitle' => '봉투 발주 현황', 'printShowApproval' => false]) ?>
<section class="no-print border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">발주 현황</span>
<div class="flex items-center gap-2">
<a href="<?= mgmt_url('bag-orders/export') . '?' . http_build_query(array_filter(['start_date' => $startDate ?? '', 'end_date' => $endDate ?? '', 'status' => $status ?? ''])) ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
<a href="<?= mgmt_url('bag-orders/export') . '?' . http_build_query(array_filter(['start_month' => $startMonth ?? '', 'end_month' => $endMonth ?? '', 'company_idx' => $companyIdx ?? 0, 'bag_code' => $bagCode ?? '', 'receive_type' => $receiveType ?? ''])) ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= mgmt_url('bag-orders/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">발주 등록</a>
</div>
</div>
</section>
<section class="p-2 bg-white border-b border-gray-200">
<form method="GET" action="<?= mgmt_url('bag-orders') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">발주일</label>
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<label class="text-sm text-gray-600">~</label>
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
<label class="text-sm text-gray-600">상태</label>
<select name="status" class="border border-gray-300 rounded px-2 py-1 text-sm">
<option value="">전체</option>
<option value="normal" <?= ($status ?? '') === 'normal' ? 'selected' : '' ?>>정상</option>
<option value="cancelled" <?= ($status ?? '') === 'cancelled' ? 'selected' : '' ?>>취소</option>
<option value="deleted" <?= ($status ?? '') === 'deleted' ? 'selected' : '' ?>>삭제</option>
<section class="no-print p-2 bg-white border-b border-gray-200">
<!-- GBMS 발주현황: 발주기간은 [시작] ~ [끝] 한 줄 고정, 필터 블록은 가로 나열 후 좁으면 블록 단위로만 줄바꿈 -->
<form method="GET" action="<?= mgmt_url('bag-orders') ?>" class="flex flex-wrap items-end gap-x-5 gap-y-3 w-full">
<div class="flex flex-nowrap items-center gap-2 shrink-0">
<label class="text-sm text-gray-600 whitespace-nowrap">발주 기간</label>
<select name="start_month" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11.5rem] max-w-[13rem] shrink-0">
<?php foreach ($bagOrderYmChoices as $ym): ?>
<option value="<?= esc($ym) ?>" <?= ($startMonth ?? date('Y-m')) === $ym ? 'selected' : '' ?>><?= esc($bagOrderYmLabel($ym)) ?></option>
<?php endforeach; ?>
</select>
<span class="text-sm text-gray-500 select-none">~</span>
<select name="end_month" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11.5rem] max-w-[13rem] shrink-0">
<?php foreach ($bagOrderYmChoices as $ym): ?>
<option value="<?= esc($ym) ?>" <?= ($endMonth ?? date('Y-m')) === $ym ? 'selected' : '' ?>><?= esc($bagOrderYmLabel($ym)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-nowrap items-center gap-2 shrink-0">
<label class="text-sm text-gray-600 whitespace-nowrap">제작 업체</label>
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11rem] max-w-[14rem]">
<option value="0">전 체</option>
<?php foreach (($companyOptions ?? []) as $company): ?>
<option value="<?= (int) $company->cp_idx ?>" <?= (int) ($companyIdx ?? 0) === (int) $company->cp_idx ? 'selected' : '' ?>>
<?= esc((string) $company->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-nowrap items-center gap-2 shrink-0">
<label class="text-sm text-gray-600 whitespace-nowrap">품 명</label>
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11rem] max-w-[16rem]">
<option value="">전 체</option>
<?php foreach (($bagCodeOptions ?? []) as $bag): ?>
<option value="<?= esc((string) $bag->cd_code) ?>" <?= (string) ($bagCode ?? '') === (string) $bag->cd_code ? 'selected' : '' ?>>
<?= esc((string) $bag->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-nowrap items-center gap-2 shrink-0">
<label class="text-sm text-gray-600 whitespace-nowrap">입고 구분</label>
<select name="receive_type" class="border border-gray-300 rounded px-2 py-1 text-sm w-[8.5rem]">
<option value="all" <?= ($receiveType ?? 'all') === 'all' ? 'selected' : '' ?>>전 체</option>
<option value="received" <?= ($receiveType ?? 'all') === 'received' ? 'selected' : '' ?>>입고완료</option>
<option value="pending" <?= ($receiveType ?? 'all') === 'pending' ? 'selected' : '' ?>>미입고</option>
</select>
</div>
<div class="flex flex-nowrap items-center gap-2 shrink-0 sm:ml-auto">
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= mgmt_url('bag-orders') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
<a href="<?= mgmt_url('bag-orders') ?>" class="text-sm text-gray-500 hover:underline whitespace-nowrap">초기화</a>
</div>
</form>
</section>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<div class="bag-order-print-wrap border border-gray-300 overflow-auto mt-2">
<table class="bag-order-print-table w-full data-table">
<thead>
<tr>
<th class="w-16">번호</th>
<th>LOT번호</th>
<th>발주일</th>
<th>제작업체</th>
<th>입고처</th>
<th>품목수</th>
<th>총수량</th>
<th>총금액</th>
<th class="w-20">상태</th>
<th class="w-44">작업</th>
<th class="w-32">발주일자</th>
<th class="min-w-[10rem]">제작 업체</th>
<th class="min-w-[12rem]">품 명</th>
<th class="w-28">발주 수량</th>
<th class="w-28">입고 수량</th>
<th class="w-28">미입고수량</th>
<th class="w-32">발주 금액</th>
<th class="min-w-[9rem]">입고처</th>
<th class="min-w-[8rem]">비 고</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($list as $row): ?>
<tr>
<td class="text-center"><?= esc($row->bo_idx) ?></td>
<td class="text-center font-mono"><?= esc($row->bo_lot_no) ?></td>
<td class="text-center"><?= esc($row->bo_order_date) ?></td>
<td class="text-left pl-2"><?= esc($companyMap[$row->bo_company_idx] ?? '') ?></td>
<td class="text-left pl-2"><?= esc($agencyMap[$row->bo_agency_idx] ?? '') ?></td>
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['count'] ?? 0)) ?></td>
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['qty'] ?? 0)) ?></td>
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['amount'] ?? 0)) ?></td>
<td class="text-center">
<?php $printedGroup = []; ?>
<?php foreach (($rows ?? []) as $row): ?>
<?php if (! empty($row['is_subtotal'])): ?>
<tr class="bg-gray-50 font-semibold">
<td colspan="3" class="text-center"><?= esc((string) ($row['label'] ?? '소계')) ?></td>
<td><?= number_format((int) ($row['order_qty'] ?? 0)) ?></td>
<td><?= number_format((int) ($row['received_qty'] ?? 0)) ?></td>
<td><?= number_format((int) ($row['pending_qty'] ?? 0)) ?></td>
<td><?= number_format((float) ($row['amount'] ?? 0)) ?></td>
<td></td>
<td></td>
</tr>
<?php continue; ?>
<?php endif; ?>
<?php
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
echo esc($statusMap[$row->bo_status] ?? $row->bo_status);
$boIdx = (int) ($row['bo_idx'] ?? 0);
$showGroup = ! isset($printedGroup[$boIdx]);
$rowspan = (int) (($groupRows[$boIdx] ?? 1));
if ($showGroup) {
$printedGroup[$boIdx] = true;
}
?>
</td>
<td class="text-center">
<a href="<?= mgmt_url('bag-orders/detail/' . (int) $row->bo_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">상세</a>
<form action="<?= mgmt_url('bag-orders/cancel/' . (int) $row->bo_idx) ?>" method="POST" class="inline" onsubmit="return confirm('취소하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-orange-600 hover:underline text-sm mr-1">취소</button>
</form>
<form action="<?= mgmt_url('bag-orders/delete/' . (int) $row->bo_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form>
</td>
<tr>
<?php if ($showGroup): ?>
<td class="text-center align-top" rowspan="<?= $rowspan ?>"><?= esc((string) ($row['order_date'] ?? '')) ?></td>
<td class="text-left pl-2 align-top" rowspan="<?= $rowspan ?>"><?= esc((string) ($row['company_name'] ?? '')) ?></td>
<?php endif; ?>
<td class="text-left pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
<td><?= number_format((int) ($row['order_qty'] ?? 0)) ?></td>
<td><?= number_format((int) ($row['received_qty'] ?? 0)) ?></td>
<td><?= number_format((int) ($row['pending_qty'] ?? 0)) ?></td>
<td><?= number_format((float) ($row['amount'] ?? 0)) ?></td>
<td class="text-left pl-2"><?= esc((string) ($row['agency_name'] ?? '')) ?></td>
<td></td>
</tr>
<?php endforeach; ?>
<?php if (empty($list)): ?>
<tr><td colspan="10" class="text-center text-gray-400 py-4">등록된 발주가 없습니다.</td></tr>
<?php if (empty($rows ?? [])): ?>
<tr><td colspan="9" class="text-center text-gray-400 py-6">조회 조건에 해당하는 발주 내역이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-100 font-bold">
<td colspan="3" class="text-center">총계</td>
<td class="text-right"><?= number_format((int) ($grandTotals['order_qty'] ?? 0)) ?></td>
<td class="text-right"><?= number_format((int) ($grandTotals['received_qty'] ?? 0)) ?></td>
<td class="text-right"><?= number_format((int) ($grandTotals['pending_qty'] ?? 0)) ?></td>
<td class="text-right"><?= number_format((float) ($grandTotals['amount'] ?? 0)) ?></td>
<td></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
<style>
@media print {
#debug-icon,
#debug-bar,
#debug-bar-contents,
#debug-toolbar,
.debug-toolbar,
.ci-debug-toolbar,
[id^='debug-bar-'],
[id^='debug-icon'],
[class*='debug-toolbar'] {
display: none !important;
visibility: hidden !important;
}
.bag-order-print-wrap {
overflow: visible !important;
border: none !important;
margin-top: 0 !important;
}
.bag-order-print-table {
width: 100% !important;
table-layout: auto !important;
}
.bag-order-print-table th,
.bag-order-print-table td {
white-space: nowrap !important;
word-break: keep-all !important;
overflow-wrap: normal !important;
font-size: 10px !important;
padding: 2px 3px !important;
}
}
</style>

View File

@@ -1,83 +1,701 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">발주 등록</span>
<?php
$editDefaults = is_array($editDefaults ?? null) ? $editDefaults : [];
$editMode = (bool) ($editMode ?? false);
$oldBagCodes = old('item_bag_code', $editDefaults['item_bag_code'] ?? []);
$oldQtySheets = old('item_qty_sheet', $editDefaults['item_qty_sheet'] ?? []);
$oldQtyBoxes = old('item_qty_box', $editDefaults['item_qty_box'] ?? []);
$oldBagCodes = is_array($oldBagCodes) ? $oldBagCodes : [];
$oldQtySheets = is_array($oldQtySheets) ? $oldQtySheets : [];
$oldQtyBoxes = is_array($oldQtyBoxes) ? $oldQtyBoxes : [];
$itemRowCount = max(8, count($oldBagCodes), count($oldQtySheets), count($oldQtyBoxes));
$defaultOrderDate = old('bo_order_date', $editDefaults['bo_order_date'] ?? date('Y-m-d'));
$defaultOrderMonth = old('bo_order_month_ui', substr($defaultOrderDate, 0, 7));
$defaultOrderYear = (int) substr($defaultOrderMonth, 0, 4);
$monthOptionValues = [];
for ($year = $defaultOrderYear - 2; $year <= $defaultOrderYear + 2; $year++) {
for ($month = 1; $month <= 12; $month++) {
$monthValue = sprintf('%04d-%02d', $year, $month);
$monthLabel = $year . '년 ' . $month . '월';
$monthOptionValues[] = ['value' => $monthValue, 'label' => $monthLabel];
}
}
$bagMeta = [];
foreach (($bagReferenceRows ?? []) as $row) {
$bagMeta[$row['code']] = [
'name' => $row['name'],
'orderPrice' => (float) $row['orderPrice'],
'boxPerPack' => (int) $row['boxPerPack'],
'packPerSheet' => (int) $row['packPerSheet'],
'totalPerBox' => max(1, (int) $row['totalPerBox']),
];
}
$initialSelectedItems = [];
$maxOldCount = max(count($oldBagCodes), count($oldQtySheets), count($oldQtyBoxes));
for ($i = 0; $i < $maxOldCount; $i++) {
$code = trim((string) ($oldBagCodes[$i] ?? ''));
if ($code === '' || !isset($bagMeta[$code])) {
continue;
}
$fallbackQtyBox = (int) ($oldQtyBoxes[$i] ?? 0);
$rawQtySheet = (int) ($oldQtySheets[$i] ?? 0);
$fallbackTotalPerBox = (int) ($bagMeta[$code]['totalPerBox'] ?? 1);
if ($fallbackQtyBox <= 0 && $rawQtySheet > 0) {
$fallbackQtyBox = intdiv($rawQtySheet, max(1, $fallbackTotalPerBox));
}
$initialSelectedItems[] = [
'code' => $code,
'qtyBox' => max(0, $fallbackQtyBox),
];
}
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
$hubReturn = (bool) ($hubReturn ?? false);
$changeMode = $editMode ? (string) ($changeMode ?? 'meta') : 'meta';
if (! in_array($changeMode, ['price', 'meta', 'delete'], true)) {
$changeMode = 'meta';
}
$orderLotNo = (string) ($orderLotNo ?? '');
$orderReturnMonth = (string) ($orderReturnMonth ?? '');
$deleteMode = $editMode && $changeMode === 'delete';
$defaultDeleteBoIdx = (int) ($editDefaults['bo_source_idx'] ?? 0);
$firstNormalBoIdx = null;
foreach (($recentOrders ?? []) as $_h) {
if ((string) ($_h->bo_status ?? '') === 'normal') {
$firstNormalBoIdx = (int) $_h->bo_idx;
break;
}
}
$defaultDeleteOk = false;
foreach (($recentOrders ?? []) as $_h) {
if ((int) $_h->bo_idx === $defaultDeleteBoIdx && (string) ($_h->bo_status ?? '') === 'normal') {
$defaultDeleteOk = true;
break;
}
}
$selectedDeleteBoIdx = $defaultDeleteOk ? $defaultDeleteBoIdx : (int) ($firstNormalBoIdx ?? 0);
?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel flex flex-wrap items-center justify-between gap-2">
<span class="text-sm font-bold text-gray-700"><?= $editMode ? '발주 변경' : '발주 등록' ?></span>
<?php if ($hubReturn && $orderReturnMonth !== ''): ?>
<a href="<?= base_url('bag/order/change?month=' . rawurlencode($orderReturnMonth) . '&hub_mode=' . rawurlencode($changeMode)) ?>" class="text-sm text-blue-600 hover:underline">발주 변경 목록</a>
<?php endif; ?>
</section>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-4xl">
<form action="<?= base_url('bag/order/store') ?>" method="POST" class="space-y-4">
<?php if ($deleteMode): ?>
<form action="<?= base_url('bag/order/delete') ?>" method="post" class="mt-2 space-y-2">
<?= 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>
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bo_order_date" type="date" value="<?= esc(old('bo_order_date', date('Y-m-d'))) ?>" required/>
<div class="border border-gray-300 bg-white p-2 text-sm">
<span class="font-bold text-gray-700">발주월</span>
<span class="ml-2"><?= esc($defaultOrderMonth) ?></span>
<span class="text-gray-600 ml-2">왼쪽 목록에서 삭제할 발주를 선택한 뒤 「삭제 실행」을 누르세요.</span>
</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-32 text-right" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', '0')) ?>"/>
<span class="text-sm 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">제작업체</label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_company_idx">
<option value="">선택</option>
<?php foreach ($companies as $cp): ?>
<option value="<?= esc($cp->cp_idx) ?>" <?= (int) old('bo_company_idx') === (int) $cp->cp_idx ? 'selected' : '' ?>>
<?= esc($cp->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="block text-sm font-bold text-gray-700 w-28">입고처</label>
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_agency_idx">
<option value="">선택</option>
<?php foreach ($agencies as $ag): ?>
<option value="<?= esc($ag->sa_idx) ?>" <?= (int) old('bo_agency_idx') === (int) $ag->sa_idx ? 'selected' : '' ?>>
[<?= esc($ag->sa_kind ?? '') ?>] <?= esc($ag->sa_code ?? '') ?> — <?= esc($ag->sa_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mt-4">
<label class="block text-sm font-bold text-gray-700 mb-2">발주 품목</label>
<div class="border border-gray-300 overflow-auto">
<table class="w-full data-table">
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2">
<section class="xl:col-span-5 border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 이력</div>
<div class="overflow-auto max-h-[410px]">
<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-10">선택</th>
<th class="w-28">발주일</th>
<th>제작업체</th>
<th>입고처</th>
<th class="w-16">상태</th>
</tr>
</thead>
<tbody>
<?php for ($i = 0; $i < 3; $i++): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td>
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full" name="item_bag_code[]">
<option value="">선택</option>
<?php foreach ($bagCodes as $cd): ?>
<option value="<?= esc($cd->cd_code) ?>">
<?= 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_box[]" type="number" min="0" value="0"/>
<?php $deleteRadioFirst = true; ?>
<?php foreach (($recentOrders ?? []) as $history): ?>
<?php $rowNormal = (string) ($history->bo_status ?? '') === 'normal'; ?>
<tr class="<?= $rowNormal ? '' : 'opacity-60' ?>">
<td class="text-center align-middle">
<?php if ($rowNormal): ?>
<input type="radio" name="bo_idx" value="<?= (int) $history->bo_idx ?>" class="accent-red-600" <?= (int) $history->bo_idx === $selectedDeleteBoIdx ? 'checked' : '' ?> <?= $deleteRadioFirst ? 'required' : '' ?> />
<?php $deleteRadioFirst = false; ?>
<?php else: ?>
<span class="text-gray-400" title="삭제할 수 없는 상태">—</span>
<?php endif; ?>
</td>
<td class="text-center"><?= esc((string) $history->bo_order_date) ?></td>
<td class="text-left pl-2"><?= esc((string) ($companyMap[(int) $history->bo_company_idx] ?? '-')) ?></td>
<td class="text-left pl-2"><?= esc((string) ($agencyMap[(int) $history->bo_agency_idx] ?? '-')) ?></td>
<td class="text-center"><?= esc((string) ($statusMap[(string) $history->bo_status] ?? $history->bo_status)) ?></td>
</tr>
<?php endfor; ?>
<?php endforeach; ?>
<?php if (empty($recentOrders)): ?>
<tr><td colspan="5" class="text-center text-gray-400 py-4">발주 이력이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<section class="xl:col-span-7 border border-red-200 bg-red-50 p-4">
<p class="text-sm font-bold text-red-800 mb-2">발주 삭제</p>
<p class="text-sm text-gray-700 mb-4">목록에서 선택한 발주를 삭제 처리합니다. 계속하시겠습니까?</p>
<div class="flex flex-wrap gap-2 items-center">
<button type="submit" class="bg-red-600 text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90" <?= $firstNormalBoIdx === null ? 'disabled' : '' ?>>삭제 실행</button>
<a href="<?= base_url('bag/order/change?month=' . rawurlencode($orderReturnMonth !== '' ? $orderReturnMonth : substr($defaultOrderDate, 0, 7))) ?>" class="bg-gray-200 text-gray-800 px-6 py-1.5 rounded-sm text-sm">취소</a>
</div>
</section>
</div>
</form>
<?php else: ?>
<form action="<?= base_url('bag/order/store') ?>" method="POST" class="mt-2 space-y-2" id="bag-order-store-form">
<?= csrf_field() ?>
<?php if ($editMode): ?>
<input type="hidden" name="bo_source_idx" value="<?= esc((string) ($editDefaults['bo_source_idx'] ?? 0)) ?>" />
<fieldset class="border border-gray-200 rounded px-3 py-2 mb-1 text-sm bg-white">
<legend class="text-xs font-bold text-gray-600 px-1">변경 구분</legend>
<div class="flex flex-wrap gap-x-4 gap-y-1">
<label class="inline-flex items-center gap-1 cursor-pointer">
<input type="radio" name="bo_change_mode" value="price" <?= $changeMode === 'price' ? 'checked' : '' ?> />
<span>발주·도매·판매 단가</span>
</label>
<label class="inline-flex items-center gap-1 cursor-pointer">
<input type="radio" name="bo_change_mode" value="meta" <?= $changeMode === 'meta' ? 'checked' : '' ?> />
<span>업체·수수료·협회·발주</span>
</label>
</div>
<p class="text-xs text-gray-500 mt-1">
발주 삭제는 <a class="text-blue-600 hover:underline" href="<?= base_url('bag/order/revise/' . (int) ($editDefaults['bo_source_idx'] ?? 0) . '?change_mode=delete') ?>">발주 삭제 화면</a>으로 이동합니다.
</p>
</fieldset>
<?php endif; ?>
<?php if ($hubReturn && $orderReturnMonth !== ''): ?>
<input type="hidden" name="order_return_hub" value="1" />
<input type="hidden" name="order_return_month" value="<?= esc($orderReturnMonth) ?>" />
<?php endif; ?>
<div class="border border-gray-300 bg-white p-2">
<div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex items-center gap-2">
<label for="bo_order_month_ui" class="font-bold text-gray-700">발주월</label>
<select id="bo_order_month_ui" name="bo_order_month_ui" class="border border-gray-300 rounded px-2 py-1 w-40">
<?php foreach ($monthOptionValues as $monthOption): ?>
<option value="<?= esc($monthOption['value']) ?>" <?= $monthOption['value'] === $defaultOrderMonth ? 'selected' : '' ?>>
<?= esc($monthOption['label']) ?>
</option>
<?php endforeach; ?>
</select>
<span id="bo_order_month_ko" class="text-sm text-gray-700"></span>
</div>
<p class="text-blue-600 font-bold">※ 발주수량을 박스단위로 떨어지게 입력해 주세요.</p>
</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>
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2">
<section class="xl:col-span-5 border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 이력</div>
<div class="overflow-auto max-h-[410px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-28">발주일</th>
<th>제작업체</th>
<th>입고처</th>
<th class="w-16">상태</th>
</tr>
</thead>
<tbody>
<?php foreach (($recentOrders ?? []) as $history): ?>
<tr>
<td class="text-center">
<a href="<?= base_url('bag/order/revise/' . (int) $history->bo_idx) ?>" class="text-blue-600 hover:underline">
<?= esc((string) $history->bo_order_date) ?>
</a>
</td>
<td class="text-left pl-2"><?= esc((string) ($companyMap[(int) $history->bo_company_idx] ?? '-')) ?></td>
<td class="text-left pl-2"><?= esc((string) ($agencyMap[(int) $history->bo_agency_idx] ?? '-')) ?></td>
<td class="text-center"><?= esc((string) ($statusMap[(string) $history->bo_status] ?? $history->bo_status)) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($recentOrders)): ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">발주 이력이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<section class="xl:col-span-7 border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700"><?= $editMode ? '발주 변경 수정' : '발주 Form' ?></div>
<div class="p-2 space-y-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<div class="flex items-center gap-2 text-sm">
<label for="bo_order_date" class="w-20 font-bold text-gray-700">발주일 <span class="text-red-500">*</span></label>
<input id="bo_order_date" name="bo_order_date" type="date" value="<?= esc($defaultOrderDate) ?>" required class="border border-gray-300 rounded px-2 py-1 w-full" />
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_association_idx" class="w-20 font-bold text-gray-700">협회</label>
<select id="bo_association_idx" name="bo_association_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($associations ?? []) as $association): ?>
<option value="<?= esc((string) $association->cp_idx) ?>" <?= (int) old('bo_association_idx', $editDefaults['bo_association_idx'] ?? 0) === (int) $association->cp_idx ? 'selected' : '' ?>>
<?= esc((string) $association->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_company_idx" class="w-20 font-bold text-gray-700">제작업체</label>
<select id="bo_company_idx" name="bo_company_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($companies ?? []) as $company): ?>
<option value="<?= esc((string) $company->cp_idx) ?>" <?= (int) old('bo_company_idx', $editDefaults['bo_company_idx'] ?? 0) === (int) $company->cp_idx ? 'selected' : '' ?>>
<?= esc((string) $company->cp_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex items-center gap-2 text-sm">
<label for="bo_agency_idx" class="w-20 font-bold text-gray-700">입고처</label>
<select id="bo_agency_idx" name="bo_agency_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($agencies ?? []) as $agency): ?>
<option value="<?= esc((string) $agency->sa_idx) ?>" <?= (int) old('bo_agency_idx', $editDefaults['bo_agency_idx'] ?? 0) === (int) $agency->sa_idx ? 'selected' : '' ?>>
[<?= esc((string) ($agency->sa_kind ?? '')) ?>] <?= esc((string) ($agency->sa_code ?? '')) ?> — <?= esc((string) $agency->sa_name) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="border border-gray-300 overflow-auto">
<table class="w-full data-table text-sm order-input-table" id="order-item-table">
<thead>
<tr>
<th class="w-12">번호</th>
<th class="w-16">선택</th>
<th>품명</th>
<th class="w-24">수량</th>
<th class="w-24">단가</th>
<th class="w-24">낱장환산</th>
<?php if ($editMode): ?>
<th class="w-24">선정수량</th>
<th class="w-20">LOT</th>
<?php endif; ?>
<th class="w-28">금액</th>
</tr>
</thead>
<tbody id="selected-order-items-body"></tbody>
<tfoot>
<tr>
<th colspan="3" class="text-center">계</th>
<th class="text-right pr-2" id="sum-box-qty">0</th>
<th></th>
<th class="text-right pr-2" id="sum-sheet-qty">0</th>
<?php if ($editMode): ?>
<th></th>
<th></th>
<?php endif; ?>
<th class="text-right pr-2" id="sum-amount">0</th>
</tr>
</tfoot>
</table>
</div>
<div class="border border-gray-300 bg-gray-50 p-2 text-sm">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-1">
<div class="flex items-center justify-between">
<span class="text-gray-700">품목 금액 합계</span>
<strong id="sum-item-amount">0</strong>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-700">조달수수료</span>
<span class="flex items-center gap-1">
<input id="bo_fee_rate_summary" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', $editDefaults['bo_fee_rate'] ?? '0')) ?>" class="border border-gray-300 rounded px-2 py-0.5 w-20 text-right" />
<strong>%</strong>
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-700">조달수수료액</span>
<strong id="sum-fee-amount">0</strong>
</div>
<div class="flex items-center justify-between border-t border-red-300 pt-1">
<span class="font-bold text-gray-800">품목 + 수수료 합계</span>
<strong id="sum-grand-amount" class="text-red-700">0</strong>
</div>
</div>
</div>
<div class="flex gap-2 pt-1">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition"><?= $editMode ? '변경 저장' : '발주' ?></button>
<a href="<?= base_url('bag/purchase-inbound') ?>" 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>
</div>
</section>
</div>
<section class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 등록 종류</div>
<div class="overflow-auto">
<table class="w-full data-table text-sm order-reference-table">
<thead>
<tr>
<th class="w-12">번호</th>
<th class="w-20">선택</th>
<th>봉투 종류</th>
<th class="w-24">발주단가</th>
<th class="w-24">Box당 팩</th>
<th class="w-24">팩당 낱장</th>
<th class="w-28">1박스 총 낱장</th>
</tr>
</thead>
<tbody>
<?php foreach (($bagReferenceRows ?? []) as $idx => $row): ?>
<tr data-reference-row data-code="<?= esc((string) $row['code']) ?>" class="cursor-pointer">
<td class="text-center"><?= $idx + 1 ?></td>
<td class="text-center">
<button type="button" class="js-toggle-bag border border-gray-300 rounded px-2 py-0.5 text-xs hover:bg-gray-100" data-code="<?= esc((string) $row['code']) ?>">선택</button>
</td>
<td class="text-left pl-2"><?= esc((string) $row['name']) ?></td>
<td class="text-right pr-2"><?= number_format((float) $row['orderPrice'], 2) ?></td>
<td class="text-right pr-2"><?= number_format((int) $row['boxPerPack']) ?></td>
<td class="text-right pr-2"><?= number_format((int) $row['packPerSheet']) ?></td>
<td class="text-right pr-2"><?= number_format((int) $row['totalPerBox']) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($bagReferenceRows)): ?>
<tr><td colspan="6" class="text-center text-gray-400 py-4">표시할 봉투 기준 데이터가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</form>
<?php endif; ?>
<?php if (! $deleteMode): ?>
<style>
.order-input-table tbody tr,
.order-reference-table tbody tr {
height: 34px;
}
.order-input-table tbody td,
.order-reference-table tbody td {
padding-top: 4px;
padding-bottom: 4px;
}
</style>
<script>
(() => {
const bagMeta = <?= json_encode($bagMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const initialSelectedItems = <?= json_encode($initialSelectedItems, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const selectedBody = document.getElementById('selected-order-items-body');
const referenceRows = Array.from(document.querySelectorAll('[data-reference-row]'));
const sumBoxQtyEl = document.getElementById('sum-box-qty');
const sumSheetQtyEl = document.getElementById('sum-sheet-qty');
const sumAmountEl = document.getElementById('sum-amount');
const sumItemAmountEl = document.getElementById('sum-item-amount');
const sumFeeAmountEl = document.getElementById('sum-fee-amount');
const sumGrandAmountEl = document.getElementById('sum-grand-amount');
const monthInput = document.getElementById('bo_order_month_ui');
const monthKoLabel = document.getElementById('bo_order_month_ko');
const orderDateInput = document.getElementById('bo_order_date');
const feeRateSummaryInput = document.getElementById('bo_fee_rate_summary');
const orderForm = document.querySelector('form[action*="bag/order/store"]');
const selectedItems = new Map();
let activeCode = null;
const editMode = <?= $editMode ? 'true' : 'false' ?>;
const changeMode = <?= json_encode($changeMode, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const orderLotNo = <?= json_encode($orderLotNo, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const colEmpty = editMode ? 9 : 7;
const formatNumber = (value) => new Intl.NumberFormat('ko-KR').format(Number.isFinite(value) ? value : 0);
const escapeHtml = (value) => String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
const ensureMonthOption = (monthValue) => {
if (!monthInput || !monthValue) return;
const hasOption = Array.from(monthInput.options || []).some((option) => option.value === monthValue);
if (hasOption) return;
const [year, month] = monthValue.split('-');
const label = `${Number(year)}년 ${Number(month)}월`;
const option = new Option(label, monthValue, false, false);
monthInput.add(option);
};
const syncMonthFromDate = () => {
if (!orderDateInput || !monthInput || !orderDateInput.value) return;
const monthValue = orderDateInput.value.substring(0, 7);
ensureMonthOption(monthValue);
monthInput.value = monthValue;
updateMonthKoLabel();
};
const syncDateFromMonth = () => {
if (!orderDateInput || !monthInput || !monthInput.value) return;
const parts = monthInput.value.split('-');
if (parts.length !== 2) return;
const year = Number(parts[0]);
const month = Number(parts[1]);
if (!Number.isFinite(year) || !Number.isFinite(month)) return;
const currentDay = orderDateInput.value ? Number(orderDateInput.value.split('-')[2]) : 1;
const lastDay = new Date(year, month, 0).getDate();
const day = String(Math.min(Math.max(currentDay, 1), lastDay)).padStart(2, '0');
orderDateInput.value = `${String(year)}-${String(month).padStart(2, '0')}-${day}`;
updateMonthKoLabel();
};
const updateMonthKoLabel = () => {
if (!monthKoLabel || !monthInput || !monthInput.value) {
if (monthKoLabel) monthKoLabel.textContent = '';
return;
}
const [year, month] = monthInput.value.split('-');
if (!year || !month) {
monthKoLabel.textContent = '';
return;
}
monthKoLabel.textContent = `${Number(year)}년 ${Number(month)}월`;
};
const updateReferenceSelectionUi = () => {
referenceRows.forEach((row) => {
const code = row.dataset.code || '';
const button = row.querySelector('.js-toggle-bag');
const isSelected = selectedItems.has(code);
row.classList.toggle('bg-blue-50', isSelected);
if (button) {
button.textContent = isSelected ? '선택됨' : '선택';
button.classList.toggle('bg-blue-600', isSelected);
button.classList.toggle('text-white', isSelected);
button.classList.toggle('border-blue-600', isSelected);
}
});
};
const applyModeLock = () => {
if (!editMode) return;
const isPrice = changeMode === 'price';
selectedBody.querySelectorAll('.item-qty-box').forEach((el) => {
el.readOnly = isPrice;
});
};
const updateTotals = () => {
let sumBoxQty = 0;
let sumSheetQty = 0;
let sumAmount = 0;
selectedBody.querySelectorAll('tr[data-item-row]').forEach((row) => {
const code = row.dataset.code || '';
const qtyInput = row.querySelector('.item-qty-box');
const qtyBox = Math.max(0, parseInt(qtyInput?.value || '0', 10));
const meta = bagMeta[code] || { orderPrice: 0, totalPerBox: 1 };
const priceInput = row.querySelector('.item-unit-price-input');
let unitPrice = Number(meta.orderPrice || 0);
if (priceInput) {
unitPrice = Math.max(0, parseFloat(priceInput.value || '0') || 0);
}
const totalPerBox = Math.max(1, Number(meta.totalPerBox || 1));
const qtySheet = qtyBox * totalPerBox;
const amount = qtySheet * unitPrice;
const unitPriceEl = row.querySelector('.item-unit-price');
const qtySheetEl = row.querySelector('.item-qty-sheet');
const selQtyEl = row.querySelector('.item-sel-qty');
const sheetHelpEl = row.querySelector('.item-sheet-help');
const amountEl = row.querySelector('.item-amount');
if (unitPriceEl) unitPriceEl.textContent = formatNumber(unitPrice);
if (qtySheetEl) qtySheetEl.textContent = formatNumber(qtySheet);
if (selQtyEl) selQtyEl.textContent = formatNumber(qtySheet);
if (sheetHelpEl) sheetHelpEl.textContent = `낱장 ${formatNumber(qtySheet)}장`;
if (amountEl) amountEl.textContent = formatNumber(amount);
selectedItems.set(code, { qtyBox });
sumBoxQty += qtyBox;
sumSheetQty += qtySheet;
sumAmount += amount;
});
if (sumBoxQtyEl) sumBoxQtyEl.textContent = formatNumber(sumBoxQty);
if (sumSheetQtyEl) sumSheetQtyEl.textContent = formatNumber(sumSheetQty);
if (sumAmountEl) sumAmountEl.textContent = formatNumber(sumAmount);
if (sumItemAmountEl) sumItemAmountEl.textContent = formatNumber(sumAmount);
const feeRateSource = feeRateSummaryInput?.value ?? '0';
const feeRate = Math.max(0, parseFloat(feeRateSource) || 0);
const feeAmount = Math.round(sumAmount * (feeRate / 100));
const grandAmount = sumAmount + feeAmount;
if (feeRateSummaryInput && feeRateSummaryInput.value !== feeRate.toString()) {
feeRateSummaryInput.value = feeRate.toString();
}
if (sumFeeAmountEl) sumFeeAmountEl.textContent = formatNumber(feeAmount);
if (sumGrandAmountEl) sumGrandAmountEl.textContent = formatNumber(grandAmount);
};
const setActiveRow = (code) => {
activeCode = code || null;
selectedBody.querySelectorAll('tr[data-item-row]').forEach((row) => {
row.classList.toggle('bg-amber-50', row.dataset.code === activeCode);
});
};
const renderSelectedRows = () => {
const codes = Object.keys(bagMeta).filter((code) => selectedItems.has(code));
if (codes.length === 0) {
selectedBody.innerHTML = `<tr><td colspan="${colEmpty}" class="text-center text-gray-400 py-4">아래 "발주 등록 종류"에서 봉투를 선택해 주세요.</td></tr>`;
setActiveRow(null);
updateTotals();
updateReferenceSelectionUi();
applyModeLock();
return;
}
selectedBody.innerHTML = codes.map((code, idx) => {
const meta = bagMeta[code];
const qtyBox = Math.max(0, parseInt(String(selectedItems.get(code)?.qtyBox ?? 0), 10));
const name = meta?.name || code;
const up = Number(meta.orderPrice || 0);
const priceCell = editMode && changeMode === 'price'
? `<td class="text-right pr-1"><input type="number" name="item_unit_price[]" step="0.01" min="0" class="item-unit-price-input border border-gray-300 rounded px-1 py-0.5 w-full text-sm text-right" value="${up}" /></td>`
: `<td class="text-right pr-2 item-unit-price">0</td>`;
const extraCols = editMode
? `<td class="text-right pr-2 item-sel-qty">0</td><td class="text-center text-[11px] item-lot-cell leading-tight text-gray-700">생성<br /><span class="font-mono">${escapeHtml(orderLotNo || '—')}</span></td>`
: '';
return `
<tr data-item-row data-code="${escapeHtml(code)}" class="cursor-pointer">
<td class="text-center">${idx + 1}</td>
<td class="text-center">
<button type="button" class="js-remove-selected text-xs text-red-600 hover:underline" data-code="${escapeHtml(code)}">해제</button>
</td>
<td class="text-left pl-2">
${escapeHtml(name)}
<input type="hidden" name="item_bag_code[]" value="${escapeHtml(code)}" />
</td>
<td>
<input name="item_qty_box[]" type="number" min="0" step="1" value="${qtyBox}" class="item-qty-box border border-gray-300 rounded px-2 py-1 text-sm w-full text-right leading-tight" />
<p class="text-[11px] text-gray-500 mt-1 item-sheet-help">낱장 0장</p>
</td>
${priceCell}
<td class="text-right pr-2 item-qty-sheet">0</td>
${extraCols}
<td class="text-right pr-2 item-amount">0</td>
</tr>
`;
}).join('');
if (!activeCode || !selectedItems.has(activeCode)) {
activeCode = codes[0];
}
setActiveRow(activeCode);
updateTotals();
updateReferenceSelectionUi();
applyModeLock();
};
const toggleSelection = (code) => {
if (!code || !bagMeta[code]) return;
if (selectedItems.has(code)) {
selectedItems.delete(code);
if (activeCode === code) activeCode = null;
} else {
selectedItems.set(code, { qtyBox: 0 });
activeCode = code;
}
renderSelectedRows();
};
initialSelectedItems.forEach((item) => {
if (!item || !item.code || !bagMeta[item.code]) return;
selectedItems.set(item.code, { qtyBox: Math.max(0, parseInt(String(item.qtyBox ?? 0), 10)) });
activeCode = item.code;
});
selectedBody.addEventListener('click', (event) => {
const removeButton = event.target.closest('.js-remove-selected');
if (removeButton) {
toggleSelection(removeButton.dataset.code || '');
return;
}
const row = event.target.closest('tr[data-item-row]');
if (!row) return;
const code = row.dataset.code || '';
setActiveRow(code);
const qtyInput = row.querySelector('.item-qty-box');
if (qtyInput) qtyInput.focus();
});
selectedBody.addEventListener('input', (event) => {
const qtyInput = event.target.closest('.item-qty-box');
const priceField = event.target.closest('.item-unit-price-input');
if (qtyInput) {
const row = qtyInput.closest('tr[data-item-row]');
if (!row) return;
const code = row.dataset.code || '';
selectedItems.set(code, { qtyBox: Math.max(0, parseInt(qtyInput.value || '0', 10)) });
updateTotals();
return;
}
if (priceField) {
updateTotals();
}
});
referenceRows.forEach((row) => {
row.addEventListener('click', (event) => {
const button = event.target.closest('.js-toggle-bag');
if (button) {
toggleSelection(button.dataset.code || '');
return;
}
if (event.target.closest('td')) {
toggleSelection(row.dataset.code || '');
}
});
});
if (monthInput) monthInput.addEventListener('change', () => { syncDateFromMonth(); updateTotals(); });
if (orderDateInput) orderDateInput.addEventListener('change', syncMonthFromDate);
if (feeRateSummaryInput) {
feeRateSummaryInput.addEventListener('input', () => {
updateTotals();
});
}
if (orderForm) {
orderForm.addEventListener('submit', (event) => {
const hasValidItem = Array.from(selectedBody.querySelectorAll('tr[data-item-row]')).some((row) => {
const qtyInput = row.querySelector('.item-qty-box');
return Math.max(0, parseInt(qtyInput?.value || '0', 10)) > 0;
});
if (!hasValidItem) {
event.preventDefault();
alert('봉투를 선택하고 수량을 1 이상 입력해 주세요.');
}
});
}
syncMonthFromDate();
updateMonthKoLabel();
renderSelectedRows();
if (editMode) {
document.querySelectorAll('input[name="bo_change_mode"]').forEach((r) => {
r.addEventListener('change', () => {
const u = new URL(window.location.href);
u.searchParams.set('change_mode', r.value);
window.location.href = u.toString();
});
});
}
})();
</script>
<?php endif; ?>

View File

@@ -0,0 +1,396 @@
<?php
$startDate = (string) ($startDate ?? date('Y-m-01'));
$endDate = (string) ($endDate ?? date('Y-m-d'));
$workDate = (string) ($workDate ?? date('Y-m-d'));
$itemCode = (string) ($itemCode ?? '');
$viewType = (string) ($viewType ?? 'box');
$inspectionRuns = is_array($inspectionRuns ?? null) ? $inspectionRuns : [];
$popupItems = is_array($popupItems ?? null) ? $popupItems : [];
$items = is_array($items ?? null) ? $items : [];
$overviewRows = is_array($overviewRows ?? null) ? $overviewRows : [];
$selectedInspectionItemId = (int) ($selectedInspectionItemId ?? 0);
$selectedInspectionId = (int) ($selectedInspectionId ?? 0);
$boxRows = is_array($boxRows ?? null) ? $boxRows : [];
$selectedBoxCode = (string) ($selectedBoxCode ?? '');
$selectedPackCode = (string) ($selectedPackCode ?? '');
$sheetRows = is_array($sheetRows ?? null) ? $sheetRows : [];
$overviewTotalQty = 0;
foreach ($overviewRows as $row) {
$overviewTotalQty += (int) ($row['bisi_system_qty'] ?? 0);
}
$overviewTotalActual = 0;
foreach ($overviewRows as $row) {
$overviewTotalActual += (int) ($row['bisi_actual_qty'] ?? 0);
}
$overviewTotalDiff = $overviewTotalActual - $overviewTotalQty;
$packTotalQty = 0;
foreach ($boxRows as $row) {
$packTotalQty += (int) ($row['bisp_sheet_qty'] ?? 0);
}
$packTotalActual = 0;
foreach ($boxRows as $row) {
$packTotalActual += (int) ($row['bisp_actual_qty'] ?? 0);
}
$packTotalDiff = $packTotalActual - $packTotalQty;
$sheetTotalQty = 0;
foreach ($sheetRows as $row) {
$sheetTotalQty += (int) ($row['biss_system_qty'] ?? 0);
}
$sheetTotalActual = 0;
foreach ($sheetRows as $row) {
$sheetTotalActual += (int) ($row['biss_actual_qty'] ?? 0);
}
$sheetTotalDiff = $sheetTotalActual - $sheetTotalQty;
?>
<div class="space-y-2">
<section class="border border-gray-300 bg-white p-2">
<form method="get" class="flex flex-wrap items-end justify-between gap-2 text-sm">
<div class="flex flex-wrap items-end gap-2">
<label class="font-bold text-gray-700">실사기간</label>
<input type="date" name="start_date" value="<?= esc($startDate) ?>" class="border border-gray-300 rounded px-2 py-1">
<span>~</span>
<input type="date" name="end_date" value="<?= esc($endDate) ?>" class="border border-gray-300 rounded px-2 py-1">
<label class="font-bold text-gray-700 ml-2">실사품목</label>
<select name="item_code" class="border border-gray-300 rounded px-2 py-1 min-w-[11rem]">
<option value="">전체</option>
<?php foreach ($items as $it): ?>
<?php $code = (string) ($it['bag_code'] ?? ''); ?>
<option value="<?= esc($code) ?>" <?= $itemCode === $code ? 'selected' : '' ?>>
<?= esc((string) ($it['bag_name'] ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
<label class="font-bold text-gray-700 ml-2">조회구분</label>
<select name="view_type" class="border border-gray-300 rounded px-2 py-1 min-w-[7rem]">
<option value="box" <?= $viewType === 'box' ? 'selected' : '' ?>>박스별</option>
<option value="pack" <?= $viewType === 'pack' ? 'selected' : '' ?>>팩별</option>
</select>
</div>
<div class="flex items-center gap-2">
<button type="submit" class="bg-btn-search text-white px-3 py-1 rounded-sm">조회</button>
<button type="button" id="open-inspection-popup" class="border border-blue-300 text-blue-700 px-3 py-1 rounded-sm hover:bg-blue-50">실사 선별</button>
</div>
</form>
<p class="mt-1 text-xs text-blue-700">※ 해당 박스와 팩을 클릭하면 팩과 낱장이 조회됩니다.</p>
</section>
<section class="grid grid-cols-1 xl:grid-cols-2 gap-2">
<div class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">실사 선별자</div>
<div class="overflow-auto max-h-[500px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-24">실사일자</th>
<th>종류</th>
<th class="w-40">박스</th>
<th class="w-20">전산재고</th>
<th class="w-20">실사재고</th>
</tr>
</thead>
<tbody>
<?php if ($overviewRows !== []): ?>
<?php $prevBagName = null; ?>
<?php foreach ($overviewRows as $row): ?>
<?php
$itemId = (int) ($row['bisi_idx'] ?? 0);
$bagName = (string) ($row['bisi_bag_name'] ?? '');
$showBagName = $prevBagName !== $bagName;
$prevBagName = $bagName;
$boxCode = (string) ($row['box_code'] ?? '');
$isSelected = $itemId === $selectedInspectionItemId
&& ($selectedBoxCode === '' || $selectedBoxCode === $boxCode);
$url = base_url('bag/inventory/inspection-work?' . http_build_query([
'start_date' => $startDate,
'end_date' => $endDate,
'bis_id' => $selectedInspectionId,
'item_code' => $itemCode,
'view_type' => $viewType,
'sel_item_id' => $itemId,
'sel_box_code' => $boxCode,
'sel_pack_code' => '',
]));
?>
<tr class="<?= $isSelected ? 'bg-blue-100' : 'cursor-pointer hover:bg-blue-50' ?>" onclick="window.location.href='<?= esc($url, 'attr') ?>'">
<td class="text-center"><?= esc((string) ($row['bis_work_date'] ?? '')) ?></td>
<td class="pl-2"><?= $showBagName ? esc($bagName) : '' ?></td>
<td class="text-center"><?= esc((string) ($row['box_code'] ?? '')) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($row['bisi_system_qty'] ?? 0)) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($row['bisi_actual_qty'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="5" class="text-center text-gray-400 py-4">조회 결과가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-100 font-semibold">
<td class="text-center" colspan="3">합계</td>
<td class="text-right pr-2"><?= number_format($overviewTotalQty) ?></td>
<td class="text-right pr-2"><?= number_format($overviewTotalActual) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="space-y-2">
<div class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">실사 선별 품목(선택 박스 조회)</div>
<form method="post" action="<?= base_url('bag/inventory/inspection-select/save') ?>" id="inspection-save-form">
<?= csrf_field() ?>
<input type="hidden" name="bisi_idx" value="<?= esc((string) $selectedInspectionItemId) ?>">
<input type="hidden" name="bis_id" value="<?= esc((string) $selectedInspectionId) ?>">
<input type="hidden" name="start_date" value="<?= esc($startDate) ?>">
<input type="hidden" name="end_date" value="<?= esc($endDate) ?>">
<input type="hidden" name="item_code" value="<?= esc($itemCode) ?>">
<input type="hidden" name="view_type" value="<?= esc($viewType) ?>">
<input type="hidden" name="sel_item_id" value="<?= esc((string) $selectedInspectionItemId) ?>">
<input type="hidden" name="sel_box_code" value="<?= esc($selectedBoxCode) ?>">
<input type="hidden" name="sel_pack_code" value="<?= esc($selectedPackCode) ?>">
<input type="hidden" name="pack_actual_json" value="">
<div class="overflow-auto max-h-[260px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th>팩코드</th>
<th class="w-16">포장량</th>
<th class="w-16">재고</th>
<th class="w-20">실사재고</th>
<th class="w-16">차이</th>
<th>낱장(시작)</th>
<th>낱장(끝)</th>
</tr>
</thead>
<tbody>
<?php if ($boxRows !== []): ?>
<?php foreach ($boxRows as $row): ?>
<?php
$code = (string) ($row['bisp_box_code'] ?? '');
$packCode = (string) ($row['bisp_pack_code'] ?? '');
$systemQty = (int) ($row['bisp_sheet_qty'] ?? 0);
$actualQty = isset($row['bisp_actual_qty']) ? (int) $row['bisp_actual_qty'] : $systemQty;
$url = base_url('bag/inventory/inspection-work?' . http_build_query([
'start_date' => $startDate,
'end_date' => $endDate,
'bis_id' => $selectedInspectionId,
'item_code' => $itemCode,
'view_type' => $viewType,
'sel_item_id' => $selectedInspectionItemId,
'sel_box_code' => $code,
'sel_pack_code' => $packCode,
]));
$isSelected = $selectedBoxCode === $code && $selectedPackCode === $packCode;
?>
<tr class="<?= $isSelected ? 'bg-blue-100' : 'cursor-pointer hover:bg-blue-50' ?>" onclick="window.location.href='<?= esc($url, 'attr') ?>'">
<td class="pl-2"><?= esc($packCode) ?></td>
<td class="text-center js-pack-system"><?= number_format($systemQty) ?></td>
<td class="text-right pr-2 js-pack-stock"><?= number_format($systemQty) ?></td>
<td class="text-right pr-2">
<input type="number" min="0"
value="<?= esc((string) $actualQty) ?>"
data-pack-idx="<?= esc((string) ($row['bisp_idx'] ?? 0), 'attr') ?>"
data-original-value="<?= esc((string) $systemQty, 'attr') ?>"
data-system-qty="<?= esc((string) $systemQty, 'attr') ?>"
class="border border-gray-300 rounded px-1 py-0.5 w-20 text-right"
onclick="event.stopPropagation();">
</td>
<?php $diff = (int) ($row['bisp_diff_qty'] ?? 0); ?>
<td class="text-right pr-2 js-pack-diff <?= $diff === 0 ? '' : ($diff > 0 ? 'text-blue-700' : 'text-red-700') ?>"><?= number_format($diff) ?></td>
<td class="text-center"><?= esc((string) ($row['bisp_sheet_start_code'] ?? '')) ?></td>
<td class="text-center"><?= esc((string) ($row['bisp_sheet_end_code'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="7" class="text-center text-gray-400 py-4">선택된 품목이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-100 font-semibold">
<td class="text-center" colspan="2">합계</td>
<td class="text-right pr-2"><?= number_format($packTotalQty) ?></td>
<td class="text-right pr-2"><?= number_format($packTotalActual) ?></td>
<td class="text-right pr-2 <?= $packTotalDiff === 0 ? '' : ($packTotalDiff > 0 ? 'text-blue-700' : 'text-red-700') ?>"><?= number_format($packTotalDiff) ?></td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
</div>
<div class="p-2 border-t border-gray-300 flex items-center justify-between">
<span class="text-xs text-gray-500">선택 품목의 팩 실사수량을 입력 후 저장하세요.</span>
<div class="flex items-center gap-2">
<button type="submit" class="bg-btn-search text-white px-3 py-1 rounded-sm text-sm">실사 저장</button>
</div>
</div>
</form>
</div>
<div class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">실사 선별 내용(선택 낱장 코드 조회)</div>
<div class="overflow-auto max-h-[240px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-12">No</th>
<th>낱장</th>
<th class="w-16">전산</th>
<th class="w-16">실사</th>
<th class="w-16">차이</th>
</tr>
</thead>
<tbody>
<?php if ($sheetRows !== []): ?>
<?php foreach ($sheetRows as $row): ?>
<?php
$sSystem = (int) ($row['biss_system_qty'] ?? 1);
$sActual = (int) ($row['biss_actual_qty'] ?? 0);
$sDiff = (int) ($row['biss_diff_qty'] ?? 0);
?>
<tr>
<td class="text-center"><?= esc((string) ($row['no'] ?? '')) ?></td>
<td class="pl-2"><?= esc((string) ($row['biss_sheet_code'] ?? '')) ?></td>
<td class="text-right pr-2"><?= number_format($sSystem) ?></td>
<td class="text-right pr-2"><?= number_format($sActual) ?></td>
<td class="text-right pr-2 <?= $sDiff === 0 ? '' : ($sDiff > 0 ? 'text-blue-700' : 'text-red-700') ?>"><?= number_format($sDiff) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="5" class="text-center text-gray-400 py-4">선택된 팩/박스가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-100 font-semibold">
<td class="text-center" colspan="2">합계</td>
<td class="text-right pr-2"><?= number_format($sheetTotalQty) ?></td>
<td class="text-right pr-2"><?= number_format($sheetTotalActual) ?></td>
<td class="text-right pr-2 <?= $sheetTotalDiff === 0 ? '' : ($sheetTotalDiff > 0 ? 'text-blue-700' : 'text-red-700') ?>"><?= number_format($sheetTotalDiff) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="border border-gray-300 bg-white p-2 text-sm text-gray-600">
실사 저장 시 차이수량이 즉시 장부 재고에 반영됩니다.
</div>
</div>
</section>
</div>
<div id="inspection-popup" class="fixed inset-0 bg-black/40 hidden items-center justify-center z-[999]">
<div class="bg-white border border-gray-400 w-[min(720px,95vw)] max-h-[90vh] overflow-auto p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-bold">실사 선별</h3>
<button type="button" id="close-inspection-popup" class="text-gray-600 hover:text-gray-900">닫기</button>
</div>
<form method="post" action="<?= base_url('bag/inventory/inspection-run') ?>" id="inspection-run-form" class="space-y-3">
<?= csrf_field() ?>
<div class="flex items-center gap-2">
<label class="font-bold text-gray-700 text-sm">작업 일자</label>
<input type="date" name="work_date" value="<?= esc($workDate) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm">
</div>
<p class="text-red-600 font-semibold">바코드가 없는 봉투는 실사에서 제외 됩니다.</p>
<div class="overflow-auto border border-gray-300 max-h-[55vh]">
<table class="w-full data-table text-sm">
<thead>
<tr><th>종류</th><th class="w-20">선택구분</th></tr>
</thead>
<tbody>
<?php foreach ($popupItems as $row): ?>
<?php $hasBarcode = (bool) ($row['has_barcode'] ?? false); ?>
<tr class="<?= $hasBarcode ? '' : 'bg-gray-50 text-gray-400' ?>">
<td class="pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
<td class="text-center">
<input type="checkbox" name="bag_codes[]" value="<?= esc((string) ($row['bag_code'] ?? ''), 'attr') ?>" <?= $hasBarcode ? '' : 'disabled' ?>>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="flex justify-end gap-2">
<button type="submit" class="bg-btn-search text-white px-5 py-1.5 rounded-sm text-sm">실행</button>
</div>
</form>
</div>
</div>
<script>
(() => {
const popup = document.getElementById('inspection-popup');
const openBtn = document.getElementById('open-inspection-popup');
const closeBtn = document.getElementById('close-inspection-popup');
if (openBtn && popup) {
openBtn.addEventListener('click', () => {
popup.classList.remove('hidden');
popup.classList.add('flex');
});
}
if (closeBtn && popup) {
closeBtn.addEventListener('click', () => {
popup.classList.add('hidden');
popup.classList.remove('flex');
});
}
const form = document.getElementById('inspection-run-form');
if (form) {
form.addEventListener('submit', (event) => {
if (!window.confirm('전산 선별 처리를 실행하시겠습니까?')) {
event.preventDefault();
}
});
}
const saveForm = document.getElementById('inspection-save-form');
if (saveForm) {
const formatNumber = (value) => {
const n = Number.isFinite(value) ? value : 0;
return n.toLocaleString('ko-KR');
};
const updateDiff = (input) => {
const row = input.closest('tr');
if (!row) {
return;
}
const system = parseInt(String(input.getAttribute('data-system-qty') ?? '0'), 10) || 0;
const actual = Math.max(0, parseInt(String(input.value ?? '0'), 10) || 0);
const diff = actual - system;
const diffCell = row.querySelector('.js-pack-diff');
if (!diffCell) {
return;
}
diffCell.textContent = formatNumber(diff);
diffCell.classList.remove('text-blue-700', 'text-red-700');
if (diff > 0) {
diffCell.classList.add('text-blue-700');
} else if (diff < 0) {
diffCell.classList.add('text-red-700');
}
};
const qtyInputs = saveForm.querySelectorAll('input[data-pack-idx]');
qtyInputs.forEach((input) => {
input.addEventListener('input', () => updateDiff(input));
updateDiff(input);
});
saveForm.addEventListener('submit', () => {
const payload = {};
qtyInputs.forEach((input) => {
const idx = String(input.getAttribute('data-pack-idx') ?? '').trim();
if (idx === '') {
return;
}
payload[idx] = Math.max(0, parseInt(String(input.value ?? '0'), 10) || 0);
});
const jsonInput = saveForm.querySelector('input[name="pack_actual_json"]');
if (jsonInput) {
jsonInput.value = JSON.stringify(payload);
}
});
}
})();
</script>

View File

@@ -0,0 +1,273 @@
<?php
$startDate = (string) ($startDate ?? date('Y-m-01'));
$endDate = (string) ($endDate ?? date('Y-m-d'));
$workDate = (string) ($workDate ?? date('Y-m-d'));
$itemCode = (string) ($itemCode ?? '');
$viewType = (string) ($viewType ?? 'box');
$inspectionRuns = is_array($inspectionRuns ?? null) ? $inspectionRuns : [];
$items = is_array($items ?? null) ? $items : [];
$popupItems = is_array($popupItems ?? null) ? $popupItems : [];
$overviewRows = is_array($overviewRows ?? null) ? $overviewRows : [];
$boxRows = is_array($boxRows ?? null) ? $boxRows : [];
$sheetRows = is_array($sheetRows ?? null) ? $sheetRows : [];
$selectedInspectionId = (int) ($selectedInspectionId ?? 0);
$selectedInspectionItemId = (int) ($selectedInspectionItemId ?? 0);
$selectedBoxCode = (string) ($selectedBoxCode ?? '');
$selectedPackCode = (string) ($selectedPackCode ?? '');
$overviewTotalQty = 0;
foreach ($overviewRows as $row) {
$overviewTotalQty += (int) ($row['bisi_system_qty'] ?? 0);
}
$packTotalQty = 0;
foreach ($boxRows as $row) {
$packTotalQty += (int) ($row['bisp_sheet_qty'] ?? 0);
}
$sheetTotalQty = 0;
foreach ($sheetRows as $row) {
$sheetTotalQty += (int) ($row['biss_system_qty'] ?? 0);
}
?>
<div class="space-y-2">
<section class="border border-gray-300 bg-white p-2">
<form method="get" class="flex flex-wrap items-end justify-between gap-2 text-sm">
<div class="flex flex-wrap items-end gap-2">
<label class="font-bold text-gray-700">실사기간</label>
<input type="date" name="start_date" value="<?= esc($startDate) ?>" class="border border-gray-300 rounded px-2 py-1">
<span>~</span>
<input type="date" name="end_date" value="<?= esc($endDate) ?>" class="border border-gray-300 rounded px-2 py-1">
<label class="font-bold text-gray-700 ml-2">실사품목</label>
<select name="item_code" class="border border-gray-300 rounded px-2 py-1 min-w-[11rem]">
<option value="">전체</option>
<?php foreach ($items as $it): ?>
<?php $code = (string) ($it['bag_code'] ?? ''); ?>
<option value="<?= esc($code) ?>" <?= $itemCode === $code ? 'selected' : '' ?>><?= esc((string) ($it['bag_name'] ?? '')) ?></option>
<?php endforeach; ?>
</select>
<label class="font-bold text-gray-700 ml-2">조회구분</label>
<select name="view_type" class="border border-gray-300 rounded px-2 py-1 min-w-[7rem]">
<option value="box" <?= $viewType === 'box' ? 'selected' : '' ?>>박스별</option>
<option value="pack" <?= $viewType === 'pack' ? 'selected' : '' ?>>팩별</option>
</select>
</div>
<div class="flex items-center gap-2">
<button type="submit" class="bg-btn-search text-white px-3 py-1 rounded-sm">조회</button>
<button type="button" id="open-inspection-popup" class="border border-blue-300 text-blue-700 px-3 py-1 rounded-sm hover:bg-blue-50">실사 선별</button>
</div>
</form>
<p class="mt-1 text-xs text-blue-700">※ 실사 선별 처리 결과를 조회하는 화면입니다(읽기 전용).</p>
</section>
<section class="grid grid-cols-1 xl:grid-cols-2 gap-2">
<div class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">실사 선별자</div>
<div class="overflow-auto max-h-[500px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-24">실사일자</th>
<th>종류</th>
<th class="w-40">박스</th>
<th class="w-20">전산재고</th>
</tr>
</thead>
<tbody>
<?php if ($overviewRows !== []): ?>
<?php $prevBagName = null; ?>
<?php foreach ($overviewRows as $row): ?>
<?php
$itemId = (int) ($row['bisi_idx'] ?? 0);
$bagName = (string) ($row['bisi_bag_name'] ?? '');
$showBagName = $prevBagName !== $bagName;
$prevBagName = $bagName;
$isSelected = $itemId === $selectedInspectionItemId;
$url = base_url('bag/inventory/inspection-select?' . http_build_query([
'start_date' => $startDate,
'end_date' => $endDate,
'bis_id' => $selectedInspectionId,
'item_code' => $itemCode,
'view_type' => $viewType,
'sel_item_id' => $itemId,
]));
?>
<tr class="<?= $isSelected ? 'bg-blue-100' : 'cursor-pointer hover:bg-blue-50' ?>" onclick="window.location.href='<?= esc($url, 'attr') ?>'">
<td class="text-center"><?= esc((string) ($row['bis_work_date'] ?? '')) ?></td>
<td class="pl-2"><?= $showBagName ? esc($bagName) : '' ?></td>
<td class="text-center"><?= esc((string) ($row['box_code'] ?? '')) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($row['bisi_system_qty'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">조회 결과가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-100 font-semibold">
<td class="text-center" colspan="3">합계</td>
<td class="text-right pr-2"><?= number_format($overviewTotalQty) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="space-y-2">
<div class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">실사 선별 품목(읽기 전용)</div>
<div class="overflow-auto max-h-[260px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th>팩코드</th>
<th class="w-16">포장량</th>
<th class="w-16">재고</th>
<th>낱장(시작)</th>
<th>낱장(끝)</th>
</tr>
</thead>
<tbody>
<?php if ($boxRows !== []): ?>
<?php foreach ($boxRows as $row): ?>
<?php
$code = (string) ($row['bisp_box_code'] ?? '');
$packCode = (string) ($row['bisp_pack_code'] ?? '');
$systemQty = (int) ($row['bisp_sheet_qty'] ?? 0);
$url = base_url('bag/inventory/inspection-select?' . http_build_query([
'start_date' => $startDate,
'end_date' => $endDate,
'bis_id' => $selectedInspectionId,
'item_code' => $itemCode,
'view_type' => $viewType,
'sel_item_id' => $selectedInspectionItemId,
'sel_box_code' => $code,
'sel_pack_code' => $packCode,
]));
$isSelected = $selectedBoxCode === $code && $selectedPackCode === $packCode;
?>
<tr class="<?= $isSelected ? 'bg-blue-100' : 'cursor-pointer hover:bg-blue-50' ?>" onclick="window.location.href='<?= esc($url, 'attr') ?>'">
<td class="pl-2"><?= esc($packCode) ?></td>
<td class="text-center"><?= number_format($systemQty) ?></td>
<td class="text-right pr-2"><?= number_format($systemQty) ?></td>
<td class="text-center"><?= esc((string) ($row['bisp_sheet_start_code'] ?? '')) ?></td>
<td class="text-center"><?= esc((string) ($row['bisp_sheet_end_code'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="5" class="text-center text-gray-400 py-4">선택된 품목이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-100 font-semibold">
<td class="text-center" colspan="2">합계</td>
<td class="text-right pr-2"><?= number_format($packTotalQty) ?></td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">실사 선별 내용(읽기 전용)</div>
<div class="overflow-auto max-h-[240px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-12">No</th>
<th>낱장</th>
<th class="w-16">수량</th>
</tr>
</thead>
<tbody>
<?php if ($sheetRows !== []): ?>
<?php foreach ($sheetRows as $row): ?>
<tr>
<td class="text-center"><?= esc((string) ($row['no'] ?? '')) ?></td>
<td class="pl-2"><?= esc((string) ($row['biss_sheet_code'] ?? '')) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($row['biss_system_qty'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="3" class="text-center text-gray-400 py-4">선택된 팩/박스가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-100 font-semibold">
<td class="text-center" colspan="2">합계</td>
<td class="text-right pr-2"><?= number_format($sheetTotalQty) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</section>
</div>
<div id="inspection-popup" class="fixed inset-0 bg-black/40 hidden items-center justify-center z-[999]">
<div class="bg-white border border-gray-400 w-[min(720px,95vw)] max-h-[90vh] overflow-auto p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-bold">실사 선별</h3>
<button type="button" id="close-inspection-popup" class="text-gray-600 hover:text-gray-900">닫기</button>
</div>
<form method="post" action="<?= base_url('bag/inventory/inspection-run') ?>" id="inspection-run-form" class="space-y-3">
<?= csrf_field() ?>
<div class="flex items-center gap-2">
<label class="font-bold text-gray-700 text-sm">작업 일자</label>
<input type="date" name="work_date" value="<?= esc($workDate) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm">
</div>
<p class="text-red-600 font-semibold">바코드가 없는 봉투는 실사에서 제외 됩니다.</p>
<div class="overflow-auto border border-gray-300 max-h-[55vh]">
<table class="w-full data-table text-sm">
<thead>
<tr><th>종류</th><th class="w-20">선택구분</th></tr>
</thead>
<tbody>
<?php foreach ($popupItems as $row): ?>
<?php $hasBarcode = (bool) ($row['has_barcode'] ?? false); ?>
<tr class="<?= $hasBarcode ? '' : 'bg-gray-50 text-gray-400' ?>">
<td class="pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
<td class="text-center">
<input type="checkbox" name="bag_codes[]" value="<?= esc((string) ($row['bag_code'] ?? ''), 'attr') ?>" <?= $hasBarcode ? '' : 'disabled' ?>>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="flex justify-end gap-2">
<button type="submit" class="bg-btn-search text-white px-5 py-1.5 rounded-sm text-sm">실행</button>
</div>
</form>
</div>
</div>
<script>
(() => {
const popup = document.getElementById('inspection-popup');
const openBtn = document.getElementById('open-inspection-popup');
const closeBtn = document.getElementById('close-inspection-popup');
if (openBtn && popup) {
openBtn.addEventListener('click', () => {
popup.classList.remove('hidden');
popup.classList.add('flex');
});
}
if (closeBtn && popup) {
closeBtn.addEventListener('click', () => {
popup.classList.add('hidden');
popup.classList.remove('flex');
});
}
const form = document.getElementById('inspection-run-form');
if (form) {
form.addEventListener('submit', (event) => {
if (!window.confirm('전산 선별 처리를 실행하시겠습니까?')) {
event.preventDefault();
}
});
}
})();
</script>

View File

@@ -0,0 +1,106 @@
<?php
$month = $month ?? date('Y-m');
$hubMode = in_array(($hubMode ?? 'meta'), ['price', 'meta', 'delete'], true) ? (string) ($hubMode ?? 'meta') : 'meta';
$monthOptionValues = [];
$y = (int) substr($month, 0, 4);
for ($year = $y - 2; $year <= $y + 2; $year++) {
for ($m = 1; $m <= 12; $m++) {
$monthValue = sprintf('%04d-%02d', $year, $m);
$monthOptionValues[] = ['value' => $monthValue, 'label' => $year . '년 ' . $m . '월'];
}
}
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel flex flex-wrap items-center justify-between gap-2">
<span class="text-sm font-bold text-gray-700">발주 변경</span>
<a href="<?= base_url('bag/purchase-inbound') ?>" class="text-sm text-gray-600 hover:underline">발주 입고 관리로</a>
</section>
<div class="mt-2 space-y-3">
<?php if (session()->getFlashdata('success')): ?>
<div class="border border-green-300 bg-green-50 text-green-800 px-3 py-2 text-sm"><?= esc((string) session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="border border-red-300 bg-red-50 text-red-800 px-3 py-2 text-sm"><?= esc((string) session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<form method="get" action="<?= base_url('bag/order/change') ?>" class="border border-gray-300 bg-white p-3 flex flex-wrap items-end gap-4">
<div class="flex items-center gap-2">
<label for="hub_month" class="text-sm font-bold text-gray-700">발주월</label>
<select id="hub_month" name="month" class="border border-gray-300 rounded px-2 py-1 w-44 text-sm">
<?php foreach ($monthOptionValues as $opt): ?>
<option value="<?= esc($opt['value']) ?>" <?= $opt['value'] === $month ? 'selected' : '' ?>><?= esc($opt['label']) ?></option>
<?php endforeach; ?>
</select>
</div>
<fieldset class="border border-gray-200 rounded px-3 py-2">
<legend class="text-xs font-bold text-gray-600 px-1">변경 구분</legend>
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
<label class="inline-flex items-center gap-1 cursor-pointer">
<input type="radio" name="hub_mode" value="price" <?= $hubMode === 'price' ? 'checked' : '' ?> />
<span>발주·도매·판매 단가</span>
</label>
<label class="inline-flex items-center gap-1 cursor-pointer">
<input type="radio" name="hub_mode" value="meta" <?= $hubMode === 'meta' ? 'checked' : '' ?> />
<span>업체·수수료·협회·발주</span>
</label>
<label class="inline-flex items-center gap-1 cursor-pointer">
<input type="radio" name="hub_mode" value="delete" <?= $hubMode === 'delete' ? 'checked' : '' ?> />
<span>발주 삭제</span>
</label>
</div>
</fieldset>
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">조회</button>
</form>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-2">
<section class="lg:col-span-5 border border-gray-300 bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 목록 (발주일 · 제작업체)</div>
<div class="overflow-auto max-h-[min(420px,60vh)]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-28">발주일</th>
<th>제작업체</th>
<th class="w-20">상태</th>
</tr>
</thead>
<tbody>
<?php foreach (($monthOrders ?? []) as $row): ?>
<?php
$canEdit = ((string) ($row->bo_status ?? '')) === 'normal';
$href = base_url('bag/order/revise/' . (int) $row->bo_idx . '?change_mode=' . rawurlencode($hubMode));
?>
<tr class="<?= $canEdit ? '' : 'opacity-70' ?>">
<td class="text-center">
<?php if ($canEdit): ?>
<a href="<?= esc($href) ?>" class="text-blue-600 hover:underline font-medium"><?= esc((string) ($row->bo_order_date ?? '')) ?></a>
<?php else: ?>
<span><?= esc((string) ($row->bo_order_date ?? '')) ?></span>
<?php endif; ?>
</td>
<td class="text-left pl-2"><?= esc((string) ($companyMap[(int) ($row->bo_company_idx ?? 0)] ?? '-')) ?></td>
<td class="text-center text-xs"><?= esc((string) ($statusMap[(string) ($row->bo_status ?? '')] ?? $row->bo_status)) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($monthOrders)): ?>
<tr><td colspan="3" class="text-center text-gray-400 py-6">해당 월 발주가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<section class="lg:col-span-7 border border-dashed border-gray-300 bg-gray-50 p-4 text-sm text-gray-600">
<p class="font-bold text-gray-800 mb-2">안내</p>
<ul class="list-disc pl-5 space-y-1">
<li><strong class="text-gray-900">발주 삭제</strong>를 선택하고 <strong class="text-gray-900">조회</strong>하면 발주 삭제 화면으로 이동합니다 (해당 월에 정상 발주가 있을 때).</li>
<li>그 외 변경 구분에서는 왼쪽 목록에서 <strong class="text-gray-900">발주일</strong>을 클릭하면 선택한 <strong class="text-gray-900">변경 구분</strong>으로 발주 변경 화면이 열립니다.</li>
<li><strong class="text-gray-900">발주·도매·판매 단가</strong>: 품목별 단가를 수정해 저장합니다 (발주 변경 시에만).</li>
<li><strong class="text-gray-900">업체·수수료·협회·발주</strong>: 발주일·입고처·수량 등을 수정해 저장합니다.</li>
<li><strong class="text-gray-900">발주 삭제</strong> 화면에서 목록을 선택해 삭제 처리합니다 (복구 불가에 가깝게 동작하므로 확인 후 진행하세요).</li>
</ul>
</section>
</div>
</div>

View File

@@ -58,7 +58,11 @@
<section class="mt-4">
<div class="flex items-center justify-between mb-2 border-b pb-1">
<h3 class="text-base font-bold text-gray-700">입고 현황</h3>
<a href="<?= base_url('bag/receiving/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">입고 처리</a>
<div class="flex items-center gap-2">
<a href="<?= base_url('bag/receiving/scanner') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">입고 처리</a>
<a href="<?= base_url('bag/receiving/batch') ?>" class="border border-gray-300 text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50">일괄 입고</a>
<a href="<?= base_url('bag/receiving/status') ?>" class="border border-gray-300 text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50">입고 현황 리포트</a>
</div>
</div>
<table class="data-table">
<thead><tr>

View File

@@ -0,0 +1,119 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel flex flex-wrap items-center justify-between gap-2">
<span class="text-sm font-bold text-gray-700">일괄 입고</span>
<div class="flex items-center gap-2">
<a href="<?= base_url('bag/receiving/scanner') ?>" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">스캐너 입고</a>
<a href="<?= base_url('bag/receiving/status') ?>" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">입고 현황</a>
</div>
</section>
<?php if (session()->getFlashdata('success')): ?>
<div class="mt-2 border border-green-300 bg-green-50 text-green-800 px-3 py-2 text-sm"><?= esc((string) session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="mt-2 border border-red-300 bg-red-50 text-red-800 px-3 py-2 text-sm"><?= esc((string) session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<section class="p-2 bg-white border border-gray-300 mt-2">
<form method="get" action="<?= base_url('bag/receiving/batch') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">제작 업체</label>
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1 text-sm w-56">
<option value="0">전 체</option>
<?php foreach (($companies ?? []) as $company): ?>
<option value="<?= (int) $company->cp_idx ?>" <?= (int) ($companyIdx ?? 0) === (int) $company->cp_idx ? 'selected' : '' ?>>
<?= esc((string) ($company->cp_name ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
</form>
</section>
<form action="<?= base_url('bag/receiving/batch/store') ?>" method="post" class="mt-2 space-y-2">
<?= csrf_field() ?>
<input type="hidden" name="company_idx" value="<?= (int) ($companyIdx ?? 0) ?>" />
<section class="p-2 bg-white border border-gray-300">
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2 items-end">
<div class="xl:col-span-3 flex items-center gap-2">
<label class="text-sm text-gray-600 shrink-0 w-28">인수자 (대행소)</label>
<select name="br_receiver_ref" class="border border-gray-300 rounded px-2 py-1 text-sm w-full" required>
<?php foreach (($receiverOptions ?? []) as $opt): ?>
<option value="<?= esc((string) ($opt['ref'] ?? '')) ?>" <?= (string) ($receiverRef ?? '') === (string) ($opt['ref'] ?? '') ? 'selected' : '' ?>>
<?= esc((string) ($opt['label'] ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="xl:col-span-3 flex items-center gap-2">
<label class="text-sm text-gray-600 shrink-0 w-28">인계자 (제작업체)</label>
<select name="br_sender_idx" class="border border-gray-300 rounded px-2 py-1 text-sm w-full">
<?php foreach (($senders ?? []) as $sender): ?>
<option value="<?= (int) ($sender->mg_idx ?? 0) ?>" <?= (int) ($senderIdx ?? 0) === (int) ($sender->mg_idx ?? 0) ? 'selected' : '' ?>>
<?= esc((string) ($sender->mg_name ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="xl:col-span-2 flex items-center gap-2">
<label class="text-sm text-gray-600 w-16">입고일</label>
<input type="date" name="br_receive_date" value="<?= esc((string) old('br_receive_date', date('Y-m-d'))) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm w-full" required />
</div>
<div class="xl:col-span-2">
<button type="submit" class="w-full border border-blue-600 text-blue-700 px-2 py-1 rounded-sm text-sm hover:bg-blue-50">입고 처리</button>
</div>
<div class="xl:col-span-2 text-xs text-gray-500">체크한 LOT-봉투 행의 미입고량을 전부 입고 처리합니다.</div>
</div>
</section>
<div class="border border-gray-300 overflow-auto bg-white">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-12"><input type="checkbox" id="check-all" /></th>
<th>발주일자</th>
<th>봉투종류</th>
<th>발주량(매)</th>
<th>미입고량(매)</th>
<th>제작업체</th>
<th>LOT NO</th>
<th>발주NO</th>
</tr>
</thead>
<tbody>
<?php foreach (($rows ?? []) as $row): ?>
<tr>
<td class="text-center">
<input type="checkbox" name="selected_rows[]" value="<?= esc((string) ($row['row_key'] ?? '')) ?>" />
</td>
<td class="text-center"><?= esc((string) ($row['order_date'] ?? '')) ?></td>
<td class="text-left pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
<td class="text-right"><?= number_format((int) ($row['order_qty_sheet'] ?? 0)) ?></td>
<td class="text-right text-blue-700 font-semibold"><?= number_format((int) ($row['pending_qty_sheet'] ?? 0)) ?></td>
<td class="text-left pl-2"><?= esc((string) ($row['company_name'] ?? '')) ?></td>
<td class="text-center font-mono"><?= esc((string) ($row['lot_no'] ?? '')) ?></td>
<td class="text-center"><?= esc((string) ($row['order_no'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows ?? [])): ?>
<tr><td colspan="8" class="text-center text-gray-400 py-4">일괄 입고 가능한 미입고 행이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="flex items-center gap-2">
<a href="<?= base_url('bag/purchase-inbound') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm">취소</a>
</div>
</form>
<script>
(() => {
const all = document.getElementById('check-all');
if (!all) return;
all.addEventListener('change', () => {
document.querySelectorAll('input[name="selected_rows[]"]').forEach((el) => {
el.checked = all.checked;
});
});
})();
</script>

View File

@@ -0,0 +1,153 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel flex flex-wrap items-center justify-between gap-2">
<span class="text-sm font-bold text-gray-700">발주 입고(스캐너 대체 수동입력)</span>
<div class="flex items-center gap-2">
<a href="<?= base_url('bag/receiving/batch') ?>" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">일괄 입고</a>
<a href="<?= base_url('bag/receiving/status') ?>" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">입고 현황</a>
</div>
</section>
<?php if (session()->getFlashdata('success')): ?>
<div class="mt-2 border border-green-300 bg-green-50 text-green-800 px-3 py-2 text-sm"><?= esc((string) session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="mt-2 border border-red-300 bg-red-50 text-red-800 px-3 py-2 text-sm"><?= esc((string) session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<section class="p-2 bg-white border border-gray-300 mt-2">
<form method="get" action="<?= base_url('bag/receiving/scanner') ?>" class="flex flex-wrap items-end gap-2">
<div class="flex flex-col min-w-[14rem] max-w-[22rem]">
<label class="text-xs text-gray-500 mb-1">제작업체</label>
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1.5 text-sm bg-white">
<option value="0">제작업체 선택</option>
<?php foreach (($companies ?? []) as $company): ?>
<option value="<?= (int) ($company->cp_idx ?? 0) ?>" <?= (int) ($companyIdx ?? 0) === (int) ($company->cp_idx ?? 0) ? 'selected' : '' ?>>
<?= esc((string) ($company->cp_name ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shrink-0">조회</button>
</form>
<?php if ((int) ($companyIdx ?? 0) <= 0): ?>
<p class="text-xs text-gray-600 mt-2">제작업체를 선택하면 해당 업체 발주 중 미입고 내역을 조회합니다.</p>
<?php elseif (empty($rows ?? [])): ?>
<p class="text-xs text-amber-700 mt-2">미입고 잔량이 있는 발주가 없습니다. 발주 등록 후 다시 확인해 주세요.</p>
<?php endif; ?>
</section>
<form action="<?= base_url('bag/receiving/scanner/store') ?>" method="post" class="mt-2 space-y-2">
<?= csrf_field() ?>
<input type="hidden" name="company_idx" value="<?= (int) ($companyIdx ?? 0) ?>" />
<section class="p-2 bg-white border border-gray-300">
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2 items-end">
<div class="xl:col-span-3 flex items-center gap-2">
<label class="text-sm text-gray-600 shrink-0 w-28">인수자 (대행소)</label>
<select name="br_receiver_ref" class="border border-gray-300 rounded px-2 py-1 text-sm w-full" required>
<?php foreach (($receiverOptions ?? []) as $opt): ?>
<option value="<?= esc((string) ($opt['ref'] ?? '')) ?>" <?= (string) ($receiverRef ?? '') === (string) ($opt['ref'] ?? '') ? 'selected' : '' ?>>
<?= esc((string) ($opt['label'] ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="xl:col-span-3 flex items-center gap-2">
<label class="text-sm text-gray-600 shrink-0 w-28">인계자 (제작업체)</label>
<select name="br_sender_idx" class="border border-gray-300 rounded px-2 py-1 text-sm w-full">
<?php foreach (($senders ?? []) as $sender): ?>
<option value="<?= (int) ($sender->mg_idx ?? 0) ?>" <?= (int) ($senderIdx ?? 0) === (int) ($sender->mg_idx ?? 0) ? 'selected' : '' ?>>
<?= esc((string) ($sender->mg_name ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="xl:col-span-2 flex items-center gap-2">
<label class="text-sm text-gray-600 w-16">입고일</label>
<input type="date" name="br_receive_date" value="<?= esc((string) old('br_receive_date', date('Y-m-d'))) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm w-full" required />
</div>
<div class="xl:col-span-2">
<button type="submit" class="w-full border border-blue-600 text-blue-700 px-2 py-1 rounded-sm text-sm hover:bg-blue-50">입고 처리</button>
</div>
<div class="xl:col-span-2 text-xs text-gray-500">상단에서 제작업체를 조회한 뒤, 아래에서 입고량(매)을 입력해 저장합니다.</div>
</div>
</section>
<div class="border border-gray-300 overflow-auto bg-white">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th>발주일자</th>
<th>봉투종류</th>
<th>발주량(매)</th>
<th>미입고량(매)</th>
<th>입고량(매)</th>
<th>제작업체</th>
<th>LOT NO</th>
<th>발주NO</th>
</tr>
</thead>
<tbody>
<?php foreach (($rows ?? []) as $row): ?>
<?php $k = (string) ($row['row_key'] ?? ''); ?>
<tr data-row-key="<?= esc($k) ?>" data-lot-no="<?= esc((string) ($row['lot_no'] ?? '')) ?>" data-bag-code="<?= esc((string) ($row['bag_code'] ?? '')) ?>" data-total-per-box="<?= (int) ($row['total_per_box'] ?? 1) ?>" data-pack-per-sheet="<?= (int) ($row['pack_per_sheet'] ?? 1) ?>" data-pending-original="<?= (int) ($row['pending_qty_sheet'] ?? 0) ?>">
<td class="text-center"><?= esc((string) ($row['order_date'] ?? '')) ?></td>
<td class="text-left pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
<td class="text-right"><?= number_format((int) ($row['order_qty_sheet'] ?? 0)) ?></td>
<td class="text-right pending-cell"><?= number_format((int) ($row['pending_qty_sheet'] ?? 0)) ?></td>
<td class="text-right">
<input type="number" min="0" max="<?= (int) ($row['pending_qty_sheet'] ?? 0) ?>" name="receive_qty_sheet[<?= esc($k) ?>]" value="<?= esc((string) old('receive_qty_sheet.' . $k, '0')) ?>" class="w-24 border border-gray-300 rounded px-1 py-0.5 text-sm text-right receive-input" />
</td>
<td class="text-left pl-2"><?= esc((string) ($row['company_name'] ?? '')) ?></td>
<td class="text-center font-mono"><?= esc((string) ($row['lot_no'] ?? '')) ?></td>
<td class="text-center"><?= esc((string) ($row['order_no'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows ?? [])): ?>
<tr><td colspan="8" class="text-center text-gray-400 py-4"><?php
if ((int) ($companyIdx ?? 0) <= 0) {
echo '제작업체를 선택하고 조회해 주세요.';
} else {
echo '해당 제작업체의 미입고 발주 내역이 없습니다.';
}
?></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="flex items-center gap-2">
<a href="<?= base_url('bag/purchase-inbound') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm">취소</a>
</div>
</form>
<script>
(() => {
const parseIntSafe = (v) => {
const n = Number(String(v ?? '').replace(/,/g, ''));
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
};
const refreshPending = (row) => {
const pendingCell = row.querySelector('.pending-cell');
const input = row.querySelector('.receive-input');
if (!pendingCell || !input) return;
const original = parseIntSafe(row.getAttribute('data-pending-original'));
const current = parseIntSafe(input.value);
const remain = Math.max(0, original - current);
pendingCell.textContent = Number(remain || 0).toLocaleString('ko-KR');
};
document.querySelectorAll('.receive-input').forEach((input) => {
input.addEventListener('input', (e) => {
const row = e.target.closest('tr');
if (!row) return;
const original = parseIntSafe(row.getAttribute('data-pending-original'));
const current = Math.min(parseIntSafe(input.value), original);
input.value = String(current);
refreshPending(row);
});
const row = input.closest('tr');
if (row) refreshPending(row);
});
})();
</script>

View File

@@ -0,0 +1,138 @@
<?= view('components/print_header', ['printTitle' => '봉투 입고 현황', 'printShowApproval' => false]) ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">입고 현황</span>
<div class="flex items-center gap-2">
<a href="<?= base_url('bag/receiving/status/export') . '?' . http_build_query(array_filter(['start_date' => $startDate ?? '', 'end_date' => $endDate ?? '', 'company_idx' => $companyIdx ?? 0, 'bag_code' => $bagCode ?? '', 'receive_type' => $receiveType ?? ''])) ?>" class="border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50">엑셀저장</a>
<button onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">인쇄</button>
<a href="<?= base_url('bag/receiving/scanner') ?>" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">입고 처리</a>
</div>
</div>
</section>
<section class="p-2 bg-white border-b border-gray-200 no-print">
<form method="get" action="<?= base_url('bag/receiving/status') ?>" class="flex flex-wrap items-center gap-2">
<label class="text-sm text-gray-600">입고 기간</label>
<input type="date" name="start_date" value="<?= esc((string) ($startDate ?? '')) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm" />
<span class="text-sm text-gray-500">~</span>
<input type="date" name="end_date" value="<?= esc((string) ($endDate ?? '')) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm" />
<label class="text-sm text-gray-600 ml-2">제작 업체</label>
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1 text-sm w-52">
<option value="0">전 체</option>
<?php foreach (($companies ?? []) as $company): ?>
<option value="<?= (int) $company->cp_idx ?>" <?= (int) ($companyIdx ?? 0) === (int) $company->cp_idx ? 'selected' : '' ?>>
<?= esc((string) ($company->cp_name ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
<label class="text-sm text-gray-600">품명</label>
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 text-sm w-44">
<option value="">전 체</option>
<?php foreach (($bagCodeOptions ?? []) as $bag): ?>
<option value="<?= esc((string) ($bag->cd_code ?? '')) ?>" <?= (string) ($bagCode ?? '') === (string) ($bag->cd_code ?? '') ? 'selected' : '' ?>>
<?= esc((string) ($bag->cd_name ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
<label class="text-sm text-gray-600">입고 구분</label>
<select name="receive_type" class="border border-gray-300 rounded px-2 py-1 text-sm w-36">
<option value="all" <?= ($receiveType ?? 'all') === 'all' ? 'selected' : '' ?>>전 체</option>
<option value="completed" <?= ($receiveType ?? 'all') === 'completed' ? 'selected' : '' ?>>완료</option>
<option value="pending" <?= ($receiveType ?? 'all') === 'pending' ? 'selected' : '' ?>>미완료</option>
</select>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
<a href="<?= base_url('bag/receiving/status') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
</form>
</section>
<div class="border border-gray-300 overflow-auto mt-2 receiving-status-print-wrap">
<table class="w-full data-table text-sm receiving-status-print-table">
<thead>
<tr>
<th>입고일자</th>
<th>품명</th>
<th>입고수량</th>
<th>발주일자</th>
<th>발주수량</th>
<th>발주번호</th>
<th>제작업체</th>
<th>입고여부</th>
<th>입고처</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<?php
$prevDate = null;
$runningSum = 0;
?>
<?php foreach (($rows ?? []) as $i => $row): ?>
<?php
$d = (string) ($row['display_date'] ?? '');
if ($prevDate !== null && $d !== $prevDate):
?>
<tr class="bg-gray-50 font-semibold">
<td colspan="2" class="text-center">소 계</td>
<td class="text-right"><?= number_format($runningSum) ?></td>
<td colspan="7"></td>
</tr>
<?php
$runningSum = 0;
endif;
$runningSum += (int) ($row['received_qty_sheet'] ?? 0);
$prevDate = $d;
?>
<tr>
<td class="text-center"><?= esc($d) ?></td>
<td class="text-left pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
<td class="text-right"><?= number_format((int) ($row['received_qty_sheet'] ?? 0)) ?></td>
<td class="text-center"><?= esc((string) ($row['order_date'] ?? '')) ?></td>
<td class="text-right"><?= number_format((int) ($row['order_qty_sheet'] ?? 0)) ?></td>
<td class="text-center"><?= esc((string) ($row['order_no'] ?? '')) ?></td>
<td class="text-left pl-2"><?= esc((string) ($row['company_name'] ?? '')) ?></td>
<td class="text-center <?= ($row['receive_status_label'] ?? '') === '완료' ? 'text-red-600 font-bold' : 'text-blue-600 font-bold' ?>"><?= esc((string) ($row['receive_status_label'] ?? '')) ?></td>
<td class="text-left pl-2"><?= esc((string) ($row['agency_name'] ?? '')) ?></td>
<td></td>
</tr>
<?php endforeach; ?>
<?php if (! empty($rows ?? [])): ?>
<tr class="bg-gray-50 font-semibold">
<td colspan="2" class="text-center">소 계</td>
<td class="text-right"><?= number_format($runningSum) ?></td>
<td colspan="7"></td>
</tr>
<?php endif; ?>
<?php if (empty($rows ?? [])): ?>
<tr><td colspan="10" class="text-center text-gray-400 py-4">조회 결과가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-100 font-bold">
<td colspan="2" class="text-center">합 계</td>
<td class="text-right"><?= number_format((int) ($grandTotalReceive ?? 0)) ?></td>
<td colspan="7"></td>
</tr>
</tfoot>
</table>
</div>
<style>
@media print {
.receiving-status-print-wrap {
overflow: visible !important;
border: none !important;
}
.receiving-status-print-table th,
.receiving-status-print-table td {
white-space: nowrap !important;
font-size: 10px !important;
padding: 2px 3px !important;
}
}
</style>