Files
jongryangje/app/Views/bag/create_bag_order.php
taekyoungc 215d87f991 feat: 워크스페이스 탭 자동복원 + 주문 입금확인 토글 + 메뉴 개명(전화접수→주문접수)
종량제3 세션 작업.
- 워크스페이스 탭 계정별 저장·자동복원(member_workspace_tabs 자동생성, pref/workspace-tabs)
- 주문접수 관리 입금확인(so_paid) 수동 토글(phoneOrderMarkPaid, order/phone/manage/paid)
- 메뉴 '전화 접수 관리' → '주문접수관리' 개명(피드백 #35) + 시드/개명 SQL

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 08:04:45 +09:00

852 lines
43 KiB
PHP

<?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>
<?php if ($deleteMode): ?>
<form action="<?= base_url('bag/order/delete') ?>" method="post" class="mt-2 space-y-2">
<?= csrf_field() ?>
<div class="border border-gray-300 rounded-lg 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="grid grid-cols-1 xl:grid-cols-12 gap-2">
<section class="xl:col-span-5 border border-gray-300 rounded-lg overflow-hidden 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-10 text-center">선택</th>
<th class="w-28 text-center">발주일</th>
<th class="text-left">제작업체</th>
<th class="text-left">입고처</th>
<th class="w-16 text-center">상태</th>
</tr>
</thead>
<tbody>
<?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 js-delete-pick" <?= (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 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-3">아래 <b>삭제 대상 발주 내역</b>을 확인한 뒤 「삭제 실행」을 누르세요.</p>
<div id="delete-detail" class="bg-white border border-red-200 rounded p-3 text-sm mb-4 max-h-[320px] overflow-auto">삭제할 발주를 선택하세요.</div>
<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>
<script>
(() => {
const detailBox = document.getElementById('delete-detail');
if (!detailBox) return;
const detailBase = new URL(<?= json_encode(base_url('bag/order/detail-json'), JSON_UNESCAPED_SLASHES) ?>, window.location.href).pathname;
const nf = (n) => new Intl.NumberFormat('ko-KR').format(Number(n) || 0);
const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
const load = (id) => {
if (!id) { detailBox.innerHTML = '삭제할 발주를 선택하세요.'; return; }
detailBox.innerHTML = '불러오는 중…';
fetch(detailBase + '/' + id, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then((r) => r.json())
.then((d) => {
if (!d || !d.ok) { detailBox.innerHTML = '<span class="text-red-600">' + esc((d && d.error) || '불러오지 못했습니다.') + '</span>'; return; }
const rows = (d.items || []).map((it, i) => `<tr>
<td class="text-center">${i + 1}</td>
<td class="text-left pl-2">${esc(it.bag_name || it.bag_code)}</td>
<td class="text-right pr-2">${nf(it.qty_box)}</td>
<td class="text-right pr-2">${nf(it.qty_sheet)}</td>
<td class="text-right pr-2">${nf(it.amount)}</td></tr>`).join('');
detailBox.innerHTML = `
<dl class="grid grid-cols-[6rem_1fr] gap-y-0.5 mb-2">
<dt class="font-semibold text-gray-600">발주번호</dt><dd class="font-mono">${esc(d.bo_lot_no)} <span class="text-gray-400">(v${d.bo_version})</span></dd>
<dt class="font-semibold text-gray-600">발주일</dt><dd>${esc(d.order_date)}</dd>
<dt class="font-semibold text-gray-600">제작업체</dt><dd>${esc(d.company || '-')}</dd>
<dt class="font-semibold text-gray-600">입고처</dt><dd>${esc(d.agency || '-')}</dd>
</dl>
<table class="w-full data-table text-sm">
<thead><tr><th class="w-8 text-center">번호</th><th class="text-left">품명</th><th class="text-right">박스수량</th><th class="text-right">낱장</th><th class="text-right">금액</th></tr></thead>
<tbody>${rows || '<tr><td colspan="5" class="text-center text-gray-400 py-3">품목이 없습니다.</td></tr>'}</tbody>
</table>`;
})
.catch(() => { detailBox.innerHTML = '<span class="text-red-600">오류가 발생했습니다.</span>'; });
};
document.querySelectorAll('.js-delete-pick').forEach((r) => {
r.addEventListener('change', () => { if (r.checked) load(r.value); });
});
const checked = document.querySelector('.js-delete-pick:checked');
if (checked) load(checked.value);
})();
</script>
<?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)) ?>" />
<input type="hidden" name="bo_change_mode" value="meta" />
<div class="border border-amber-300 rounded px-3 py-2 mb-1 text-sm bg-amber-50 flex flex-wrap items-center gap-x-3 gap-y-1">
<span class="font-bold text-gray-700">원 발주번호(LOT)</span>
<span class="font-mono text-gray-900"><?= esc($orderLotNo !== '' ? $orderLotNo : '-') ?></span>
<span class="text-gray-500">발주 #<?= (int) ($editDefaults['bo_source_idx'] ?? 0) ?></span>
<span class="text-gray-500">— 변경 저장 시 원 발주번호는 유지되고, 이전 내용은 이력(버전)으로 보존됩니다.</span>
<a class="text-red-600 hover:underline ml-auto" href="<?= base_url('bag/order/revise/' . (int) ($editDefaults['bo_source_idx'] ?? 0) . '?change_mode=delete') ?>">발주 삭제</a>
</div>
<?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 rounded-lg 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="grid grid-cols-1 xl:grid-cols-12 gap-2 items-start">
<section class="xl:col-span-7 border border-gray-300 rounded-lg overflow-hidden bg-white">
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700"><?= $editMode ? '발주 변경 수정' : '발주등록' ?></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_manager_idx" class="w-20 font-bold text-gray-700">발주담당</label>
<select id="bo_manager_idx" name="bo_manager_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
<option value="">선택</option>
<?php foreach (($managers ?? []) as $manager): ?>
<option value="<?= esc((string) $manager->mg_idx) ?>" <?= (int) old('bo_manager_idx', $editDefaults['bo_manager_idx'] ?? 0) === (int) $manager->mg_idx ? 'selected' : '' ?>>
<?= esc((string) $manager->mg_name) ?><?= ($manager->mg_affiliation ?? '') !== '' ? ' (' . esc((string) $manager->mg_affiliation) . ')' : '' ?>
</option>
<?php endforeach; ?>
</select>
</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 rounded-lg p-4 overflow-auto">
<table class="w-full data-table text-sm order-input-table" id="order-item-table">
<thead>
<tr>
<th class="w-12 text-center">번호</th>
<th class="w-16 text-center">선택</th>
<th class="text-left">품명</th>
<th class="w-24 text-right">박스수량</th>
<th class="w-24 text-right">단가</th>
<th class="w-24 text-right">낱장환산</th>
<?php if ($editMode): ?>
<th class="w-24 text-right">선정수량</th>
<th class="w-20 text-center">LOT</th>
<?php endif; ?>
<th class="w-28 text-right">금액</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 rounded-lg 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>
<?php if ($editMode): ?>
<a href="<?= base_url('bag/order/create') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
<?php endif; ?>
</div>
</div>
</section>
<section class="xl:col-span-5 border border-gray-300 rounded-lg overflow-hidden 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-[560px]">
<table class="w-full data-table text-sm order-reference-table">
<thead>
<tr>
<th class="w-12 text-center">번호</th>
<th class="w-20 text-center">선택</th>
<th class="text-left">봉투 종류</th>
<th class="w-24 text-right">발주단가</th>
<th class="w-24 text-right">Box당 팩</th>
<th class="w-24 text-right">팩당 낱장</th>
<th class="w-28 text-right">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>
</div>
<section class="border border-gray-300 rounded-lg overflow-hidden 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 text-center">발주일</th>
<th class="text-left">제작업체</th>
<th class="text-left">입고처</th>
<th class="w-16 text-center">상태</th>
</tr>
</thead>
<tbody>
<?php foreach (($recentOrders ?? []) as $history): ?>
<tr>
<td class="text-center">
<button type="button" class="js-order-view text-blue-600 hover:underline font-medium" data-order-id="<?= (int) $history->bo_idx ?>" title="발주 내역 보기">
<?= esc((string) $history->bo_order_date) ?>
</button>
</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>
</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>
<!-- 발주 내역 보기 모달 -->
<div id="order-view-modal" class="hidden fixed inset-0 z-50 items-center justify-center" style="background:rgba(0,0,0,.4)">
<div class="bg-white rounded-lg shadow-xl w-[680px] max-w-[95vw] max-h-[85vh] overflow-auto">
<div class="flex items-center justify-between border-b px-4 py-2">
<span class="font-bold text-gray-800">발주 내역</span>
<button type="button" id="order-view-close" class="text-gray-500 hover:text-gray-800 text-lg leading-none">✕</button>
</div>
<div class="p-4 text-sm" id="order-view-body">불러오는 중…</div>
<div class="flex items-center justify-end gap-2 border-t px-4 py-2">
<form id="order-view-delete-form" action="<?= base_url('bag/order/delete') ?>" method="post" class="mr-auto">
<?= csrf_field() ?>
<input type="hidden" name="bo_idx" id="order-view-delete-id" value="" />
<input type="hidden" name="return_to" value="create" />
<button type="submit" id="order-view-delete" class="hidden bg-red-600 text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">삭제</button>
</form>
<a id="order-view-edit" href="#" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">발주변경</a>
<button type="button" id="order-view-close2" class="bg-gray-200 text-gray-700 px-4 py-1.5 rounded-sm text-sm hover:bg-gray-300">닫기</button>
</div>
</div>
</div>
<script>
(() => {
const modal = document.getElementById('order-view-modal');
if (!modal) return;
const body = document.getElementById('order-view-body');
const editLink = document.getElementById('order-view-edit');
const deleteBtn = document.getElementById('order-view-delete');
const deleteIdInput = document.getElementById('order-view-delete-id');
const deleteForm = document.getElementById('order-view-delete-form');
const reviseBase = <?= json_encode(base_url('bag/order/revise'), JSON_UNESCAPED_SLASHES) ?>;
const detailBase = new URL(<?= json_encode(base_url('bag/order/detail-json'), JSON_UNESCAPED_SLASHES) ?>, window.location.href).pathname;
const nf = (n) => new Intl.NumberFormat('ko-KR').format(Number(n) || 0);
const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
const open = () => { modal.classList.remove('hidden'); modal.style.display = 'flex'; };
const close = () => { modal.classList.add('hidden'); modal.style.display = 'none'; };
document.getElementById('order-view-close')?.addEventListener('click', close);
document.getElementById('order-view-close2')?.addEventListener('click', close);
modal.addEventListener('click', (e) => { if (e.target === modal) close(); });
if (deleteForm) {
deleteForm.addEventListener('submit', (e) => {
if (!confirm('이 발주를 삭제하시겠습니까?')) { e.preventDefault(); }
});
}
document.querySelectorAll('.js-order-view').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.dataset.orderId;
if (!id) return;
editLink.href = reviseBase + '/' + id;
if (deleteIdInput) deleteIdInput.value = id;
if (deleteBtn) deleteBtn.classList.add('hidden'); // 상태 확인 전까지 숨김
if (editLink) editLink.classList.add('hidden'); // 상태 확인 전까지 숨김
body.innerHTML = '불러오는 중…';
open();
fetch(detailBase + '/' + id, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then((r) => r.json())
.then((d) => {
if (!d || !d.ok) { body.innerHTML = '<p class="text-red-600">' + esc((d && d.error) || '불러오지 못했습니다.') + '</p>'; return; }
// 정상 상태 발주만 삭제·변경 가능 (삭제/취소된 발주는 두 버튼 모두 숨김)
const isNormal = d.status === 'normal';
if (deleteBtn) deleteBtn.classList.toggle('hidden', !isNormal);
if (editLink) editLink.classList.toggle('hidden', !isNormal);
const rows = (d.items || []).map((it, i) => `<tr>
<td class="text-center">${i + 1}</td>
<td class="text-left pl-2">${esc(it.bag_name || it.bag_code)}</td>
<td class="text-right pr-2">${nf(it.qty_box)}</td>
<td class="text-right pr-2">${nf(it.qty_sheet)}</td>
<td class="text-right pr-2">${nf(it.unit_price)}</td>
<td class="text-right pr-2">${nf(it.amount)}</td></tr>`).join('');
body.innerHTML = `
<dl class="grid grid-cols-[7rem_1fr] gap-y-1 mb-3">
<dt class="font-semibold text-gray-600">발주번호(LOT)</dt><dd class="font-mono">${esc(d.bo_lot_no)} <span class="text-gray-400">(v${d.bo_version})</span></dd>
<dt class="font-semibold text-gray-600">발주일</dt><dd>${esc(d.order_date)}</dd>
<dt class="font-semibold text-gray-600">제작업체</dt><dd>${esc(d.company || '-')}</dd>
<dt class="font-semibold text-gray-600">입고처</dt><dd>${esc(d.agency || '-')}</dd>
<dt class="font-semibold text-gray-600">발주담당</dt><dd>${esc(d.manager || '-')}</dd>
<dt class="font-semibold text-gray-600">조달수수료</dt><dd>${nf(d.fee_rate)}%</dd>
</dl>
<table class="w-full data-table text-sm">
<thead><tr><th class="w-10 text-center">번호</th><th class="text-left">품명</th><th class="text-right">박스수량</th><th class="text-right">낱장</th><th class="text-right">단가</th><th class="text-right">금액</th></tr></thead>
<tbody>${rows || '<tr><td colspan="6" class="text-center text-gray-400 py-3">품목이 없습니다.</td></tr>'}</tbody>
</table>`;
})
.catch(() => { body.innerHTML = '<p class="text-red-600">오류가 발생했습니다.</p>'; });
});
});
})();
</script>
<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();
}
});
// 박스수량 입력 중 Enter → 다음 봉투의 박스수량 입력칸으로 이동(#39)
selectedBody.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' || !event.target.classList.contains('item-qty-box')) return;
event.preventDefault();
const inputs = Array.from(selectedBody.querySelectorAll('.item-qty-box'));
const next = inputs[inputs.indexOf(event.target) + 1];
if (next) { next.focus(); next.select(); }
else { event.target.blur(); }
});
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; ?>