Files
jongryangje/app/Views/admin/sales_report/supply_demand.php
taekyoungc a8afaf4af2 style: 표/패널 UI 전면 통일 + 화면설명 드로어·글씨크기·탭 개선
표 디자인
- 모든 표를 가벼운 스타일로 통일(.data-table 경량화: 작은 회색 헤더·연한 구분선·hover)
- 표/패널 바깥 테두리 둥글게(rounded-lg) 일괄 적용, 표 래퍼에 패딩 카드(p-4) 통일
- 표 헤더·데이터 정렬을 전 화면 좌측 기준으로 통일
  - .data-table th/td text-align:left (전역), 흩어진 center/right 정렬 정리
  - 재디자인 Tailwind 표(포장단위·단가·기본코드·담당자·업체·판매대행소·무료대상자·지정판매소)도 셀 좌측화
- 기본정보관리 등 나머지 소메뉴 표를 기본 코드 관리 스타일(가벼운 표·상태 pill)로 재디자인

워크스페이스/공통
- "이 화면 설명" → 새 탭 대신 우측 드로어 팝업(현재 화면과 동시에 보기, Esc·드래그 폭조절)
- 상단바 글씨 크기 조절(A−/A+), 작업 내용에 zoom 적용
- 탭 최대치 도달 시 자동 삭제 대신 안내 토스트, "모두 닫기"(업무 현황 탭은 보존)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:26:36 +09:00

279 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
$refDate = (string) ($refDate ?? date('Y-m-d'));
$leadDays = (int) ($leadDays ?? 40);
$stockScope = (string) ($stockScope ?? 'all');
$salesScope = (string) ($salesScope ?? 'all');
$rows = is_array($rows ?? null) ? $rows : [];
$queried = (bool) ($queried ?? false);
$stockLabel = (string) ($stockLabel ?? 'ALL');
$salesLabel = (string) ($salesLabel ?? 'ALL');
$fmtKrRef = static function (string $ymd): string {
$ts = strtotime($ymd);
return $ts ? date('Y.m.d', $ts) . ' 현재' : $ymd;
};
/** 툴팁: 의미 + 계산(간단) */
$tipPage = "봉투 품목별로 재고가 며칠 버티는지, 언제·얼마나 발주할지 보는 수급·발주 계획표입니다.";
$tipLead = "의미: 발주 후 입고까지 걸리는 제작기일(일). 재고 소진 전에 발주하려는 여유.\n계산: 발주예정일 = 기준일 + 소진일수 보유일수";
$tipStock = "의미: 표에 넣을 현재고·총재고 범위.\n기존=바코드 미등록(수기), 바코드=등록 품목.";
$tipSales = "의미: 소진일수에 쓸 판매 속도 범위.\n최근 12개월 순판매(또는 바코드 판매) 월평균.";
$tipTotal = "의미: 지금·곧 쓸 수 있는 재고 합계.\n계산: 현재고 + 입고예정량";
$tipMonth = "의미: 요즘 한 달 판매 규모(평균).\n최근 12개월 월평균 판매량.";
$tipDepl = "의미: 이 판매 속도면 재고가 며칠 남는지.\n계산: (총재고 ÷ 월판매량) × 30";
$tipSched = "의미: 발주를 넣기 좋은 날(제작기일 반영).\n계산: 기준일 + 소진일수 보유일수. 기한 지남=빨간색·긴급";
$tipOrder = "의미: 그 시점에 맞춰 제안하는 추가 발주 장수.\n촉박하거나 발주예정일이 지난 품목만 표시.";
$printExtraLines = [
$fmtKrRef($refDate),
'적정재고 보유일수(제작기일): ' . $leadDays . '일',
'현재고: ' . $stockLabel . ' · 월평균판매량: ' . $salesLabel,
'※ 제작기일 ' . $leadDays . '일 기준으로 발주예정일 산정 (레거시 화면 유추)',
];
?>
<?= 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">
<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 text-sm">
<form method="get" action="<?= mgmt_url('reports/supply-demand') ?>" class="flex flex-wrap items-end gap-x-4 gap-y-3">
<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="ref_date" value="<?= esc($refDate) ?>" class="border border-gray-300 rounded px-2 py-1" required/>
<span class="text-gray-600"><?= esc($fmtKrRef($refDate)) ?></span>
</div>
<div class="flex flex-wrap items-center gap-2">
<label class="font-bold text-gray-700 whitespace-nowrap inline-flex items-center gap-0.5">
적정재고 보유일수
<?= view('components/field_tooltip', ['text' => $tipLead]) ?>
</label>
<input type="number" name="lead_days" value="<?= (int) $leadDays ?>" min="1" max="365"
class="border border-gray-300 rounded px-2 py-1 w-20 text-right"/>
</div>
<fieldset class="flex flex-wrap items-center gap-2 border-0 p-0 m-0">
<legend class="font-bold text-gray-700 whitespace-nowrap mr-1 inline-flex items-center gap-0.5">
현재고 선택 옵션
<?= view('components/field_tooltip', ['text' => $tipStock]) ?>
</legend>
<?php foreach (['all' => 'ALL', 'legacy' => '기존 봉투', 'barcode' => '바코드 봉투'] as $val => $lab): ?>
<label class="inline-flex items-center gap-1">
<input type="radio" name="stock_scope" value="<?= esc($val) ?>" <?= $stockScope === $val ? 'checked' : '' ?>/>
<?= esc($lab) ?>
</label>
<?php endforeach; ?>
</fieldset>
<fieldset class="flex flex-wrap items-center gap-2 border-0 p-0 m-0">
<legend class="font-bold text-gray-700 whitespace-nowrap mr-1 inline-flex items-center gap-0.5">
월 평균판매량 선택 옵션
<?= view('components/field_tooltip', ['text' => $tipSales]) ?>
</legend>
<?php foreach (['all' => 'ALL', 'legacy' => '기존 봉투', 'barcode' => '바코드 봉투'] as $val => $lab): ?>
<label class="inline-flex items-center gap-1">
<input type="radio" name="sales_scope" value="<?= esc($val) ?>" <?= $salesScope === $val ? 'checked' : '' ?>/>
<?= esc($lab) ?>
</label>
<?php endforeach; ?>
</fieldset>
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
</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; ?>
<div class="supply-plan-print-sheet">
<div class="supply-plan-print m-2 border border-gray-300 rounded-lg p-4 overflow-auto print:m-0">
<table class="w-full data-table text-sm supply-plan-table">
<thead>
<tr class="bg-gray-100">
<th colspan="4" class="text-center border-b border-gray-300 sp-group-h">최근 발주 내역</th>
<th colspan="5" class="text-center border-b border-gray-300 border-l sp-group-h">현재고 및 예상 판매일수</th>
<th colspan="2" class="text-center border-b border-gray-300 border-l sp-group-h">추가발주 예정내역</th>
</tr>
<tr>
<th class="sp-col-date">발주일자</th>
<th class="sp-col-name">봉투종류</th>
<th class="sp-col-num text-right">발주량</th>
<th class="sp-col-num text-right">발주시재고</th>
<th class="sp-col-num text-right border-l">현재고</th>
<th class="sp-col-num text-right">입고예정량</th>
<th class="sp-col-num text-right">
<span class="inline-flex items-center justify-end gap-0.5 w-full">총재고<?= view('components/field_tooltip', ['text' => $tipTotal, 'placement' => 'below']) ?></span>
</th>
<th class="sp-col-num text-right">
<span class="inline-flex items-center justify-end gap-0.5 w-full">월판매량<?= view('components/field_tooltip', ['text' => $tipMonth, 'placement' => 'below']) ?></span>
</th>
<th class="sp-col-num text-right">
<span class="inline-flex items-center justify-end gap-0.5 w-full">소진일수(일)<?= view('components/field_tooltip', ['text' => $tipDepl, 'placement' => 'below']) ?></span>
</th>
<th class="sp-col-date text-center border-l">
<span class="inline-flex items-center justify-center gap-0.5">발주예정일<?= view('components/field_tooltip', ['text' => $tipSched, 'placement' => 'below']) ?></span>
</th>
<th class="sp-col-num text-right">
<span class="inline-flex items-center justify-end gap-0.5 w-full">발주수량<?= view('components/field_tooltip', ['text' => $tipOrder, 'placement' => 'below']) ?></span>
</th>
</tr>
</thead>
<tbody>
<?php if ($queried && $rows === []): ?>
<tr>
<td colspan="11" class="text-center text-gray-500 py-8">표시할 품목이 없습니다.</td>
</tr>
<?php endif; ?>
<?php foreach ($rows as $row): ?>
<?php
$depl = (int) ($row['depletion_days'] ?? 0);
$deplDisplay = $depl <= 0 ? '—' : number_format($depl);
$sched = (string) ($row['schedule_date'] ?? '');
$schedOver = (bool) ($row['schedule_overdue'] ?? false);
$schedDisplay = '—';
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $sched, $m)) {
$y = (int) $m[1];
if ($y >= 1990 && $y <= 2200) {
$schedDisplay = $m[1] . '.' . $m[2] . '.' . $m[3];
}
}
?>
<tr>
<td class="sp-col-date text-center"><?= ($row['last_order_date'] ?? '') !== '' ? esc(str_replace('-', '.', (string) $row['last_order_date'])) : '—' ?></td>
<td class="sp-col-name text-left"><?= esc((string) ($row['bag_name'] ?? $row['bag_code'] ?? '')) ?></td>
<td class="sp-col-num text-right tabular-nums"><?= (int) ($row['last_order_qty'] ?? 0) > 0 ? number_format((int) $row['last_order_qty']) : '—' ?></td>
<td class="sp-col-num text-right tabular-nums"><?= (int) ($row['stock_at_order'] ?? 0) > 0 ? number_format((int) $row['stock_at_order']) : '—' ?></td>
<td class="sp-col-num text-right tabular-nums border-l"><?= number_format((int) ($row['current_stock'] ?? 0)) ?></td>
<td class="sp-col-num text-right tabular-nums"><?= number_format((int) ($row['pending_inbound'] ?? 0)) ?></td>
<td class="sp-col-num text-right tabular-nums font-semibold"><?= number_format((int) ($row['total_stock'] ?? 0)) ?></td>
<td class="sp-col-num text-right tabular-nums"><?= number_format((int) ($row['monthly_avg_sales'] ?? 0)) ?></td>
<td class="sp-col-num text-right tabular-nums"><?= esc($deplDisplay) ?></td>
<td class="sp-col-date text-center border-l <?= $schedOver ? 'text-red-600 font-bold' : '' ?>"><?= esc($schedDisplay) ?></td>
<td class="sp-col-num text-right tabular-nums <?= (int) ($row['order_qty'] ?? 0) > 0 ? 'text-red-600 font-bold' : '' ?>">
<?= number_format((int) ($row['order_qty'] ?? 0)) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</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: 280px;
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; }
.supply-plan-table thead th { font-size: 0.75rem; line-height: 1.2; padding: 0.35rem 0.25rem; overflow: visible; }
.supply-plan-table thead th .field-tip-panel { max-width: 260px; }
.supply-plan-table tbody td { padding: 0.25rem 0.35rem; }
@media screen {
.supply-plan-print { overflow-x: auto; }
.supply-plan-table { min-width: 960px; }
}
@media print {
@page {
size: A4 portrait;
margin: 10mm 8mm;
}
.no-print { display: none !important; }
.supply-plan-print-sheet {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
.supply-plan-print {
border: none !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
width: 100% !important;
max-width: 100% !important;
}
.supply-plan-table.data-table {
min-width: 0 !important;
width: 100% !important;
max-width: 100% !important;
table-layout: fixed !important;
font-size: 6px !important;
}
.supply-plan-table.data-table th,
.supply-plan-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;
}
.supply-plan-table .sp-group-h {
font-size: 5px !important;
padding: 1px !important;
}
/* 세로 A4: 날짜 2×4.5% + 품목 11% + 수치 8×10% = 100% */
.supply-plan-table .sp-col-date {
width: 4.5%;
font-size: 5px !important;
text-align: center;
}
.supply-plan-table .sp-col-name {
width: 11%;
text-align: left !important;
font-size: 5px !important;
line-height: 1.2;
}
.supply-plan-table .sp-col-num {
width: 10%;
font-size: 5px !important;
text-align: right !important;
}
}
</style>