Files
jongryangje/app/Views/admin/sales_report/misc_flow.php
taekyoungc 0f1d414f37 사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.
통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 16:15:15 +09:00

370 lines
19 KiB
PHP

<?= view('components/print_header', ['printTitle' => '기타 입출고']) ?>
<?php
$filters = is_array($filters ?? null) ? $filters : [];
$flowYear = (string) ($filters['flow_y'] ?? '');
$flowMonthNum = (string) ($filters['flow_m'] ?? '');
$dateYearMin = (int) ($dateYearMin ?? ((int) date('Y') - 12));
$dateYearMax = (int) ($dateYearMax ?? ((int) date('Y') + 2));
$bagCodeFilter = (string) ($filters['bag_code'] ?? '');
$bagKind = (string) ($filters['bag_kind'] ?? '');
$bagCancelOnly = ! empty($filters['bag_cancel']);
$selKey = (string) ($filters['sel_key'] ?? '');
$selectedGroup = is_array($selectedGroup ?? null) ? $selectedGroup : null;
$detailLines = is_array($detailLines ?? null) ? $detailLines : [];
$groupList = is_array($groupList ?? null) ? $groupList : [];
$packagingMap = is_array($packagingMap ?? null) ? $packagingMap : [];
$bagKindOptions = is_array($bagKindOptions ?? null) ? $bagKindOptions : [];
$bagCodes = is_array($bagCodes ?? null) ? $bagCodes : [];
$miscFlowListUrl = static function (array $extra = []) use ($filters): string {
$qs = array_merge($filters, $extra);
unset($qs['sel_key']);
if (isset($extra['sel_key'])) {
$qs['sel_key'] = $extra['sel_key'];
}
return mgmt_url('reports/misc-flow') . ($qs !== [] ? '?' . http_build_query($qs) : '');
};
$detailTotalQty = 0;
foreach ($detailLines as $line) {
$detailTotalQty += (int) ($line->bmf_qty ?? 0);
}
$registerDate = $selectedGroup ? (string) ($selectedGroup['date'] ?? date('Y-m-d')) : date('Y-m-d');
$registerType = $selectedGroup ? (string) ($selectedGroup['type'] ?? 'in') : 'in';
$registerReason = $selectedGroup ? (string) ($selectedGroup['reason'] ?? '') : '';
?>
<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 flex-wrap items-center gap-2">
<button type="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="<?= esc(work_area_home_url()) ?>" class="border border-gray-400 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">종료</a>
</div>
</div>
</section>
<?php if (! ($tableExists ?? false)): ?>
<div class="border border-orange-300 bg-orange-50 p-3 m-2 text-sm text-orange-700 no-print">
bag_misc_flow 테이블이 생성되지 않았습니다. <code>writable/database/bag_misc_flow_tables.sql</code>을 실행해 주세요.
</div>
<?php elseif (($totalRowsForLg ?? 0) === 0): ?>
<div class="border border-blue-200 bg-blue-50 p-3 m-2 text-sm text-blue-900 no-print">
선택한 지자체에 등록된 <strong>기타 입출고</strong> 데이터가 없습니다. 아래 <strong>품목 등록</strong>으로 첫 건을 넣으면 좌측 리스트에 표시됩니다.
</div>
<?php elseif (($groupList ?? []) === [] && ($fetchedRowCount ?? 0) === 0 && (($flowYear ?? '') !== '' || ($flowMonthNum ?? '') !== '' || ($bagCodeFilter ?? '') !== '' || ($bagKind ?? '') !== '' || ! empty($filters['bag_cancel']))): ?>
<div class="border border-amber-200 bg-amber-50 p-3 m-2 text-sm text-amber-900 no-print">
조회 조건(수불 년월·봉투코드·구분 등)에 맞는 내역이 없습니다. <strong>수불 년월을 「전체」</strong>로 두거나 조건을 넓혀 다시 조회해 주세요.
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="mx-2 mt-2 border border-green-300 bg-green-50 text-green-800 text-sm px-3 py-2 no-print"><?= esc((string) session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="mx-2 mt-2 border border-red-300 bg-red-50 text-red-800 text-sm px-3 py-2 no-print"><?= esc((string) session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<!-- 조회 조건 (레거시: 수불 년월, 봉투코드, 봉투 취소, 구분, 조회) -->
<section class="p-2 bg-white border-b border-gray-200 no-print">
<form method="get" action="<?= mgmt_url('reports/misc-flow') ?>" class="flex flex-wrap items-end gap-2 text-sm">
<span class="font-bold text-gray-700 whitespace-nowrap">수불 년월</span>
<select name="flow_y" class="border border-gray-300 rounded px-2 py-1 min-w-[5.5rem]" aria-label="수불 년도">
<option value="">전체</option>
<?php for ($yy = $dateYearMax; $yy >= $dateYearMin; $yy--): ?>
<option value="<?= $yy ?>" <?= $flowYear === (string) $yy ? 'selected' : '' ?>><?= $yy ?>년</option>
<?php endfor; ?>
</select>
<select name="flow_m" class="border border-gray-300 rounded px-2 py-1 min-w-[4.5rem]" aria-label="수불 월" <?= $flowYear === '' ? 'disabled' : '' ?>>
<option value=""><?= $flowYear === '' ? '—' : '전체' ?></option>
<?php for ($mi = 1; $mi <= 12; $mi++): ?>
<option value="<?= $mi ?>" <?= $flowMonthNum !== '' && (int) $flowMonthNum === $mi ? 'selected' : '' ?>><?= $mi ?>월</option>
<?php endfor; ?>
</select>
<label class="font-bold text-gray-700 whitespace-nowrap">봉투코드</label>
<input type="text" name="bag_code" value="<?= esc($bagCodeFilter) ?>" placeholder="코드 일부"
class="border border-gray-300 rounded px-2 py-1 w-28 font-mono"/>
<label class="inline-flex items-center gap-1 text-gray-700 whitespace-nowrap">
<input type="checkbox" name="bag_cancel" value="1" <?= $bagCancelOnly ? 'checked' : '' ?>/>
봉투 취소
</label>
<span class="text-xs text-gray-500 hidden sm:inline" title="출고 건만 조회">(출고만)</span>
<label class="font-bold text-gray-700 whitespace-nowrap">구분</label>
<select name="bag_kind" class="border border-gray-300 rounded px-2 py-1 min-w-[8rem]">
<option value="">전체</option>
<?php foreach ($bagKindOptions as $opt): ?>
<option value="<?= esc((string) $opt->cd_code) ?>" <?= $bagKind === (string) $opt->cd_code ? 'selected' : '' ?>>
<?= esc((string) $opt->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
<a href="<?= mgmt_url('reports/misc-flow') ?>" class="text-gray-500 hover:text-gray-800 px-2 py-1">초기화</a>
</form>
</section>
<div class="grid grid-cols-1 xl:grid-cols-4 gap-2 p-2">
<!-- 입출고 리스트 -->
<section class="border border-gray-300 bg-white xl:col-span-1">
<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-[520px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-24">수불일자</th>
<th class="w-16">수량</th>
<th class="w-14">구분</th>
<th>메모</th>
</tr>
</thead>
<tbody>
<?php if ($groupList !== []): ?>
<?php foreach ($groupList as $grp): ?>
<?php
$key = (string) ($grp['key'] ?? '');
$isSelected = $key !== '' && $key === $selKey;
$listUrl = $miscFlowListUrl(['sel_key' => $key]);
?>
<tr
class="<?= $isSelected ? 'bg-blue-100 font-semibold' : '' ?> cursor-pointer hover:bg-blue-50"
onclick="window.location.href='<?= esc($listUrl, 'attr') ?>'"
>
<td class="text-center <?= $isSelected ? 'border-l-4 border-blue-600' : '' ?>"><?= esc((string) ($grp['date'] ?? '')) ?></td>
<td class="text-right pr-2"><?= number_format((int) ($grp['totalQty'] ?? 0)) ?></td>
<td class="text-center">
<?php if ((string) ($grp['type'] ?? '') === 'in'): ?>
<span class="text-blue-700">입고</span>
<?php else: ?>
<span class="text-red-700">출고</span>
<?php endif; ?>
</td>
<td class="text-left pl-2 truncate max-w-[8rem]" title="<?= esc((string) ($grp['reason'] ?? '')) ?>"><?= esc((string) ($grp['reason'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="4" class="text-center text-gray-400 py-6">
<?php if (($totalRowsForLg ?? 0) === 0): ?>
등록된 기타 입출고가 없습니다.
<?php elseif (($fetchedRowCount ?? 0) === 0): ?>
선택한 기간·조건에 해당하는 내역이 없습니다.
<?php else: ?>
조회 결과가 없습니다.
<?php endif; ?>
</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<!-- 우측: 입출고 정보 + 봉투 코드 + 등록 -->
<div class="xl:col-span-3 space-y-2">
<form method="post" action="<?= mgmt_url('reports/misc-flow/delete') ?>" id="misc-flow-delete-form" class="no-print">
<?= csrf_field() ?>
<input type="hidden" name="flow_y" value="<?= esc($flowYear) ?>"/>
<input type="hidden" name="flow_m" value="<?= esc($flowMonthNum) ?>"/>
<input type="hidden" name="bag_code" value="<?= esc($bagCodeFilter) ?>"/>
<input type="hidden" name="bag_kind" value="<?= esc($bagKind) ?>"/>
<?php if ($bagCancelOnly): ?><input type="hidden" name="bag_cancel" value="1"/><?php endif; ?>
<input type="hidden" name="sel_key" value="<?= esc($selKey) ?>"/>
<div class="flex flex-wrap gap-2 mb-1">
<button type="submit" class="bg-red-600 text-white px-4 py-1 rounded-sm text-sm disabled:opacity-40"
<?= $selKey === '' ? 'disabled' : '' ?>
onclick="return confirm('선택한 입출고 건을 삭제할까요? 재고가 복원됩니다.');">삭제</button>
<?php
$cancelQs = $filters;
unset($cancelQs['sel_key']);
$cancelUrl = mgmt_url('reports/misc-flow') . ($cancelQs !== [] ? '?' . http_build_query($cancelQs) : '');
?>
<a href="<?= esc($cancelUrl) ?>" class="border border-gray-400 text-gray-700 px-4 py-1 rounded-sm text-sm hover:bg-gray-50 inline-block">취소</a>
</div>
</form>
<!-- 입출고 일자 (상세) -->
<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="p-2 grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<?php if ($selectedGroup): ?>
<div class="flex flex-wrap items-center gap-2">
<span class="font-bold text-gray-700 whitespace-nowrap">수불 일자</span>
<span class="border border-gray-200 bg-gray-50 px-2 py-1 rounded"><?= esc((string) ($selectedGroup['date'] ?? '')) ?></span>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="font-bold text-gray-700 whitespace-nowrap">선택</span>
<span class="border border-gray-200 bg-gray-50 px-2 py-1 rounded"><?= esc((string) ($selectedGroup['typeLabel'] ?? '')) ?></span>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="font-bold text-gray-700 whitespace-nowrap">분류</span>
<span class="border border-gray-200 bg-gray-50 px-2 py-1 rounded min-w-[6rem]">
<?= esc($selectedBagKindLabel ?? '') !== '' ? esc($selectedBagKindLabel) : '—' ?>
</span>
</div>
<div class="md:col-span-2">
<span class="font-bold text-gray-700 block mb-1">비고</span>
<div class="border border-gray-200 bg-gray-50 rounded px-2 py-2 min-h-[4rem] whitespace-pre-wrap"><?= esc((string) ($selectedGroup['reason'] ?? '')) ?></div>
</div>
<?php else: ?>
<p class="text-gray-400 col-span-2 py-2">좌측 입출고 리스트에서 건을 선택하거나, 아래에서 신규 등록해 주세요.</p>
<?php endif; ?>
</div>
</section>
<!-- 입출고 봉투 코드 -->
<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 max-h-[280px]">
<table class="w-full data-table text-sm">
<thead>
<tr>
<th class="w-10">No</th>
<th>봉투 코드</th>
<th>봉투 종류</th>
<th class="w-20">수량</th>
<th class="w-14">단위</th>
</tr>
</thead>
<tbody class="text-right">
<?php if ($detailLines !== []): ?>
<?php foreach ($detailLines as $idx => $line): ?>
<?php
$code = (string) ($line->bmf_bag_code ?? '');
$pu = $packagingMap[$code] ?? null;
$unitLabel = '매';
if ($pu && (int) ($pu->pu_pack_per_sheet ?? 0) > 0) {
$unitLabel = '매';
}
?>
<tr>
<td class="text-center"><?= $idx + 1 ?></td>
<td class="text-center font-mono"><?= esc($code) ?></td>
<td class="text-left pl-2"><?= esc((string) ($line->bmf_bag_name ?? '')) ?></td>
<td class="pr-2"><?= number_format((int) ($line->bmf_qty ?? 0)) ?></td>
<td class="text-center"><?= esc($unitLabel) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="5" class="text-center text-gray-400 py-6">봉투 코드 내역이 없습니다.</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr class="bg-gray-50 font-semibold">
<td colspan="3" class="text-center">합계</td>
<td class="text-right pr-2"><?= number_format($detailTotalQty) ?></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</section>
<!-- 품목 등록 (동일 수불일자·구분·비고로 묶임) -->
<?php if ($tableExists ?? false): ?>
<section class="border border-gray-300 bg-white no-print">
<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="<?= mgmt_url('reports/misc-flow') ?>" class="p-2 flex flex-wrap items-end gap-2 text-sm">
<?= csrf_field() ?>
<input type="hidden" name="flow_y" value="<?= esc($flowYear) ?>"/>
<input type="hidden" name="flow_m" value="<?= esc($flowMonthNum) ?>"/>
<input type="hidden" name="bag_code" value="<?= esc($bagCodeFilter) ?>"/>
<input type="hidden" name="bag_kind" value="<?= esc($bagKind) ?>"/>
<?php if ($bagCancelOnly): ?><input type="hidden" name="bag_cancel" value="1"/><?php endif; ?>
<input type="hidden" name="sel_key" value="<?= esc($selKey) ?>"/>
<label class="font-bold text-gray-700">수불 일자</label>
<input type="date" name="bmf_date" value="<?= esc($registerDate) ?>" class="border border-gray-300 rounded px-2 py-1" required/>
<label class="font-bold text-gray-700">선택</label>
<select name="bmf_type" class="border border-gray-300 rounded px-2 py-1 min-w-[7.5rem] w-32">
<option value="in" <?= $registerType === 'in' ? 'selected' : '' ?>>입고</option>
<option value="out" <?= $registerType === 'out' ? 'selected' : '' ?>>출고</option>
</select>
<label class="font-bold text-gray-700">분류</label>
<select name="bmf_bag_kind" id="bmf-bag-kind" class="border border-gray-300 rounded px-2 py-1 min-w-[7rem]">
<option value="">전체</option>
<?php foreach ($bagKindOptions as $opt): ?>
<option value="<?= esc((string) $opt->cd_code) ?>" <?= ($selectedBagKind ?? '') === (string) $opt->cd_code ? 'selected' : '' ?>>
<?= esc((string) $opt->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
<label class="font-bold text-gray-700">비고</label>
<input type="text" name="bmf_reason" value="<?= esc($registerReason) ?>" placeholder="입출고 메모" maxlength="200"
class="border border-gray-300 rounded px-2 py-1 w-40 max-w-[10rem] shrink-0" required/>
<label class="font-bold text-gray-700">봉투 코드</label>
<select name="bmf_bag_code" id="bmf-bag-code" class="border border-gray-300 rounded px-2 py-1 min-w-[11rem]" required>
<option value="">선택</option>
<?php foreach ($bagCodes as $bc): ?>
<?php $code = (string) $bc->cd_code; ?>
<option value="<?= esc($code) ?>" data-kind-prefix="<?= esc(substr($code, 0, 2)) ?>">
<?= esc($code . ' - ' . $bc->cd_name) ?>
</option>
<?php endforeach; ?>
</select>
<label class="font-bold text-gray-700">수량</label>
<input type="number" name="bmf_qty" min="1" class="border border-gray-300 rounded px-2 py-1 w-24" required/>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">등록</button>
</form>
<p class="px-2 pb-2 text-xs text-gray-500">동일 수불일자·입출고·비고로 등록한 품목은 좌측 리스트에서 한 건으로 묶여 표시됩니다.</p>
</section>
<?php endif; ?>
</div>
</div>
<script>
(function () {
const kindSelect = document.getElementById('bmf-bag-kind');
const bagSelect = document.getElementById('bmf-bag-code');
if (!kindSelect || !bagSelect) return;
const allOptions = Array.from(bagSelect.querySelectorAll('option[data-kind-prefix]'));
function filterBagCodes() {
const prefix = kindSelect.value;
const current = bagSelect.value;
allOptions.forEach(function (opt) {
const show = prefix === '' || opt.getAttribute('data-kind-prefix') === prefix;
opt.hidden = !show;
opt.disabled = !show;
});
const selected = bagSelect.querySelector('option[value="' + CSS.escape(current) + '"]');
if (selected && !selected.hidden) {
bagSelect.value = current;
} else {
bagSelect.value = '';
}
}
kindSelect.addEventListener('change', filterBagCodes);
filterBagCodes();
const flowYearSelect = document.querySelector('select[name="flow_y"]');
const flowMonthSelect = document.querySelector('form[method="get"] select[name="flow_m"]');
if (flowYearSelect && flowMonthSelect) {
const syncFlowMonthSelect = function () {
const hasYear = flowYearSelect.value !== '';
flowMonthSelect.disabled = !hasYear;
if (!hasYear) {
flowMonthSelect.value = '';
}
const firstOpt = flowMonthSelect.querySelector('option[value=""]');
if (firstOpt) {
firstOpt.textContent = hasYear ? '전체' : '—';
}
};
flowYearSelect.addEventListener('change', syncFlowMonthSelect);
syncFlowMonthSelect();
}
})();
</script>