- 사용자 매뉴얼: 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>
334 lines
13 KiB
PHP
334 lines
13 KiB
PHP
<?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>
|