실사 저장값이 페이지 이동 후 원복되지 않도록 저장/조회 경로를 보강하고, 코드 범위 보정과 bis 간 동기화를 추가했다. 또한 메뉴 관리를 레벨4 이상으로 제한하고 메뉴 변경 사항을 모든 지자체에 일괄 반영하도록 동기화 로직을 도입했다. Made-with: Cursor
397 lines
19 KiB
PHP
397 lines
19 KiB
PHP
<?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>
|