Files
jongryangje/app/Views/bag/flow.php
taekyoungc 8763876f19 사용자 매뉴얼·번호알기·gov-portal 대시보드와 메뉴 동선·수불 리포트를 보강한다.
- 사용자 매뉴얼: league/commonmark 기반 bag/manual(로그인 전용),
  ManualRenderer + Config\Manual manifest, 콘텐츠 8종, E2E
- 번호알기(봉투번호확인): bag/number-lookup, BagNumberLookup, E2E
- gov-portal 대시보드 시안(기본/strip)·기본코드관리 화면
- 메뉴 관리: 등록·수정 후 메뉴 화면 유지, 수정 버튼 클릭 시 상단 스크롤
- 수불/분석 리포트(LOT 수불·반품/파기·수급계획·추이) 표시 보강
- .gitignore: docs/ → /docs/ 앵커링(최상위 개발문서만 제외, app/Docs는 추적)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 00:46:51 +09:00

334 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$startDate = (string) ($startDate ?? date('Y-m-01'));
$endDate = (string) ($endDate ?? date('Y-m-d'));
$aggMode = (string) ($aggMode ?? 'period');
$bagCode = (string) ($bagCode ?? '');
$bagKind = (string) ($bagKind ?? '');
$saIdx = (int) ($saIdx ?? 0);
$rows = is_array($rows ?? null) ? $rows : [];
$bagProducts = is_array($bagProducts ?? null) ? $bagProducts : [];
$bagKindOptions = is_array($bagKindOptions ?? null) ? $bagKindOptions : [];
$agencies = is_array($agencies ?? null) ? $agencies : [];
$queried = (bool) ($queried ?? false);
$exportParams = array_filter([
'search' => '1',
'start_date' => $startDate,
'end_date' => $endDate,
'agg_mode' => $aggMode,
'bag_code' => $bagCode,
'bag_kind' => $bagKind,
'sa_idx' => $saIdx > 0 ? (string) $saIdx : '',
], static fn ($v) => $v !== null && $v !== '');
$excelUrl = $queried
? base_url('bag/flow/export') . '?' . http_build_query($exportParams)
: '';
$fmt = static fn ($n): string => number_format((int) $n);
$printExtraLines = [];
if ($queried) {
$aggLabel = $aggMode === 'daily' ? '일자별' : '기간별';
$printExtraLines[] = '조회기간: ' . $startDate . ' ~ ' . $endDate . ' (' . $aggLabel . ')';
}
$tipPage = "조회 기간 동안 봉투 품목별 입고·출고·잔량을 집계하는 수불표입니다.\n"
. "· 집계방식: 일자별(날짜마다) / 기간별(기간 합계)\n"
. "· 전일재고: 조회 시작일 전날 기준 재고(입고·반품·기타 출고 누적)\n"
. "· 입고: 입고·반품·기타 / 출고: 판매·일반·무료불출·반품·기타\n"
. "· 대행소 선택 시 판매 열만 해당 대행소 소속 판매소 기준\n"
. "조회 후 표·엑셀·인쇄에 반영됩니다.";
?>
<div class="flow-print-sheet">
<?= view('components/print_header', [
'printTitle' => '기간별 봉투 수불 현황',
'printExtraLines' => $printExtraLines,
]) ?>
<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 inline-flex items-center gap-1">
기간별 봉투 수불 현황
<?= view('components/field_tooltip', ['text' => $tipPage, 'placement' => 'below']) ?>
</span>
<div class="flex flex-wrap items-center gap-2">
<?php if ($excelUrl !== ''): ?>
<a href="<?= esc($excelUrl, 'attr') ?>" target="_blank" rel="noopener noreferrer"
class="bg-green-700 text-white px-3 py-1 rounded-sm text-sm hover:bg-green-800">엑셀저장</a>
<?php else: ?>
<span class="bg-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm cursor-not-allowed" title="조회 후 이용">엑셀저장</span>
<?php endif; ?>
<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="<?= base_url('dashboard') ?>" class="border border-gray-400 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/flow') ?>" class="flex flex-wrap items-end gap-x-3 gap-y-2 text-sm">
<input type="hidden" name="search" value="1"/>
<div class="flex flex-wrap items-center gap-2">
<label class="font-bold text-gray-700 whitespace-nowrap">조회기간</label>
<input type="date" name="start_date" value="<?= esc($startDate) ?>" class="border border-gray-300 rounded px-2 py-1" required/>
<span>~</span>
<input type="date" name="end_date" value="<?= esc($endDate) ?>" class="border border-gray-300 rounded px-2 py-1" required/>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="font-bold text-gray-700 whitespace-nowrap">봉투형식</label>
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 min-w-[11rem]">
<option value="">전체 봉투</option>
<?php foreach ($bagProducts as $bp): ?>
<option value="<?= esc((string) $bp['code']) ?>" <?= $bagCode === (string) $bp['code'] ? 'selected' : '' ?>>
<?= esc((string) $bp['code']) ?> — <?= esc((string) $bp['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-wrap items-center gap-2">
<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>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="font-bold text-gray-700 whitespace-nowrap">대행소</label>
<select name="sa_idx" class="border border-gray-300 rounded px-2 py-1 min-w-[10rem]">
<option value="0">전체</option>
<?php foreach ($agencies as $agency): ?>
<?php
$aid = (int) ($agency->sa_idx ?? 0);
$label = (string) ($agency->sa_name ?? '');
if (isset($agency->sa_kind) && (string) $agency->sa_kind !== '') {
$label = (string) $agency->sa_kind . ' — ' . $label;
}
?>
<option value="<?= $aid ?>" <?= $saIdx === $aid ? 'selected' : '' ?>><?= esc($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="flex flex-wrap items-center gap-3">
<span class="font-bold text-gray-700 whitespace-nowrap">집계방식</span>
<label class="inline-flex items-center gap-1">
<input type="radio" name="agg_mode" value="daily" <?= $aggMode === 'daily' ? 'checked' : '' ?>/>
일자별
</label>
<label class="inline-flex items-center gap-1">
<input type="radio" name="agg_mode" value="period" <?= $aggMode === 'period' ? 'checked' : '' ?>/>
기간별
</label>
</div>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
<a href="<?= base_url('bag/flow') ?>" class="text-gray-500 hover:text-gray-800 px-2">초기화</a>
</form>
</section>
<?php if (! $queried): ?>
<div class="m-2 p-3 border border-blue-200 bg-blue-50 text-sm text-blue-900 no-print">
조회 조건을 설정한 뒤 <strong>조회</strong> 버튼을 눌러 주세요.
</div>
<?php endif; ?>
<?php if ($queried): ?>
<div class="p-2 overflow-auto flow-report-wrap">
<table class="w-full data-table text-sm flow-report-table">
<thead>
<tr>
<th rowspan="2" class="flow-col-date">일자</th>
<th rowspan="2" class="flow-col-item">품목</th>
<th rowspan="2" class="flow-col-num">
<span class="flow-lbl-screen">전일재고</span><span class="flow-lbl-print">전일</span>
</th>
<th colspan="4">입고</th>
<th colspan="6">출고</th>
<th rowspan="2" class="flow-col-num">잔량</th>
</tr>
<tr>
<th class="flow-col-num">입고</th>
<th class="flow-col-num">반품</th>
<th class="flow-col-num">기타</th>
<th class="flow-col-num">
<span class="flow-lbl-screen">입고계</span><span class="flow-lbl-print">입계</span>
</th>
<th class="flow-col-num">판매</th>
<th class="flow-col-num">
<span class="flow-lbl-screen">일반불출</span><span class="flow-lbl-print">일반</span>
</th>
<th class="flow-col-num">
<span class="flow-lbl-screen">무료불출</span><span class="flow-lbl-print">무료</span>
</th>
<th class="flow-col-num">반품</th>
<th class="flow-col-num">기타</th>
<th class="flow-col-num">
<span class="flow-lbl-screen">출고계</span><span class="flow-lbl-print">출계</span>
</th>
</tr>
</thead>
<tbody class="text-right">
<?php if ($rows !== []): ?>
<?php foreach ($rows as $row): ?>
<?php
$rowType = (string) ($row['row_type'] ?? 'data');
$trClass = match ($rowType) {
'subtotal', 'grand' => 'bg-amber-50 font-semibold',
default => '',
};
?>
<tr class="<?= esc($trClass) ?>">
<td class="flow-col-date text-center"><?= esc((string) ($row['date'] ?? '')) ?></td>
<td class="flow-col-item text-left pl-2"><?= esc((string) ($row['item_name'] ?? '')) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['prev_stock'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['recv_in'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['recv_return'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['recv_misc'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['recv_total'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['out_sale'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['out_issue_gen'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['out_issue_free'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['out_return'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['out_misc'] ?? 0) ?></td>
<td class="flow-col-num tabular-nums"><?= $fmt($row['out_total'] ?? 0) ?></td>
<td class="flow-col-num font-semibold tabular-nums"><?= $fmt($row['balance'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="15" class="text-center text-gray-400 py-8">조회 결과가 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<style>
.field-tip { position: relative; display: inline-flex; vertical-align: middle; }
.field-tip-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 14px; height: 14px; font-size: 10px; font-weight: 700; line-height: 1;
color: #6b7280; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 50%;
cursor: help; user-select: none;
}
.field-tip-btn:hover, .field-tip-btn:focus { color: #1d4ed8; border-color: #93c5fd; background: #eff6ff; outline: none; }
.field-tip-panel {
position: absolute; z-index: 60; left: 50%; transform: translateX(-50%);
bottom: calc(100% + 6px); width: max-content; max-width: 300px;
padding: 0.35rem 0.5rem; border-radius: 4px;
background: #1f2937; color: #f9fafb; font-size: 11px; font-weight: 500; line-height: 1.35;
text-align: left; white-space: pre-line; box-shadow: 0 2px 8px rgba(0,0,0,.15);
opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .12s, visibility .12s;
}
.field-tip--below .field-tip-panel { bottom: auto; top: calc(100% + 6px); }
.field-tip:hover .field-tip-panel,
.field-tip:focus-within .field-tip-panel { opacity: 1; visibility: visible; }
.flow-lbl-print { display: none; }
@media screen {
.flow-report-wrap { overflow-x: auto; }
.flow-report-table { min-width: 1200px; }
}
@media print {
@page {
size: A4 portrait;
margin: 10mm 8mm;
}
html { font-size: 12px !important; }
.flow-print-sheet {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
.print-header,
.print-header table,
.print-header hr {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
.print-header table td[style*="width:45%"] table {
width: 160px !important;
max-width: 38% !important;
font-size: 9px !important;
}
.flow-report-wrap {
overflow: hidden !important;
padding: 0 !important;
width: 100% !important;
max-width: 100% !important;
}
.flow-report-table.data-table {
min-width: 0 !important;
width: 100% !important;
max-width: 100% !important;
table-layout: fixed !important;
font-size: 6px !important;
}
.flow-report-table.data-table th,
.flow-report-table.data-table td {
white-space: normal !important;
word-break: keep-all;
overflow-wrap: anywhere;
padding: 1px 1px !important;
line-height: 1.1;
vertical-align: middle;
}
.flow-lbl-screen { display: none !important; }
.flow-lbl-print { display: inline !important; }
/* 세로 A4: 일자 10% + 품목 14% + 수치 12열 각 6.33% ≈ 100% */
.flow-report-table .flow-col-date {
width: 10%;
font-size: 5px !important;
text-align: center;
}
.flow-report-table .flow-col-item {
width: 14%;
text-align: left;
font-size: 5px !important;
padding-top: 3px !important;
padding-bottom: 3px !important;
line-height: 1.25;
}
.flow-report-table .flow-col-num {
width: 6.33%;
white-space: nowrap !important;
font-size: 6px !important;
text-align: right;
padding-left: 0 !important;
padding-right: 1px !important;
}
.flow-report-table thead th {
font-size: 5px !important;
font-weight: 700;
padding: 1px 0 !important;
}
.flow-report-table tbody tr {
break-inside: avoid;
page-break-inside: avoid;
}
}
</style>