사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.
통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,62 +1,232 @@
|
||||
<?= view('components/print_header', ['printTitle' => '년 판매 현황']) ?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/** @var int $year */
|
||||
/** @var string $gugunCode */
|
||||
/** @var int $saIdx */
|
||||
/** @var list<object> $agencies */
|
||||
/** @var list<array{code: string, name: string}> $gugunOptions */
|
||||
/** @var list<array{id: string, label: string}> $colSpec */
|
||||
/** @var list<array{name: string, lines: list<array<string,mixed>>}> $itemBlocks */
|
||||
/** @var array{name: string, lines: list<array<string,mixed>>} $footerBlock */
|
||||
/** @var bool $hasBsFee */
|
||||
/** @var string $lgName */
|
||||
/** @var string $gugunLabel */
|
||||
/** @var string $agencyLabel */
|
||||
/** @var list<string> $printExtraLines */
|
||||
/** @var bool $hasYearlyData */
|
||||
|
||||
$yMax = (int) date('Y') + 1;
|
||||
$yMin = 2020;
|
||||
|
||||
$exportParams = array_merge([
|
||||
'year' => (string) ($year ?? date('Y')),
|
||||
'export' => '1',
|
||||
], array_filter([
|
||||
'gugun_code' => (string) ($gugunCode ?? ''),
|
||||
'sa_idx' => (int) ($saIdx ?? 0) > 0 ? (string) (int) ($saIdx ?? 0) : '',
|
||||
], static fn ($v): bool => $v !== '' && $v !== null && $v !== 0));
|
||||
$excelUrl = mgmt_url('reports/yearly-sales?' . http_build_query($exportParams));
|
||||
|
||||
$colCount = 2 + count($colSpec ?? []);
|
||||
$nMetricCols = max(1, count($colSpec ?? []));
|
||||
$metricColPct = round(86 / $nMetricCols, 4);
|
||||
|
||||
$fmtMeasureCell = static function (array $cell, string $measureKey, bool $hasBsFee): string {
|
||||
if ($measureKey === 'fee' && ! $hasBsFee) {
|
||||
return '—';
|
||||
}
|
||||
if ($measureKey === 'qty') {
|
||||
return number_format((int) ($cell['qty'] ?? 0));
|
||||
}
|
||||
|
||||
return number_format((int) round((float) ($cell[$measureKey] ?? 0)));
|
||||
};
|
||||
?>
|
||||
<?= view('components/print_header', [
|
||||
'printTitle' => ((int) ($year ?? date('Y'))) . '년 판매 현황',
|
||||
'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">년 판매 현황 (월별)</span>
|
||||
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||
<span class="text-sm font-bold text-gray-700">년 판매 현황</span>
|
||||
<div class="flex flex-wrap 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 transition">인쇄</button>
|
||||
<a href="<?= esc($excelUrl, 'attr') ?>" class="inline-flex items-center border border-green-600 text-green-700 px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="p-2 bg-white border-b border-gray-200">
|
||||
<form method="GET" action="<?= mgmt_url('reports/yearly-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||
<label class="text-sm text-gray-600">연도</label>
|
||||
<select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||
<?php for ($y = (int) date('Y'); $y >= 2020; $y--): ?>
|
||||
<option value="<?= $y ?>" <?= (int)($year ?? date('Y')) === $y ? 'selected' : '' ?>><?= $y ?>년</option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
|
||||
<section class="p-3 bg-white border-b border-gray-200 no-print">
|
||||
<form method="GET" action="<?= mgmt_url('reports/yearly-sales') ?>" class="flex flex-wrap items-end gap-3 text-sm">
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">조회 년도</label>
|
||||
<select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[7rem]">
|
||||
<?php for ($y = $yMax; $y >= $yMin; $y--): ?>
|
||||
<option value="<?= $y ?>" <?= (int) ($year ?? date('Y')) === $y ? 'selected' : '' ?>><?= $y ?>년</option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">구·군</label>
|
||||
<select name="gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem] max-w-[18rem]">
|
||||
<option value="">전체</option>
|
||||
<?php foreach ($gugunOptions ?? [] as $g): ?>
|
||||
<?php $gc = (string) ($g['code'] ?? ''); ?>
|
||||
<option value="<?= esc($gc, 'attr') ?>" <?= ($gugunCode ?? '') === $gc ? 'selected' : '' ?>><?= esc((string) ($g['name'] ?? $gc)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">대행소</label>
|
||||
<select name="sa_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[12rem] max-w-[20rem]">
|
||||
<option value="0">전체</option>
|
||||
<?php foreach ($agencies ?? [] as $agency): ?>
|
||||
<?php $aid = (int) ($agency->sa_idx ?? 0); ?>
|
||||
<option value="<?= esc((string) $aid) ?>" <?= (int) ($saIdx ?? 0) === $aid ? 'selected' : '' ?>>
|
||||
<?= esc(trim((string) ($agency->sa_name ?? ''))) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
||||
</form>
|
||||
</section>
|
||||
<div class="border border-gray-300 overflow-auto mt-2">
|
||||
<table class="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th>1월</th><th>2월</th><th>3월</th><th>4월</th><th>5월</th><th>6월</th>
|
||||
<th>7월</th><th>8월</th><th>9월</th><th>10월</th><th>11월</th><th>12월</th>
|
||||
<th class="bg-gray-100">합계</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php
|
||||
$grandTotal = array_fill(1, 13, 0); // 1~12 + 13=total
|
||||
foreach ($result as $row):
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||
<?php for ($m = 1; $m <= 12; $m++):
|
||||
$key = 'm' . sprintf('%02d', $m);
|
||||
$val = (int) $row->$key;
|
||||
$grandTotal[$m] += $val;
|
||||
?>
|
||||
<td><?= $val > 0 ? number_format($val) : '-' ?></td>
|
||||
<?php endfor; ?>
|
||||
<?php $grandTotal[13] += (int) $row->total; ?>
|
||||
<td class="font-bold bg-gray-50"><?= number_format((int) $row->total) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($result)): ?>
|
||||
<tr><td colspan="15" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<tr class="font-bold bg-gray-100">
|
||||
<td colspan="2" class="text-center">합계</td>
|
||||
<?php for ($m = 1; $m <= 12; $m++): ?>
|
||||
<td><?= $grandTotal[$m] > 0 ? number_format($grandTotal[$m]) : '-' ?></td>
|
||||
<?php endfor; ?>
|
||||
<td class="bg-gray-200"><?= number_format($grandTotal[13]) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<section class="p-3 bg-white yearly-sales-report-section">
|
||||
<style>
|
||||
/* 화면: 가로 스크롤. 인쇄: 가로 용지 + 작은 글자 + 셀 줄바꿈으로 한 페이지에 맞춤 */
|
||||
@media print {
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 5mm 6mm;
|
||||
}
|
||||
.yearly-sales-report-section {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.yearly-sales-scroll-wrap {
|
||||
overflow: visible !important;
|
||||
border: 1px solid #333 !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
#yearly-sales-table {
|
||||
font-size: 7pt !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
table-layout: fixed !important;
|
||||
}
|
||||
#yearly-sales-table th,
|
||||
#yearly-sales-table td {
|
||||
min-width: 0 !important;
|
||||
max-width: none !important;
|
||||
padding: 1px 2px !important;
|
||||
white-space: normal !important;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.15;
|
||||
vertical-align: top;
|
||||
}
|
||||
#yearly-sales-table th {
|
||||
font-size: 6.5pt !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
@media screen {
|
||||
#yearly-sales-table td.tabular-nums {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="mb-2 text-center no-print">
|
||||
<h1 class="text-lg font-bold m-0"><?= (int) ($year ?? date('Y')) ?>년 판매 현황</h1>
|
||||
<p class="text-sm text-gray-700 m-1"><?= esc(trim(($lgName ?? '') . ' · 구·군: ' . ($gugunLabel ?? '') . ' · 대행소: ' . ($agencyLabel ?? ''))) ?></p>
|
||||
<p class="text-xs text-gray-500 m-0">(단위: 매 / 원)</p>
|
||||
</div>
|
||||
|
||||
<div class="yearly-sales-scroll-wrap border border-gray-300 overflow-x-auto">
|
||||
<table class="w-full data-table text-xs sm:text-sm" id="yearly-sales-table">
|
||||
<colgroup>
|
||||
<col style="width: 9%;"/>
|
||||
<col style="width: 5%;"/>
|
||||
<?php foreach (($colSpec ?? []) as $_): ?>
|
||||
<col style="width: <?= esc((string) $metricColPct, 'attr') ?>%;"/>
|
||||
<?php endforeach; ?>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="align-middle min-w-0 sm:min-w-[7rem] max-w-[10rem] sm:max-w-[12rem] text-left pl-2">품목</th>
|
||||
<th class="align-middle min-w-0 sm:min-w-[4.5rem]">구분</th>
|
||||
<?php foreach ($colSpec ?? [] as $col): ?>
|
||||
<th class="align-middle text-center min-w-0 sm:min-w-[4.5rem] border-l border-gray-200"><?= esc((string) ($col['label'] ?? '')) ?></th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php if (! ($hasYearlyData ?? false)): ?>
|
||||
<tr>
|
||||
<td colspan="<?= (int) $colCount ?>" class="text-center text-gray-400 py-6">조회된 데이터가 없습니다.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($itemBlocks ?? [] as $block): ?>
|
||||
<?php $lines = $block['lines'] ?? []; ?>
|
||||
<?php foreach ($lines as $liIdx => $li): ?>
|
||||
<tr class="odd:bg-white even:bg-gray-50/80">
|
||||
<?php if ($liIdx === 0): ?>
|
||||
<td rowspan="4" class="text-left align-top pl-2 pt-1 font-medium border-r border-gray-200"><?= esc((string) ($block['name'] ?? '')) ?></td>
|
||||
<?php endif; ?>
|
||||
<td class="text-left pl-2 border-r border-gray-100"><?= esc((string) ($li['measure'] ?? '')) ?></td>
|
||||
<?php
|
||||
$cells = (array) ($li['cells'] ?? []);
|
||||
$mk = (string) ($li['measureKey'] ?? '');
|
||||
?>
|
||||
<?php foreach ($colSpec ?? [] as $col): ?>
|
||||
<?php $cid = (string) ($col['id'] ?? ''); ?>
|
||||
<?php $cell = (array) ($cells[$cid] ?? ['qty' => 0, 'amt' => 0.0, 'fee' => 0.0, 'levy' => 0.0]); ?>
|
||||
<td class="border-l border-gray-100 tabular-nums"><?= $fmtMeasureCell($cell, $mk, (bool) ($hasBsFee ?? false)) ?></td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php $fLines = $footerBlock['lines'] ?? []; ?>
|
||||
<?php foreach ($fLines as $fIdx => $li): ?>
|
||||
<tr class="bg-amber-50 font-semibold border-t-2 border-amber-200">
|
||||
<?php if ($fIdx === 0): ?>
|
||||
<td rowspan="4" class="text-center align-middle text-amber-900 border-r border-amber-200"><?= esc((string) ($footerBlock['name'] ?? '전체 합계')) ?></td>
|
||||
<?php endif; ?>
|
||||
<td class="text-left pl-2 border-r border-amber-100"><?= esc((string) ($li['measure'] ?? '')) ?></td>
|
||||
<?php
|
||||
$cells = (array) ($li['cells'] ?? []);
|
||||
$mk = (string) ($li['measureKey'] ?? '');
|
||||
?>
|
||||
<?php foreach ($colSpec ?? [] as $col): ?>
|
||||
<?php $cid = (string) ($col['id'] ?? ''); ?>
|
||||
<?php $cell = (array) ($cells[$cid] ?? ['qty' => 0, 'amt' => 0.0, 'fee' => 0.0, 'levy' => 0.0]); ?>
|
||||
<td class="border-l border-amber-100 tabular-nums"><?= $fmtMeasureCell($cell, $mk, (bool) ($hasBsFee ?? false)) ?></td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function () {
|
||||
const year = <?= json_encode((int) ($year ?? (int) date('Y')), JSON_THROW_ON_ERROR) ?>;
|
||||
let savedTitle = document.title;
|
||||
function stamp() {
|
||||
const d = new Date();
|
||||
const p = (n) => String(n).padStart(2, '0');
|
||||
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '_' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
|
||||
}
|
||||
window.addEventListener('beforeprint', function () {
|
||||
savedTitle = document.title;
|
||||
document.title = '년판매현황_' + year + '_' + stamp();
|
||||
});
|
||||
window.addEventListener('afterprint', function () {
|
||||
document.title = savedTitle;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user