444 lines
20 KiB
PHP
444 lines
20 KiB
PHP
<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>
|
|
|
|
<?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="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="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"><?= 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="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="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>
|
|
</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('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
|
|
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>
|