사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.
통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
123
app/Views/bag/analytics_monthly_trend.php
Normal file
123
app/Views/bag/analytics_monthly_trend.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
$baseYm = (string) ($baseYm ?? date('Y-m'));
|
||||
$baseYmDot = str_replace('-', '.', $baseYm);
|
||||
$trendBasis = (string) ($trendBasis ?? 'year_avg');
|
||||
$deviationMin = (float) ($deviationMin ?? 0);
|
||||
$queried = (bool) ($queried ?? false);
|
||||
$filters = is_array($filters ?? null) ? $filters : [];
|
||||
$rows = is_array($rows ?? null) ? $rows : [];
|
||||
$agencies = is_array($filters['agencies'] ?? null) ? $filters['agencies'] : [];
|
||||
$saIdx = (int) ($saIdx ?? 0);
|
||||
$reportMeta = is_array($reportMeta ?? null) ? $reportMeta : [];
|
||||
$shopCount = (int) ($reportMeta['shopCount'] ?? 0);
|
||||
$monthSalesShops = (int) ($reportMeta['monthSalesShops'] ?? 0);
|
||||
$error = (string) ($error ?? '');
|
||||
$lgPickNotice = (string) ($lgPickNotice ?? '');
|
||||
$prevAvgLabel = ($trendBasis ?? '') === 'month' ? '전년 동월' : '전년 평균';
|
||||
?>
|
||||
<?= view('components/print_header', [
|
||||
'printTitle' => '월별 판매 추이 분석',
|
||||
'printExtraLines' => ['기준년월: ' . $baseYmDot, '(단위: 매)'],
|
||||
]) ?>
|
||||
|
||||
<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 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">인쇄</button>
|
||||
<a href="<?= base_url('dashboard') ?>" class="border border-gray-400 text-gray-700 px-3 py-1 rounded-sm text-sm">종료</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="p-2 bg-white border-b border-gray-200 no-print text-sm">
|
||||
<form method="get" action="<?= site_url('bag/analytics/monthly-trend') ?>" class="flex flex-wrap items-end gap-3">
|
||||
<input type="hidden" name="search" value="1"/>
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">기준년월</label>
|
||||
<input type="month" name="base_ym" value="<?= esc($baseYm) ?>" class="border border-gray-300 rounded px-2 py-1 min-w-[11rem] w-full max-w-[14rem]"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">판매추이기준</label>
|
||||
<select name="trend_basis" class="border border-gray-300 rounded px-2 py-1 min-w-[12rem] w-full max-w-[16rem]">
|
||||
<option value="year_avg" <?= $trendBasis === 'year_avg' ? 'selected' : '' ?>>년 평균</option>
|
||||
<option value="month" <?= $trendBasis === 'month' ? 'selected' : '' ?>>동월(전년)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">편차</label>
|
||||
<input type="number" name="deviation_min" value="<?= esc((string) $deviationMin) ?>" step="0.01" min="0" class="border border-gray-300 rounded px-2 py-1 w-28"/>
|
||||
<span class="text-gray-500">% 이상(절대값)</span>
|
||||
</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 min-w-[14rem] w-full max-w-[18rem]">
|
||||
<option value="0">전체</option>
|
||||
<?php foreach ($agencies as $agency): ?>
|
||||
<?php $aid = (int) ($agency->sa_idx ?? 0); ?>
|
||||
<option value="<?= $aid ?>" <?= $saIdx === $aid ? 'selected' : '' ?>><?= esc((string) ($agency->sa_name ?? '')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-1">(단위: 매) · <?= esc($prevAvgLabel) ?> 대비 기준월 판매량 편차를 표시합니다.</p>
|
||||
</section>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="m-2 p-3 border border-red-200 bg-red-50 text-sm text-red-800 no-print"><?= esc($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($lgPickNotice !== ''): ?>
|
||||
<div class="m-2 p-3 border border-blue-200 bg-blue-50 text-sm text-blue-900 no-print"><?= esc($lgPickNotice) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($queried && $monthSalesShops === 0 && $shopCount > 0): ?>
|
||||
<div class="m-2 p-3 border border-amber-200 bg-amber-50 text-sm text-amber-900 no-print">
|
||||
<strong><?= esc($baseYmDot) ?></strong> 기준월에 등록된 판매 실적이 없습니다.
|
||||
(지정판매소 <?= number_format($shopCount) ?>곳 · 해당 월 판매 <?= number_format($monthSalesShops) ?>곳)
|
||||
판매 데이터가 있는 월(예: 2026-05)로 기준년월을 바꿔 조회해 보세요.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="m-2 border border-gray-300">
|
||||
<div class="bg-gray-100 px-3 py-1.5 text-sm font-bold border-b">월별 판매 추이 분석 조회 내역</div>
|
||||
<div class="overflow-auto max-h-[28rem]">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>대행소</th>
|
||||
<th class="w-24">판매소번호</th>
|
||||
<th>지정판매소</th>
|
||||
<th class="w-20">성명</th>
|
||||
<th class="w-20 text-right"><?= esc($prevAvgLabel) ?></th>
|
||||
<th class="w-20 text-right">월 판매량</th>
|
||||
<th class="w-20 text-right">평균 차</th>
|
||||
<th class="w-16 text-right">편차(%)</th>
|
||||
<th class="w-24 text-center">지정일자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (! $queried): ?>
|
||||
<tr><td colspan="9" class="text-center text-gray-400 py-8">조회 조건 입력 후 조회</td></tr>
|
||||
<?php elseif ($rows === [] && $shopCount === 0): ?>
|
||||
<tr><td colspan="9" class="text-center text-gray-500 py-8">등록된 지정판매소가 없습니다.</td></tr>
|
||||
<?php elseif ($rows === []): ?>
|
||||
<tr><td colspan="9" class="text-center text-gray-500 py-8">편차 조건(|%|)에 맞는 자료가 없습니다. (0%이면 전체 표시)</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
<tr>
|
||||
<td class="text-left pl-2"><?= esc((string) ($row['agency_name'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['shop_no'] ?? '')) ?></td>
|
||||
<td class="text-left pl-2"><?= esc((string) ($row['shop_name'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['rep_name'] ?? '')) ?></td>
|
||||
<td class="text-right tabular-nums"><?= number_format((int) ($row['prev_avg'] ?? 0)) ?></td>
|
||||
<td class="text-right tabular-nums"><?= number_format((int) ($row['monthly_qty'] ?? 0)) ?></td>
|
||||
<td class="text-right tabular-nums"><?= number_format((int) ($row['avg_diff'] ?? 0)) ?></td>
|
||||
<td class="text-right tabular-nums"><?= esc((string) ($row['deviation_pct'] ?? '0')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['designated_at'] ?? '')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
104
app/Views/bag/analytics_seasonal_trend.php
Normal file
104
app/Views/bag/analytics_seasonal_trend.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
$baseYear = (int) ($baseYear ?? (int) date('Y'));
|
||||
$season = (string) ($season ?? 'spring');
|
||||
$seasonLabel = (string) ($seasonLabel ?? '봄');
|
||||
$seasonMonthsLabel = (string) ($seasonMonthsLabel ?? '');
|
||||
$deviationMin = (float) ($deviationMin ?? 0);
|
||||
$queried = (bool) ($queried ?? false);
|
||||
$filters = is_array($filters ?? null) ? $filters : [];
|
||||
$rows = is_array($rows ?? null) ? $rows : [];
|
||||
$seasonCatalog = \App\Libraries\BagAnalyticsReportBuilder::seasonCatalog();
|
||||
$seasonScope = $seasonMonthsLabel !== ''
|
||||
? $baseYear . '년 ' . $seasonLabel . ' (' . $seasonMonthsLabel . ')'
|
||||
: (string) $baseYear . '년 ' . $seasonLabel;
|
||||
?>
|
||||
<?= view('components/print_header', [
|
||||
'printTitle' => '계절별 판매 추이 분석',
|
||||
'printExtraLines' => ['기준: ' . $seasonScope, '(단위: 매)'],
|
||||
]) ?>
|
||||
|
||||
<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 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">인쇄</button>
|
||||
<a href="<?= base_url('dashboard') ?>" class="border border-gray-400 text-gray-700 px-3 py-1 rounded-sm text-sm">종료</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="p-2 bg-white border-b border-gray-200 no-print text-sm">
|
||||
<form id="seasonal-trend-form" method="get" action="<?= site_url('bag/analytics/seasonal-trend') ?>" class="flex flex-wrap items-end gap-3">
|
||||
<input type="hidden" name="search" value="1"/>
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">기준년도</label>
|
||||
<input type="number" name="base_year" value="<?= (int) $baseYear ?>" min="2000" max="2100" class="border border-gray-300 rounded px-2 py-1 min-w-[7rem] w-28"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">계절선택</label>
|
||||
<select name="season" class="border border-gray-300 rounded px-2 py-1 min-w-[14rem] w-full max-w-[18rem]" onchange="this.form.submit()">
|
||||
<?php foreach ($seasonCatalog as $val => $def): ?>
|
||||
<option value="<?= esc($val, 'attr') ?>" <?= $season === $val ? 'selected' : '' ?>>
|
||||
<?= esc((string) $def['label']) ?> (<?= esc((string) $def['months_label']) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">편차</label>
|
||||
<input type="number" name="deviation_min" value="<?= esc((string) $deviationMin) ?>" step="0.01" min="0" class="border border-gray-300 rounded px-2 py-1 w-28"/>
|
||||
<span class="text-gray-500">% 이상(절대값)</span>
|
||||
</div>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
(단위: 매) · 계절을 바꾸면 자동 조회됩니다.
|
||||
<?php if ($queried && $seasonMonthsLabel !== ''): ?>
|
||||
· 현재: <strong><?= esc($seasonScope) ?></strong> 판매 월평균(3개월 합÷3) vs 전년 동일 계절
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="m-2 border border-gray-300">
|
||||
<div class="bg-gray-100 px-3 py-1.5 text-sm font-bold border-b">
|
||||
계절별 판매 추이 분석 조회 내역
|
||||
<?php if ($queried): ?> — <?= esc($seasonScope) ?><?php endif; ?>
|
||||
</div>
|
||||
<div class="overflow-auto max-h-[28rem]">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>대행소</th>
|
||||
<th>지정판매소</th>
|
||||
<th class="w-24">판매소번호</th>
|
||||
<th class="w-20">성명</th>
|
||||
<th class="w-24 text-right">전년 계절평균</th>
|
||||
<th class="w-24 text-right">기준년 계절평균</th>
|
||||
<th class="w-20 text-right">평균 차</th>
|
||||
<th class="w-16 text-right">편차(%)</th>
|
||||
<th class="w-24 text-center">지정일자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (! $queried): ?>
|
||||
<tr><td colspan="9" class="text-center text-gray-400 py-8">조회 조건 입력 후 조회</td></tr>
|
||||
<?php elseif ($rows === []): ?>
|
||||
<tr><td colspan="9" class="text-center text-gray-500 py-8">편차 조건(|%|)에 맞는 자료가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
<tr>
|
||||
<td class="text-left pl-2"><?= esc((string) ($row['agency_name'] ?? '')) ?></td>
|
||||
<td class="text-left pl-2"><?= esc((string) ($row['shop_name'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['shop_no'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['rep_name'] ?? '')) ?></td>
|
||||
<td class="text-right tabular-nums"><?= number_format((int) ($row['prev_season_avg'] ?? 0)) ?></td>
|
||||
<td class="text-right tabular-nums"><?= number_format((int) ($row['base_season_avg'] ?? 0)) ?></td>
|
||||
<td class="text-right tabular-nums"><?= number_format((int) ($row['avg_diff'] ?? 0)) ?></td>
|
||||
<td class="text-right tabular-nums"><?= esc((string) ($row['deviation_pct'] ?? '0')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['designated_at'] ?? '')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
99
app/Views/bag/analytics_yoy.php
Normal file
99
app/Views/bag/analytics_yoy.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
$year = (int) ($year ?? (int) date('Y'));
|
||||
$queried = (bool) ($queried ?? false);
|
||||
$filters = is_array($filters ?? null) ? $filters : [];
|
||||
$report = is_array($report ?? null) ? $report : [];
|
||||
$lgName = (string) ($lgName ?? '');
|
||||
$rows = is_array($report['rows'] ?? null) ? $report['rows'] : [];
|
||||
$months = is_array($report['months'] ?? null) ? $report['months'] : range(1, 12);
|
||||
$prevYear = (int) ($report['prevYear'] ?? $year - 1);
|
||||
$printExtra = [
|
||||
$lgName !== '' ? $lgName : '',
|
||||
'(단위: 매, 원) ' . $year . '년',
|
||||
];
|
||||
?>
|
||||
<?= view('components/print_header', ['printTitle' => '전년대비 판매 통계분석', 'printExtraLines' => $printExtra]) ?>
|
||||
|
||||
<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 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">인쇄</button>
|
||||
<a href="<?= base_url('dashboard') ?>" class="border border-gray-400 text-gray-700 px-3 py-1 rounded-sm text-sm">종료</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="p-2 bg-white border-b border-gray-200 no-print text-sm">
|
||||
<form method="get" action="<?= site_url('bag/analytics/year-over-year') ?>" class="flex flex-wrap items-end gap-3">
|
||||
<input type="hidden" name="search" value="1"/>
|
||||
<div>
|
||||
<label class="block text-gray-600 mb-0.5">조회년도</label>
|
||||
<input type="number" name="year" value="<?= (int) $year ?>" min="2000" max="2100" class="border border-gray-300 rounded px-2 py-1 w-24"/>
|
||||
</div>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="m-2 border border-gray-300 overflow-auto">
|
||||
<p class="text-center text-sm font-bold py-2 bg-gray-50 border-b">전년대비 판매 통계분석</p>
|
||||
<table class="w-full data-table text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2" class="w-32">품목</th>
|
||||
<th rowspan="2" class="w-14">구분</th>
|
||||
<th rowspan="2" class="w-12">년도</th>
|
||||
<th rowspan="2" class="w-16">계</th>
|
||||
<?php foreach ($months as $mo): ?>
|
||||
<th class="w-14"><?= (int) $mo ?>월</th>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-right">
|
||||
<?php if ($queried && $rows === []): ?>
|
||||
<tr><td colspan="<?= 4 + count($months) ?>" class="text-center text-gray-500 py-6">해당 자료가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$byProduct = [];
|
||||
foreach ($rows as $block) {
|
||||
$code = (string) ($block['bag_code'] ?? '');
|
||||
if (! isset($byProduct[$code])) {
|
||||
$byProduct[$code] = ['name' => (string) ($block['bag_name'] ?? ''), 'blocks' => []];
|
||||
}
|
||||
$byProduct[$code]['blocks'][] = $block;
|
||||
}
|
||||
foreach ($byProduct as $product):
|
||||
$productRowspan = 0;
|
||||
foreach ($product['blocks'] as $b) {
|
||||
$productRowspan += count($b['lines'] ?? []);
|
||||
}
|
||||
$printedProduct = false;
|
||||
foreach ($product['blocks'] as $block):
|
||||
$sectionRows = count($block['lines'] ?? []);
|
||||
$sectionPrinted = false;
|
||||
foreach ($block['lines'] ?? [] as $line):
|
||||
?>
|
||||
<tr>
|
||||
<?php if (! $printedProduct): ?>
|
||||
<td class="text-left pl-2 font-medium" rowspan="<?= (int) $productRowspan ?>"><?= esc($product['name']) ?></td>
|
||||
<?php $printedProduct = true; endif; ?>
|
||||
<?php if (! $sectionPrinted): ?>
|
||||
<td class="text-center" rowspan="<?= (int) $sectionRows ?>"><?= esc((string) ($block['section'] ?? '')) ?></td>
|
||||
<?php $sectionPrinted = true; endif; ?>
|
||||
<td class="text-center"><?= esc((string) ($line['label'] ?? '')) ?></td>
|
||||
<td class="tabular-nums font-semibold"><?= number_format((int) ($line['total'] ?? 0)) ?></td>
|
||||
<?php foreach ($months as $mo): ?>
|
||||
<td class="tabular-nums <?= ($line['label'] ?? '') === '증감' && (int) (($line['months'][$mo] ?? 0)) < 0 ? 'text-red-600' : '' ?>">
|
||||
<?= number_format((int) (($line['months'][$mo] ?? 0))) ?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
<?php
|
||||
endforeach;
|
||||
endforeach;
|
||||
endforeach;
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -6,6 +6,10 @@
|
||||
$canManage = ! empty($canManage);
|
||||
$rowCanEdit = $rowCanEdit ?? [];
|
||||
$showActionsCol = $canManage;
|
||||
$rowStartNo = 1;
|
||||
if (isset($pager) && $pager !== null) {
|
||||
$rowStartNo = (int) ($pager->getCurrentPage() - 1) * (int) $pager->getPerPage() + 1;
|
||||
}
|
||||
?>
|
||||
<div class="space-y-3">
|
||||
<?= view('components/print_header', ['printTitle' => '세부코드 - ' . esc($kind->ck_name)]) ?>
|
||||
@@ -41,13 +45,13 @@ $showActionsCol = $canManage;
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($list as $row): ?>
|
||||
<?php $rowNo = $rowStartNo; foreach ($list as $row): ?>
|
||||
<?php
|
||||
$isPlatform = (($row->cd_source ?? 'platform') === 'platform' && (int) ($row->cd_lg_idx ?? 0) === 0);
|
||||
$scopeLabel = $isPlatform ? '공통' : '지자체';
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center"><?= esc((string) $row->cd_idx) ?></td>
|
||||
<td class="text-center"><?= (string) $rowNo ?></td>
|
||||
<td class="text-center font-mono"><?= esc($row->cd_code) ?></td>
|
||||
<td><?= esc($row->cd_name) ?></td>
|
||||
<td class="text-center text-xs"><?= esc($scopeLabel) ?></td>
|
||||
@@ -68,6 +72,7 @@ $showActionsCol = $canManage;
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php $rowNo++; ?>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -19,7 +19,7 @@ $detailColCount = 7 + ($canManageDetails ? 1 : 0);
|
||||
<h3 class="text-base font-bold text-gray-700">기본코드 종류</h3>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs sm:text-sm">
|
||||
<?php if ($canManageKinds): ?>
|
||||
<a href="<?= base_url('admin/code-kinds/create') ?>" class="inline-flex items-center rounded bg-[#1c4e80] px-3 py-1.5 text-white shadow hover:opacity-90">코드 종류 등록</a>
|
||||
<a href="<?= base_url('admin/code-kinds/create') ?>" class="inline-flex items-center rounded bg-[#1c4e80] px-3 py-1.5 text-white shadow hover:opacity-90">기본코드 등록</a>
|
||||
<?php else: ?>
|
||||
<span class="text-gray-500">코드 종류 등록·수정은 super admin·본부 관리자만 가능합니다.</span>
|
||||
<?php endif; ?>
|
||||
@@ -28,10 +28,10 @@ $detailColCount = 7 + ($canManageDetails ? 1 : 0);
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="data-table w-full">
|
||||
<thead><tr>
|
||||
<th class="w-14"><?= $showKindActions ? 'PK' : '번호' ?></th>
|
||||
<th class="w-14">번호</th>
|
||||
<th class="w-24">코드</th>
|
||||
<th>코드명</th>
|
||||
<th class="w-24">세부건수</th>
|
||||
<th class="w-24">세부코드</th>
|
||||
<th class="w-20">상태</th>
|
||||
<th class="w-40">등록일</th>
|
||||
<?php if ($showKindActions): ?>
|
||||
@@ -47,7 +47,7 @@ $detailColCount = 7 + ($canManageDetails ? 1 : 0);
|
||||
?>
|
||||
<tr class="<?= $isSelected ? 'bg-blue-50' : '' ?> cursor-pointer hover:bg-blue-50"
|
||||
onclick="window.location.href='<?= esc($detailUrl, 'attr') ?>'">
|
||||
<td class="text-center"><?= $showKindActions ? esc((string) $row->ck_idx) : (string) $i ?></td>
|
||||
<td class="text-center"><?= (string) $i ?></td>
|
||||
<td class="text-center font-mono"><?= esc($row->ck_code) ?></td>
|
||||
<td><?= esc($row->ck_name) ?></td>
|
||||
<td class="text-center"><?= (int) ($countMap[$row->ck_idx] ?? 0) ?>개</td>
|
||||
@@ -106,13 +106,13 @@ $detailColCount = 7 + ($canManageDetails ? 1 : 0);
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (! empty($detailList)): ?>
|
||||
<?php foreach ($detailList as $row): ?>
|
||||
<?php $dNo = 0; foreach ($detailList as $row): $dNo++; ?>
|
||||
<?php
|
||||
$isPlatform = (($row->cd_source ?? 'platform') === 'platform' && (int) ($row->cd_lg_idx ?? 0) === 0);
|
||||
$scopeLabel = $isPlatform ? '공통' : '지자체';
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center"><?= esc((string) $row->cd_idx) ?></td>
|
||||
<td class="text-center"><?= (string) $dNo ?></td>
|
||||
<td class="text-center font-mono"><?= esc($row->cd_code) ?></td>
|
||||
<td><?= esc($row->cd_name) ?></td>
|
||||
<td class="text-center text-xs"><?= esc($scopeLabel) ?></td>
|
||||
|
||||
@@ -1,70 +1,482 @@
|
||||
<?php
|
||||
$bagMeta = is_array($bagMeta ?? null) ? $bagMeta : [];
|
||||
$inventoryMap = is_array($inventoryMap ?? null) ? $inventoryMap : [];
|
||||
$availableBagRows = is_array($availableBagRows ?? null) ? $availableBagRows : [];
|
||||
$recentIssueRows = is_array($recentIssueRows ?? null) ? $recentIssueRows : [];
|
||||
$dongCodes = is_array($dongCodes ?? null) ? $dongCodes : [];
|
||||
$freeDongSet = is_array($freeDongSet ?? null) ? $freeDongSet : [];
|
||||
$destTypeOptions = is_array($destTypeOptions ?? null) ? $destTypeOptions : ['동사무소', '구청', '기타'];
|
||||
$defaultDestType = (string) old('bi2_dest_type', (string) ($destTypeOptions[0] ?? '구청'));
|
||||
|
||||
$oldItemCodes = old('item_bag_code');
|
||||
$oldItemQtys = old('item_qty');
|
||||
$oldItemPacks = old('item_pack');
|
||||
$oldItemCodes = is_array($oldItemCodes) ? $oldItemCodes : [];
|
||||
$oldItemQtys = is_array($oldItemQtys) ? $oldItemQtys : [];
|
||||
$oldItemPacks = is_array($oldItemPacks) ? $oldItemPacks : [];
|
||||
|
||||
$initialRows = [];
|
||||
$oldCount = max(count($oldItemCodes), count($oldItemQtys), count($oldItemPacks));
|
||||
for ($i = 0; $i < $oldCount; $i++) {
|
||||
$initialRows[] = [
|
||||
'code' => trim((string) ($oldItemCodes[$i] ?? '')),
|
||||
'qty' => max(0, (int) ($oldItemQtys[$i] ?? 0)),
|
||||
'pack' => (string) ($oldItemPacks[$i] ?? 'sheet'),
|
||||
];
|
||||
}
|
||||
if ($initialRows === []) {
|
||||
$initialRows[] = ['code' => '', 'qty' => 0, 'pack' => 'sheet'];
|
||||
}
|
||||
?>
|
||||
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">무료용 불출 처리</span>
|
||||
</section>
|
||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||
<form action="<?= base_url('bag/issue/store') ?>" method="POST" class="space-y-4">
|
||||
|
||||
<div class="border border-gray-300 p-3 mt-2 bg-white">
|
||||
<form action="<?= base_url('bag/issue/store') ?>" method="POST" class="space-y-3" id="bag-issue-form">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">연도 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bi2_year" type="number" min="2000" max="2099" value="<?= esc(old('bi2_year', date('Y'))) ?>" required/>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-6 gap-2 text-sm">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출년도</label>
|
||||
<input class="border border-gray-300 rounded px-2 py-1 w-24 max-w-full text-right" name="bi2_year" type="number" min="2000" max="2099" value="<?= esc(old('bi2_year', date('Y'))) ?>" required/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">분기</label>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 w-24 max-w-full" name="bi2_quarter" required>
|
||||
<option value="1" <?= old('bi2_quarter', (string) ceil((int) date('n') / 3)) === '1' ? 'selected' : '' ?>>1/4</option>
|
||||
<option value="2" <?= old('bi2_quarter', (string) ceil((int) date('n') / 3)) === '2' ? 'selected' : '' ?>>2/4</option>
|
||||
<option value="3" <?= old('bi2_quarter', (string) ceil((int) date('n') / 3)) === '3' ? 'selected' : '' ?>>3/4</option>
|
||||
<option value="4" <?= old('bi2_quarter', (string) ceil((int) date('n') / 3)) === '4' ? 'selected' : '' ?>>4/4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출구분</label>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 w-28 max-w-full" name="bi2_issue_type" id="bi2_issue_type" required>
|
||||
<option value="무료용" <?= old('bi2_issue_type', '무료용') === '무료용' ? 'selected' : '' ?>>무료용</option>
|
||||
<option value="공공용" <?= old('bi2_issue_type') === '공공용' ? 'selected' : '' ?>>공공용</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출일</label>
|
||||
<input class="border border-gray-300 rounded px-2 py-1 w-36 max-w-full" name="bi2_issue_date" type="date" value="<?= esc(old('bi2_issue_date', date('Y-m-d'))) ?>" required/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출처구분</label>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 w-28 max-w-full" name="bi2_dest_type" id="bi2_dest_type">
|
||||
<?php foreach ($destTypeOptions as $option): ?>
|
||||
<option value="<?= esc($option) ?>" <?= $defaultDestType === $option ? 'selected' : '' ?>><?= esc($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-w-0 md:col-span-2 xl:col-span-2">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">불출처(동)</label>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 w-full min-w-[20rem]" id="bi2_dest_name" name="bi2_dest_name" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($dongCodes as $dong): ?>
|
||||
<?php $dCode = (string) ($dong->cd_code ?? ''); ?>
|
||||
<?php $dName = (string) ($dong->cd_name ?? $dCode); ?>
|
||||
<?php $hasFree = isset($freeDongSet[$dCode]); ?>
|
||||
<?php $oldDest = (string) old('bi2_dest_name'); ?>
|
||||
<option
|
||||
value="<?= esc($dName) ?>"
|
||||
data-dong-code="<?= esc($dCode) ?>"
|
||||
data-has-free="<?= $hasFree ? '1' : '0' ?>"
|
||||
<?= $oldDest === $dName ? 'selected' : '' ?>
|
||||
>
|
||||
<?= esc($dName) ?><?= $hasFree ? ' (무료용 가능)' : ' (무료용 없음)' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input type="hidden" name="bi2_dest_dong_code" id="bi2_dest_dong_code" value="<?= esc((string) old('bi2_dest_dong_code')) ?>" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">분기 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="bi2_quarter" required>
|
||||
<option value="">선택</option>
|
||||
<option value="1" <?= old('bi2_quarter') === '1' ? 'selected' : '' ?>>1</option>
|
||||
<option value="2" <?= old('bi2_quarter') === '2' ? 'selected' : '' ?>>2</option>
|
||||
<option value="3" <?= old('bi2_quarter') === '3' ? 'selected' : '' ?>>3</option>
|
||||
<option value="4" <?= old('bi2_quarter') === '4' ? 'selected' : '' ?>>4</option>
|
||||
</select>
|
||||
<div class="border border-gray-300 rounded p-2 bg-gray-50 space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||||
<label class="font-bold text-gray-700">바코드 스캔</label>
|
||||
<input
|
||||
id="barcode_input"
|
||||
type="text"
|
||||
class="border border-gray-300 rounded px-2 py-1 w-72"
|
||||
placeholder="스캐너로 바코드를 입력 후 Enter"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button type="button" id="add-row-btn" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm hover:bg-white">행 추가</button>
|
||||
<span class="text-xs text-gray-500">동일 바코드 연속 스캔은 무시됩니다.</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">
|
||||
입고 재고가 있는 봉투/스티커만 불출 가능합니다. 저장 시 포장단위가 낱장으로 환산되어 재고가 차감됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">구분 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_issue_type" required>
|
||||
<option value="">선택</option>
|
||||
<option value="무료용" <?= old('bi2_issue_type') === '무료용' ? 'selected' : '' ?>>무료용</option>
|
||||
<option value="공공용" <?= old('bi2_issue_type') === '공공용' ? 'selected' : '' ?>>공공용</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">불출일 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_issue_date" type="date" value="<?= esc(old('bi2_issue_date', date('Y-m-d'))) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">불출처 유형</label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bi2_dest_type" type="text" placeholder="동사무소" value="<?= esc(old('bi2_dest_type')) ?>"/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">불출처명 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bi2_dest_name" type="text" value="<?= esc(old('bi2_dest_name')) ?>" required/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">봉투코드 <span class="text-red-500">*</span></label>
|
||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bi2_bag_code" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $cd): ?>
|
||||
<option value="<?= esc($cd->cd_code) ?>" <?= old('bi2_bag_code') === $cd->cd_code ? 'selected' : '' ?>>
|
||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">수량 <span class="text-red-500">*</span></label>
|
||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bi2_qty" type="number" min="0" value="<?= esc(old('bi2_qty', '0')) ?>" required/>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table text-sm" id="issue-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">No</th>
|
||||
<th class="w-44">봉투코드</th>
|
||||
<th>봉투종류</th>
|
||||
<th class="w-28">수량</th>
|
||||
<th class="w-28">포장</th>
|
||||
<th class="w-32">재고(낱장)</th>
|
||||
<th class="w-36">환산(낱장)</th>
|
||||
<th class="w-20">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="issue-item-body"></tbody>
|
||||
<tfoot>
|
||||
<tr class="bg-gray-50 font-semibold">
|
||||
<td colspan="6" class="text-right pr-2">합계(환산 낱장)</td>
|
||||
<td class="text-right pr-2" id="sum_sheet_qty">0</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
|
||||
<a href="<?= base_url('bag/issue') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">저장</button>
|
||||
<a href="<?= base_url('bag/issue/cancel') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-2 mt-2">
|
||||
<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-[300px]">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12">No</th>
|
||||
<th class="w-28">봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th class="w-24">재고(낱장)</th>
|
||||
<th class="w-24">팩당 낱장</th>
|
||||
<th class="w-24">박스당 낱장</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($availableBagRows !== []): ?>
|
||||
<?php foreach ($availableBagRows as $idx => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $idx + 1 ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['bag_code'] ?? '')) ?></td>
|
||||
<td class="pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($row['inventory_qty'] ?? 0)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($row['pack_per_sheet'] ?? 1)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($row['total_per_box'] ?? 1)) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-gray-400 py-4">불출 가능한 재고가 없습니다.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</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-[300px]">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12">No</th>
|
||||
<th class="w-24">불출일</th>
|
||||
<th class="w-20">구분</th>
|
||||
<th class="w-28">불출처</th>
|
||||
<th class="w-24">봉투코드</th>
|
||||
<th>봉투명</th>
|
||||
<th class="w-24">수량(낱장)</th>
|
||||
<th class="w-16">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($recentIssueRows !== []): ?>
|
||||
<?php foreach ($recentIssueRows as $idx => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $idx + 1 ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row->bi2_issue_date ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row->bi2_issue_type ?? '')) ?></td>
|
||||
<td class="pl-2"><?= esc((string) ($row->bi2_dest_name ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row->bi2_bag_code ?? '')) ?></td>
|
||||
<td class="pl-2"><?= esc((string) ($row->bi2_bag_name ?? '')) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($row->bi2_qty ?? 0)) ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ((string) ($row->bi2_status ?? 'normal') === 'cancelled'): ?>
|
||||
<span class="text-orange-600">취소</span>
|
||||
<?php else: ?>
|
||||
정상
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-gray-400 py-4">최근 불출 내역이 없습니다.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const bagMeta = <?= json_encode($bagMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const initialRows = <?= json_encode($initialRows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const knownCodes = Object.keys(bagMeta);
|
||||
const body = document.getElementById('issue-item-body');
|
||||
const sumSheetQtyEl = document.getElementById('sum_sheet_qty');
|
||||
const barcodeInput = document.getElementById('barcode_input');
|
||||
const addRowBtn = document.getElementById('add-row-btn');
|
||||
const issueTypeEl = document.getElementById('bi2_issue_type');
|
||||
const destTypeEl = document.getElementById('bi2_dest_type');
|
||||
const destNameEl = document.getElementById('bi2_dest_name');
|
||||
const destDongCodeEl = document.getElementById('bi2_dest_dong_code');
|
||||
const form = document.getElementById('bag-issue-form');
|
||||
let lastScannedCode = '';
|
||||
const rows = [];
|
||||
|
||||
const createBagTypeOptions = (selectedCode) => {
|
||||
const opts = ['<option value="">선택</option>'];
|
||||
knownCodes.forEach((code) => {
|
||||
const name = bagMeta[code]?.name || code;
|
||||
opts.push(`<option value="${code}" ${code === selectedCode ? 'selected' : ''}>${code} - ${name}</option>`);
|
||||
});
|
||||
return opts.join('');
|
||||
};
|
||||
|
||||
const resolveBagCode = (raw) => {
|
||||
const src = String(raw || '').trim();
|
||||
if (src === '') return '';
|
||||
if (bagMeta[src]) return src;
|
||||
|
||||
const noSpace = src.replace(/\s+/g, '');
|
||||
if (bagMeta[noSpace]) return noSpace;
|
||||
|
||||
const compact = noSpace.replace(/[^0-9A-Za-z]/g, '');
|
||||
if (bagMeta[compact]) return compact;
|
||||
|
||||
for (const code of knownCodes) {
|
||||
if (compact.includes(code)) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const toSheetQty = (code, qty, pack) => {
|
||||
const n = Math.max(0, parseInt(String(qty || 0), 10) || 0);
|
||||
const meta = bagMeta[code] || { packPerSheet: 1, totalPerBox: 1 };
|
||||
if (pack === 'box') return n * Math.max(1, parseInt(meta.totalPerBox, 10) || 1);
|
||||
if (pack === 'pack') return n * Math.max(1, parseInt(meta.packPerSheet, 10) || 1);
|
||||
return n;
|
||||
};
|
||||
|
||||
const recompute = () => {
|
||||
let sum = 0;
|
||||
rows.forEach((row, idx) => {
|
||||
const tr = row.tr;
|
||||
tr.querySelector('.col-no').textContent = String(idx + 1);
|
||||
const code = row.codeInput.value.trim();
|
||||
const qty = row.qtyInput.value;
|
||||
const pack = row.packSelect.value;
|
||||
const sheetQty = toSheetQty(code, qty, pack);
|
||||
const invQty = parseInt((bagMeta[code]?.inventoryQty || 0), 10) || 0;
|
||||
row.invCell.textContent = new Intl.NumberFormat('ko-KR').format(invQty);
|
||||
row.sheetCell.textContent = new Intl.NumberFormat('ko-KR').format(sheetQty);
|
||||
if (code && sheetQty > invQty) {
|
||||
row.sheetCell.classList.add('text-red-600', 'font-semibold');
|
||||
} else {
|
||||
row.sheetCell.classList.remove('text-red-600', 'font-semibold');
|
||||
}
|
||||
sum += sheetQty;
|
||||
});
|
||||
sumSheetQtyEl.textContent = new Intl.NumberFormat('ko-KR').format(sum);
|
||||
};
|
||||
|
||||
const syncCode = (row, code, setAsUserSelection = false) => {
|
||||
const resolved = resolveBagCode(code);
|
||||
if (resolved === '') return false;
|
||||
row.codeInput.value = resolved;
|
||||
row.typeSelect.value = resolved;
|
||||
if (setAsUserSelection || row.packSelect.value === '') {
|
||||
row.packSelect.value = (bagMeta[resolved]?.totalPerBox || 1) > 1 ? 'box' : 'sheet';
|
||||
}
|
||||
recompute();
|
||||
return true;
|
||||
};
|
||||
|
||||
const addRow = (seed = {}) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td class="text-center col-no"></td>
|
||||
<td class="px-1 py-1">
|
||||
<input type="text" name="item_bag_code[]" class="code-input border border-gray-300 rounded px-2 py-1 w-full text-sm" value="" placeholder="봉투코드"/>
|
||||
</td>
|
||||
<td class="px-1 py-1">
|
||||
<select name="item_bag_type[]" class="type-select border border-gray-300 rounded px-2 py-1 w-full text-sm">
|
||||
${createBagTypeOptions('')}
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-1 py-1">
|
||||
<input type="number" min="0" step="1" name="item_qty[]" class="qty-input border border-gray-300 rounded px-2 py-1 w-full text-sm text-right" value="0"/>
|
||||
</td>
|
||||
<td class="px-1 py-1">
|
||||
<select name="item_pack[]" class="pack-select border border-gray-300 rounded px-2 py-1 w-full text-sm">
|
||||
<option value="box">박스</option>
|
||||
<option value="pack">팩</option>
|
||||
<option value="sheet">낱장</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-right pr-2 inv-cell">0</td>
|
||||
<td class="text-right pr-2 sheet-cell">0</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="remove-btn text-red-600 hover:underline text-xs">삭제</button>
|
||||
</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
const row = {
|
||||
tr,
|
||||
codeInput: tr.querySelector('.code-input'),
|
||||
typeSelect: tr.querySelector('.type-select'),
|
||||
qtyInput: tr.querySelector('.qty-input'),
|
||||
packSelect: tr.querySelector('.pack-select'),
|
||||
invCell: tr.querySelector('.inv-cell'),
|
||||
sheetCell: tr.querySelector('.sheet-cell'),
|
||||
removeBtn: tr.querySelector('.remove-btn'),
|
||||
};
|
||||
rows.push(row);
|
||||
|
||||
row.codeInput.addEventListener('change', () => {
|
||||
const ok = syncCode(row, row.codeInput.value, true);
|
||||
if (!ok) {
|
||||
row.codeInput.value = '';
|
||||
row.typeSelect.value = '';
|
||||
alert('입고 재고가 있는 봉투코드만 입력할 수 있습니다.');
|
||||
}
|
||||
recompute();
|
||||
});
|
||||
row.typeSelect.addEventListener('change', () => {
|
||||
row.codeInput.value = row.typeSelect.value;
|
||||
recompute();
|
||||
});
|
||||
row.qtyInput.addEventListener('input', recompute);
|
||||
row.packSelect.addEventListener('change', recompute);
|
||||
row.removeBtn.addEventListener('click', () => {
|
||||
if (rows.length <= 1) return;
|
||||
tr.remove();
|
||||
const i = rows.indexOf(row);
|
||||
if (i >= 0) rows.splice(i, 1);
|
||||
recompute();
|
||||
});
|
||||
|
||||
if (seed.code) {
|
||||
syncCode(row, seed.code, true);
|
||||
}
|
||||
row.qtyInput.value = String(Math.max(0, parseInt(String(seed.qty || 0), 10) || 0));
|
||||
if (['box', 'pack', 'sheet'].includes(String(seed.pack || ''))) {
|
||||
row.packSelect.value = String(seed.pack);
|
||||
}
|
||||
recompute();
|
||||
return row;
|
||||
};
|
||||
|
||||
const appendScannedRow = (code) => {
|
||||
const row = addRow({ code, qty: 1, pack: (bagMeta[code]?.totalPerBox || 1) > 1 ? 'box' : 'sheet' });
|
||||
row.qtyInput.focus();
|
||||
row.qtyInput.select();
|
||||
};
|
||||
|
||||
const syncDestDongCode = () => {
|
||||
const opt = destNameEl.options[destNameEl.selectedIndex];
|
||||
if (!opt) {
|
||||
destDongCodeEl.value = '';
|
||||
return;
|
||||
}
|
||||
destDongCodeEl.value = opt.getAttribute('data-dong-code') || '';
|
||||
};
|
||||
|
||||
const updateIssueTypeUi = () => {
|
||||
const isPublic = issueTypeEl.value === '공공용';
|
||||
if (isPublic) {
|
||||
destTypeEl.value = '구청';
|
||||
}
|
||||
};
|
||||
|
||||
addRowBtn.addEventListener('click', () => addRow({ code: '', qty: 0, pack: 'sheet' }));
|
||||
|
||||
barcodeInput.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Enter') return;
|
||||
event.preventDefault();
|
||||
const resolved = resolveBagCode(barcodeInput.value);
|
||||
barcodeInput.value = '';
|
||||
if (!resolved) {
|
||||
alert('인식 가능한 봉투코드가 아닙니다.');
|
||||
return;
|
||||
}
|
||||
if (resolved === lastScannedCode) {
|
||||
return;
|
||||
}
|
||||
lastScannedCode = resolved;
|
||||
appendScannedRow(resolved);
|
||||
});
|
||||
|
||||
destNameEl.addEventListener('change', syncDestDongCode);
|
||||
issueTypeEl.addEventListener('change', updateIssueTypeUi);
|
||||
|
||||
form.addEventListener('submit', (event) => {
|
||||
syncDestDongCode();
|
||||
updateIssueTypeUi();
|
||||
|
||||
const validRows = rows.filter((row) => {
|
||||
const code = resolveBagCode(row.codeInput.value);
|
||||
const qty = parseInt(row.qtyInput.value || '0', 10) || 0;
|
||||
return code !== '' && qty > 0;
|
||||
});
|
||||
if (validRows.length === 0) {
|
||||
event.preventDefault();
|
||||
alert('불출 품목을 1건 이상 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of validRows) {
|
||||
const code = resolveBagCode(row.codeInput.value);
|
||||
const sheetQty = toSheetQty(code, row.qtyInput.value, row.packSelect.value);
|
||||
const inv = parseInt((bagMeta[code]?.inventoryQty || 0), 10) || 0;
|
||||
if (sheetQty > inv) {
|
||||
event.preventDefault();
|
||||
alert(`재고 부족: ${code} (재고 ${inv}, 요청 ${sheetQty})`);
|
||||
return;
|
||||
}
|
||||
row.codeInput.value = code;
|
||||
row.typeSelect.value = code;
|
||||
}
|
||||
|
||||
if (issueTypeEl.value === '무료용') {
|
||||
const selected = destNameEl.options[destNameEl.selectedIndex];
|
||||
const hasFree = selected ? selected.getAttribute('data-has-free') === '1' : false;
|
||||
if (!hasFree) {
|
||||
event.preventDefault();
|
||||
alert('무료용 불출은 "무료용 가능" 불출처(동)만 선택할 수 있습니다.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
initialRows.forEach((row) => addRow(row));
|
||||
if (rows.length > 1 && !initialRows[0]?.code && !initialRows[0]?.qty) {
|
||||
rows[0].tr.remove();
|
||||
rows.shift();
|
||||
}
|
||||
syncDestDongCode();
|
||||
updateIssueTypeUi();
|
||||
recompute();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -17,6 +17,7 @@ $userNav = session_user_nav_display();
|
||||
<title>종량제 시스템</title>
|
||||
<!-- Tailwind CSS v3 with Plugins -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<style data-purpose="global-font-scale">html{font-size:18px}.app-brand,.app-brand *{font-size:16px}</style>
|
||||
<!-- Font: Noto Sans KR -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||
<!-- Tailwind Configuration for Custom Colors and Fonts -->
|
||||
|
||||
332
app/Views/bag/dashboard_blend_lite_inner.php
Normal file
332
app/Views/bag/dashboard_blend_lite_inner.php
Normal file
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
/**
|
||||
* 라이트(축약) 대시보드 본문 — `bag/layout/main`에 삽입.
|
||||
*
|
||||
* `dashboard_blend_inner` 기반으로 다음을 제거:
|
||||
* - KPI 카드: 「회원승인 대기」, 「지정판매소 등록」
|
||||
* - 상단 표 그리드 중: 「최근 이벤트 로그」(주간 스파크 포함)
|
||||
* - 차트 중: 도넛/주간 막대/레이더/분기 스택/요일 폴라/누적 영역
|
||||
* - 하단 보조 영역: 지정판매소 요약·승인 대기 표·운영 브리핑
|
||||
* 남기는 그래프 3종:
|
||||
* 1) 월별 출고 vs 구매신청 건수 (최근 12개월)
|
||||
* 2) 품목별 재고 (천 장)
|
||||
* 3) 판매소별 월 출고 TOP
|
||||
*
|
||||
* @var string $lgLabel
|
||||
*/
|
||||
$lgLabel = $lgLabel ?? '북구';
|
||||
$mbName = session()->get('mb_name') ?? '담당자';
|
||||
$dashHome = base_url('dashboard');
|
||||
$dashBlend = base_url('dashboard/blend');
|
||||
$dashLite = base_url('dashboard/lite');
|
||||
|
||||
// KPI: 회원승인/지정판매소 등록을 제외한 6칸.
|
||||
$kpiTop = [
|
||||
['icon' => 'fa-triangle-exclamation', 'c' => 'text-amber-700', 'bg' => 'bg-amber-50', 'v' => '3', 'l' => '재고부족', 'sub' => '품목'],
|
||||
['icon' => 'fa-cart-shopping', 'c' => 'text-sky-700', 'bg' => 'bg-sky-50', 'v' => '12', 'l' => '구매신청', 'sub' => '미처리'],
|
||||
['icon' => 'fa-truck', 'c' => 'text-emerald-700', 'bg' => 'bg-emerald-50', 'v' => '8', 'l' => '발주·입고', 'sub' => '금주'],
|
||||
['icon' => 'fa-boxes-stacked', 'c' => 'text-slate-700', 'bg' => 'bg-slate-100', 'v' => '48.2k', 'l' => '봉투재고', 'sub' => '장 합계'],
|
||||
['icon' => 'fa-file-invoice', 'c' => 'text-orange-700', 'bg' => 'bg-orange-50', 'v' => '6', 'l' => '세금계산서', 'sub' => '발행대기'],
|
||||
['icon' => 'fa-headset', 'c' => 'text-cyan-700', 'bg' => 'bg-cyan-50', 'v' => '2', 'l' => '민원·문의', 'sub' => '오늘'],
|
||||
];
|
||||
|
||||
$stockRows = [
|
||||
['일반 5L', '12,400', '안전', '3.2주'],
|
||||
['일반 10L', '8,200', '주의', '1.8주'],
|
||||
['일반 20L', '2,100', '부족', '0.6주'],
|
||||
['음식물 스티커', '15,000', '안전', '5.1주'],
|
||||
['재사용봉투', '4,300', '안전', '2.4주'],
|
||||
['특수규격 A', '890', '부족', '0.3주'],
|
||||
];
|
||||
|
||||
$orderRows = [
|
||||
['PO-2025-0218', '○○상사', '일반 5L×2박스', '발주확인', '02-26 10:20'],
|
||||
['PO-2025-0217', '△△유통', '스티커 500매', '납품중', '02-26 09:05'],
|
||||
['PO-2025-0216', '□□종량제', '20L 혼합', '입고완료', '02-25 16:40'],
|
||||
['REQ-8841', '행복마트 북구점', '5L 2,000장', '접수', '02-26 09:12'],
|
||||
['REQ-8839', '○○슈퍼', '스티커 500', '처리중', '02-26 08:45'],
|
||||
];
|
||||
|
||||
$notices = [
|
||||
'2월 말 정기 재고 실사 안내 — 2/28 17:00 마감',
|
||||
'봉투 단가 조정 예고 — 3/1 적용 예정 (안내문 배포 완료)',
|
||||
];
|
||||
?>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
.blend-dash-lite {
|
||||
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
.blend-dash-lite .dense-table th, .blend-dash-lite .dense-table td { padding: 0.25rem 0.4rem; line-height: 1.25; }
|
||||
.blend-dash-lite .dense-table thead th { font-size: 11px; font-weight: 600; color: #555; background: #f3f4f6; border-bottom: 1px solid #d1d5db; }
|
||||
.blend-dash-lite .dense-table tbody td { border-bottom: 1px solid #eee; font-size: 11px; }
|
||||
.blend-dash-lite .chart-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.04);
|
||||
}
|
||||
.blend-dash-lite .chart-card h2 {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
background: #fafafa;
|
||||
}
|
||||
.blend-dash-lite .chart-wrap { position: relative; height: 220px; padding: 0.4rem 0.5rem 0.5rem; }
|
||||
.blend-dash-lite .chart-wrap.tall { height: 280px; }
|
||||
</style>
|
||||
|
||||
<div class="blend-dash-lite bg-[#f0f2f5] -mx-4 -my-4 p-2 sm:p-3 min-h-full">
|
||||
<div class="bg-gradient-to-r from-[#eff5fb] to-[#e8eef8] border border-gray-300 rounded-sm px-3 py-1 flex flex-wrap items-center justify-between gap-2 text-[11px] mb-2">
|
||||
<span class="font-semibold text-gray-800">
|
||||
<i class="fa-solid fa-gauge-simple-high text-[#2b4c8c] mr-1"></i>업무 현황 · 라이트
|
||||
<span class="font-normal text-gray-500 ml-1">· 핵심 KPI·표 + 그래프 3종</span>
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center gap-2 text-gray-600">
|
||||
<span><i class="fa-regular fa-calendar mr-0.5"></i><?= date('Y-m-d (D) H:i') ?></span>
|
||||
<span class="text-gray-300">|</span>
|
||||
<span><?= esc($lgLabel) ?> · <strong class="text-gray-800"><?= esc($mbName) ?></strong></span>
|
||||
<button type="button" class="bg-[#2b4c8c] text-white px-2 py-0.5 rounded text-[11px]" onclick="location.reload()"><i class="fa-solid fa-rotate mr-0.5"></i>새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="mb-2 flex flex-wrap gap-2">
|
||||
<?php foreach ($notices as $n): ?>
|
||||
<div class="flex-1 min-w-[200px] flex items-center gap-2 bg-amber-50 border border-amber-200 text-amber-900 px-2 py-1 rounded text-[11px]">
|
||||
<i class="fa-solid fa-bullhorn shrink-0"></i>
|
||||
<span class="truncate" title="<?= esc($n) ?>"><?= esc($n) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-1.5 mb-2">
|
||||
<?php foreach ($kpiTop as $k): ?>
|
||||
<div class="bg-white border border-gray-200 rounded px-2 py-1.5 flex items-center gap-2 shadow-sm">
|
||||
<div class="w-8 h-8 rounded <?= $k['bg'] ?> <?= $k['c'] ?> flex items-center justify-center shrink-0 text-sm">
|
||||
<i class="fa-solid <?= esc($k['icon'], 'attr') ?>"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-base font-bold text-gray-900 leading-tight"><?= esc($k['v']) ?></div>
|
||||
<div class="text-[10px] text-gray-500 leading-tight"><?= esc($k['l']) ?></div>
|
||||
<div class="text-[9px] text-gray-400"><?= esc($k['sub']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-2 mb-2">
|
||||
<section class="bg-white border border-gray-200 rounded shadow-sm overflow-hidden">
|
||||
<div class="px-2 py-1 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
||||
<h2 class="text-[11px] font-bold text-gray-800"><i class="fa-solid fa-warehouse text-[#2b4c8c] mr-1"></i>품목별 재고·소진예상</h2>
|
||||
<a href="<?= base_url('bag/inventory-inquiry') ?>" class="text-[10px] text-blue-600 hover:underline">상세</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[220px] overflow-y-auto">
|
||||
<table class="w-full dense-table text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>품목</th>
|
||||
<th class="text-right">재고(장)</th>
|
||||
<th>상태</th>
|
||||
<th class="text-right">소진</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($stockRows as $r): ?>
|
||||
<tr>
|
||||
<td class="font-medium text-gray-800"><?= esc($r[0]) ?></td>
|
||||
<td class="text-right tabular-nums"><?= esc($r[1]) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$badge = match ($r[2]) {
|
||||
'안전' => 'bg-emerald-100 text-emerald-800',
|
||||
'주의' => 'bg-amber-100 text-amber-800',
|
||||
'부족' => 'bg-red-100 text-red-800',
|
||||
default => 'bg-gray-100 text-gray-700',
|
||||
};
|
||||
?>
|
||||
<span class="text-[10px] px-1 py-0 rounded <?= $badge ?>"><?= esc($r[2]) ?></span>
|
||||
</td>
|
||||
<td class="text-right text-gray-600"><?= esc($r[3]) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bg-white border border-gray-200 rounded shadow-sm overflow-hidden">
|
||||
<div class="px-2 py-1 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
||||
<h2 class="text-[11px] font-bold text-gray-800"><i class="fa-solid fa-list-check text-[#2b4c8c] mr-1"></i>발주 / 구매신청 진행</h2>
|
||||
<span class="text-[10px] text-gray-500">최근 5건</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[220px] overflow-y-auto">
|
||||
<table class="w-full dense-table text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>문서</th>
|
||||
<th>상대</th>
|
||||
<th>내용</th>
|
||||
<th>단계</th>
|
||||
<th class="text-right">시각</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($orderRows as $r): ?>
|
||||
<tr>
|
||||
<td class="text-blue-700 font-mono text-[10px]"><?= esc($r[0]) ?></td>
|
||||
<td class="truncate max-w-[6rem]" title="<?= esc($r[1]) ?>"><?= esc($r[1]) ?></td>
|
||||
<td class="truncate max-w-[8rem]" title="<?= esc($r[2]) ?>"><?= esc($r[2]) ?></td>
|
||||
<td><span class="text-[10px] bg-slate-100 px-1 rounded"><?= esc($r[3]) ?></span></td>
|
||||
<td class="text-right text-gray-500 text-[10px] whitespace-nowrap"><?= esc($r[4]) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="chart-card mb-2">
|
||||
<h2><i class="fa-solid fa-chart-line text-[#2b4c8c] mr-1"></i>월별 출고 vs 구매신청 건수 (최근 12개월)</h2>
|
||||
<div class="chart-wrap tall"><canvas id="liteChLineYear"></canvas></div>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2 mb-2">
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-boxes-stacked text-[#2b4c8c] mr-1"></i>품목별 재고 (천 장)</h2>
|
||||
<div class="chart-wrap"><canvas id="liteChBarSku"></canvas></div>
|
||||
</section>
|
||||
<section class="chart-card">
|
||||
<h2><i class="fa-solid fa-store text-[#2b4c8c] mr-1"></i>판매소별 월 출고 TOP</h2>
|
||||
<div class="chart-wrap"><canvas id="liteChBarHStore"></canvas></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-[10px] text-gray-400 pb-1">
|
||||
<a href="<?= esc($dashHome) ?>" class="text-[#2b4c8c] hover:underline">메인 /dashboard</a>
|
||||
· <a href="<?= esc($dashBlend) ?>" class="text-[#2b4c8c] hover:underline">/dashboard/blend</a>
|
||||
· <span class="text-gray-700 font-semibold">/dashboard/lite (현재)</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const C = {
|
||||
primary: '#2b4c8c',
|
||||
blue: '#3b82f6',
|
||||
teal: '#0d9488',
|
||||
emerald: '#059669',
|
||||
amber: '#d97706',
|
||||
rose: '#e11d48',
|
||||
grid: 'rgba(0,0,0,.06)',
|
||||
};
|
||||
|
||||
Chart.defaults.font.family = "'Malgun Gothic','Apple SD Gothic Neo','Noto Sans KR',sans-serif";
|
||||
Chart.defaults.font.size = 11;
|
||||
Chart.defaults.color = '#4b5563';
|
||||
|
||||
const commonOpts = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'bottom', labels: { boxWidth: 10, padding: 8, font: { size: 10 } } },
|
||||
},
|
||||
};
|
||||
|
||||
const axisOpts = {
|
||||
scales: {
|
||||
x: { grid: { color: C.grid }, ticks: { maxRotation: 45, minRotation: 0, font: { size: 10 } } },
|
||||
y: { grid: { color: C.grid }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
},
|
||||
};
|
||||
|
||||
new Chart(document.getElementById('liteChLineYear'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월', '1월', '2월'],
|
||||
datasets: [
|
||||
{
|
||||
label: '출고(천 장)',
|
||||
data: [320, 340, 310, 355, 380, 360, 370, 390, 400, 385, 410, 395],
|
||||
borderColor: C.primary,
|
||||
backgroundColor: 'rgba(43, 76, 140, 0.08)',
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
pointRadius: 3,
|
||||
},
|
||||
{
|
||||
label: '구매신청(건)',
|
||||
data: [118, 125, 112, 130, 142, 128, 135, 140, 155, 148, 160, 152],
|
||||
borderColor: C.teal,
|
||||
backgroundColor: 'transparent',
|
||||
tension: 0.35,
|
||||
yAxisID: 'y1',
|
||||
pointRadius: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOpts,
|
||||
scales: {
|
||||
x: axisOpts.scales.x,
|
||||
y: { type: 'linear', position: 'left', grid: { color: C.grid }, title: { display: true, text: '출고', font: { size: 10 } }, beginAtZero: true },
|
||||
y1: {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
grid: { drawOnChartArea: false },
|
||||
title: { display: true, text: '건수', font: { size: 10 } },
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('liteChBarSku'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['5L', '10L', '20L', '스티커', '재사용', '특수'],
|
||||
datasets: [{
|
||||
label: '재고',
|
||||
data: [12.4, 8.2, 2.1, 15.0, 4.3, 0.9],
|
||||
backgroundColor: [C.primary, C.blue, C.amber, C.teal, C.emerald, C.rose],
|
||||
borderRadius: 4,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
...commonOpts,
|
||||
...axisOpts,
|
||||
indexAxis: 'x',
|
||||
plugins: { ...commonOpts.plugins, legend: { display: false } },
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('liteChBarHStore'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['행복마트 북구', '◇◇할인점', '□□마트', '○○슈퍼', '△△상회'],
|
||||
datasets: [{
|
||||
label: '천 장',
|
||||
data: [5.2, 4.8, 3.9, 3.5, 2.1],
|
||||
backgroundColor: C.primary,
|
||||
borderRadius: 4,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
...commonOpts,
|
||||
indexAxis: 'y',
|
||||
plugins: { ...commonOpts.plugins, legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { color: C.grid }, beginAtZero: true, ticks: { font: { size: 10 } } },
|
||||
y: { grid: { display: false }, ticks: { font: { size: 10 } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
333
app/Views/bag/designated_shop_return.php
Normal file
333
app/Views/bag/designated_shop_return.php
Normal file
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
$saleDate = (string) ($saleDate ?? date('Y-m-d'));
|
||||
$shops = is_array($shops ?? null) ? $shops : [];
|
||||
$sales = is_array($sales ?? null) ? $sales : [];
|
||||
$canCancel = (bool) ($canCancel ?? false);
|
||||
$devSoldScans = is_array($devSoldScans ?? null) ? $devSoldScans : [];
|
||||
?>
|
||||
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">지정판매소 판매 취소</span>
|
||||
</section>
|
||||
|
||||
<div class="border border-gray-300 p-3 mt-2 bg-white space-y-3">
|
||||
<form id="cancel-form" action="<?= base_url('bag/sale/designated-cancel/submit') ?>" method="POST" class="flex flex-wrap items-end gap-2">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="sale_date" value="<?= esc($saleDate) ?>"/>
|
||||
<input type="hidden" name="selected_codes_json" id="selected-codes-json" value="[]"/>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-700 mb-1">판매일자</label>
|
||||
<input type="date" id="sale-date" value="<?= esc($saleDate) ?>" class="border border-gray-300 rounded px-2 py-1.5 text-sm w-44"/>
|
||||
</div>
|
||||
<button type="submit" class="bg-btn-exit text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90" <?= $canCancel ? '' : 'disabled' ?>>취소</button>
|
||||
<?php if (! $canCancel): ?>
|
||||
<span class="text-xs text-orange-600">과거 일자는 조회만 가능합니다. (취소 불가)</span>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-5 gap-3">
|
||||
<section class="xl:col-span-2 border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">판매 리스트</div>
|
||||
<div class="max-h-64 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">번호</th>
|
||||
<th>지정판매소</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="shop-list-body">
|
||||
<?php if ($shops !== []): ?>
|
||||
<?php foreach ($shops as $idx => $s): ?>
|
||||
<tr class="shop-row cursor-pointer hover:bg-blue-50" data-ds-idx="<?= esc((string) ($s['bssc_ds_idx'] ?? 0), 'attr') ?>">
|
||||
<td class="text-center"><?= $idx + 1 ?></td>
|
||||
<td class="text-left pl-2"><?= esc((string) ($s['ds_name'] ?? ('판매소#' . (string) ($s['bssc_ds_idx'] ?? 0)))) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="2" class="text-center text-gray-400 py-6">조회된 판매 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="xl:col-span-3 border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">판매 취소 리스트</div>
|
||||
<div class="max-h-64 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>품목</th>
|
||||
<th class="w-20">단가</th>
|
||||
<th class="w-24">판매액</th>
|
||||
<th class="w-14">취소</th>
|
||||
<th class="w-24">취소액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="item-list-body">
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-6">판매소를 선택해 주세요.</td></tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-semibold bg-gray-50">
|
||||
<td colspan="2" class="text-right px-2 py-1">판매 합계</td>
|
||||
<td class="text-right px-2 py-1" id="sum-sale-amount">0</td>
|
||||
<td class="text-center px-2 py-1">-</td>
|
||||
<td class="text-right px-2 py-1" id="sum-cancel-amount">0</td>
|
||||
</tr>
|
||||
<tr class="font-semibold bg-gray-50">
|
||||
<td colspan="4" class="text-right px-2 py-1">총 판매액 - 총 취소액</td>
|
||||
<td class="text-right px-2 py-1" id="sum-net-amount">0</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">판매 취소 봉투 코드</div>
|
||||
<div class="max-h-72 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-36">봉투 코드</th>
|
||||
<th class="w-16">수량</th>
|
||||
<th class="w-20">단가</th>
|
||||
<th class="w-24">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="code-list-body">
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-6">판매 취소 리스트에서 취소할 품목(취소 컬럼)을 선택해 주세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="border border-amber-400 bg-amber-50/50 p-3 mt-3 rounded-sm">
|
||||
<p class="text-xs text-amber-900 mb-2 leading-relaxed">
|
||||
<strong class="text-amber-950">[개발용 임시 표]</strong>
|
||||
지정판매소 <strong>판매 취소</strong> 화면 테스트를 위해, 위에서 선택한 <strong>판매일자</strong>(<code class="bg-amber-100 px-1 rounded"><?= esc($saleDate) ?></code>)에
|
||||
<code class="bg-amber-100 px-1 rounded">bssc_regdate</code>가 해당 일자이고 상태가 <strong>판매(sold)</strong>인 스캔 코드를 표시합니다(최대 500건).
|
||||
운영에는 불필요하므로 <strong>개발이 끝나면 이 블록 전체를 제거</strong>해 주세요.
|
||||
</p>
|
||||
<div class="max-h-56 overflow-auto border border-amber-300 bg-white">
|
||||
<table class="w-full data-table text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>지정판매소</th>
|
||||
<th class="w-14">주문</th>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-40">봉투 바코드</th>
|
||||
<th class="w-14">포장</th>
|
||||
<th class="w-12">수량</th>
|
||||
<th class="w-16">상태</th>
|
||||
<th class="w-36">등록일시</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($devSoldScans === []): ?>
|
||||
<tr><td colspan="8" class="text-center text-gray-400 py-4">해당 판매일자에 판매(sold) 스캔 코드가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($devSoldScans as $r): ?>
|
||||
<tr>
|
||||
<td class="text-left pl-1"><?= esc(trim((string) ($r['ds_name'] ?? '')) !== '' ? (string) $r['ds_name'] : ('판매소#' . (string) ($r['bssc_ds_idx'] ?? '0'))) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($r['bssc_so_idx'] ?? '')) ?></td>
|
||||
<td class="text-left pl-1"><?= esc((string) ($r['bssc_bag_code'] ?? '')) ?> <?= esc((string) ($r['bssc_bag_name'] ?? '')) ?></td>
|
||||
<td class="text-center font-mono"><?= esc((string) ($r['bssc_code'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($r['bssc_unit'] ?? '')) ?></td>
|
||||
<td class="text-right pr-1"><?= esc((string) ($r['bssc_qty'] ?? '0')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($r['bssc_state'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($r['bssc_regdate'] ?? '')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const saleDate = <?= json_encode($saleDate, JSON_UNESCAPED_UNICODE) ?>;
|
||||
const canCancel = <?= $canCancel ? 'true' : 'false' ?>;
|
||||
const sales = <?= json_encode($sales, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||
|
||||
const saleDateInput = document.getElementById('sale-date');
|
||||
const itemListBody = document.getElementById('item-list-body');
|
||||
const codeListBody = document.getElementById('code-list-body');
|
||||
const selectedCodesInput = document.getElementById('selected-codes-json');
|
||||
const cancelForm = document.getElementById('cancel-form');
|
||||
|
||||
let currentDsIdx = null;
|
||||
const selectedCodeSet = new Set();
|
||||
const selectedItemSet = new Set();
|
||||
|
||||
function rowsByShop() {
|
||||
return sales.filter((r) => Number(r.ds_idx) === Number(currentDsIdx));
|
||||
}
|
||||
|
||||
function rowsByItem() {
|
||||
const rows = rowsByShop();
|
||||
const map = new Map();
|
||||
rows.forEach((r) => {
|
||||
const key = String(r.bag_code || '');
|
||||
if (!map.has(key)) {
|
||||
map.set(key, {
|
||||
bag_code: key,
|
||||
bag_name: String(r.bag_name || ''),
|
||||
unit_price: Number(r.unit_price || 0),
|
||||
sale_amount: 0,
|
||||
cancel_amount: 0,
|
||||
});
|
||||
}
|
||||
const entry = map.get(key);
|
||||
entry.sale_amount += Number(r.amount || 0);
|
||||
if (selectedCodeSet.has(String(r.code || ''))) {
|
||||
entry.cancel_amount += Number(r.amount || 0);
|
||||
}
|
||||
});
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
function renderSums(items) {
|
||||
let sumSale = 0;
|
||||
let sumCancel = 0;
|
||||
items.forEach((it) => {
|
||||
sumSale += it.sale_amount;
|
||||
sumCancel += it.cancel_amount;
|
||||
});
|
||||
document.getElementById('sum-sale-amount').textContent = nf(sumSale);
|
||||
document.getElementById('sum-cancel-amount').textContent = nf(sumCancel);
|
||||
document.getElementById('sum-net-amount').textContent = nf(sumSale - sumCancel);
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
if (!currentDsIdx) {
|
||||
itemListBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-400 py-6">판매소를 선택해 주세요.</td></tr>';
|
||||
renderSums([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = rowsByItem();
|
||||
if (items.length === 0) {
|
||||
itemListBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-400 py-6">판매 품목이 없습니다.</td></tr>';
|
||||
renderSums([]);
|
||||
return;
|
||||
}
|
||||
|
||||
itemListBody.innerHTML = items.map((it) => {
|
||||
const key = String(it.bag_code || '');
|
||||
const checked = selectedItemSet.has(key) ? 'checked' : '';
|
||||
const disabled = canCancel ? '' : 'disabled';
|
||||
return `
|
||||
<tr>
|
||||
<td class="text-left pl-2">${key} ${it.bag_name || ''}</td>
|
||||
<td class="text-right pr-2">${nf(it.unit_price)}</td>
|
||||
<td class="text-right pr-2">${nf(it.sale_amount)}</td>
|
||||
<td class="text-center"><input type="checkbox" class="item-cancel-check" data-bag-code="${key}" ${checked} ${disabled}></td>
|
||||
<td class="text-right pr-2">${nf(it.cancel_amount)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
renderSums(items);
|
||||
}
|
||||
|
||||
function renderCodes() {
|
||||
if (!currentDsIdx || selectedItemSet.size === 0) {
|
||||
codeListBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-400 py-6">판매 취소 리스트에서 취소할 품목(취소 컬럼)을 선택해 주세요.</td></tr>';
|
||||
return;
|
||||
}
|
||||
const rows = rowsByShop().filter((r) => selectedItemSet.has(String(r.bag_code || '')));
|
||||
if (rows.length === 0) {
|
||||
codeListBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-400 py-6">봉투 코드가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
codeListBody.innerHTML = rows.map((r) => `
|
||||
<tr>
|
||||
<td class="text-left pl-2">${r.bag_code || ''} ${r.bag_name || ''}</td>
|
||||
<td class="text-center">${String(r.code || '')}</td>
|
||||
<td class="text-right pr-2">${nf(r.qty)}</td>
|
||||
<td class="text-right pr-2">${nf(r.unit_price)}</td>
|
||||
<td class="text-right pr-2">${nf(r.amount)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function syncSelectedCodesField() {
|
||||
selectedCodesInput.value = JSON.stringify(Array.from(selectedCodeSet));
|
||||
}
|
||||
|
||||
function toggleItemCodes(bagCode, checked) {
|
||||
rowsByShop().forEach((r) => {
|
||||
if (String(r.bag_code || '') === bagCode) {
|
||||
const code = String(r.code || '');
|
||||
if (!code) return;
|
||||
if (checked) selectedCodeSet.add(code); else selectedCodeSet.delete(code);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('shop-list-body')?.addEventListener('click', (e) => {
|
||||
const tr = e.target.closest('.shop-row');
|
||||
if (!tr) return;
|
||||
document.querySelectorAll('.shop-row').forEach((row) => row.classList.remove('bg-blue-100'));
|
||||
tr.classList.add('bg-blue-100');
|
||||
currentDsIdx = Number(tr.dataset.dsIdx || 0) || null;
|
||||
selectedCodeSet.clear();
|
||||
selectedItemSet.clear();
|
||||
renderItems();
|
||||
renderCodes();
|
||||
syncSelectedCodesField();
|
||||
});
|
||||
|
||||
itemListBody?.addEventListener('change', (e) => {
|
||||
const itemCheck = e.target.closest('.item-cancel-check');
|
||||
if (!itemCheck) return;
|
||||
const bagCode = String(itemCheck.dataset.bagCode || '');
|
||||
if (itemCheck.checked) selectedItemSet.add(bagCode); else selectedItemSet.delete(bagCode);
|
||||
toggleItemCodes(bagCode, itemCheck.checked);
|
||||
renderItems();
|
||||
renderCodes();
|
||||
syncSelectedCodesField();
|
||||
});
|
||||
|
||||
saleDateInput?.addEventListener('change', () => {
|
||||
const val = saleDateInput.value || '';
|
||||
if (!val) return;
|
||||
location.href = `<?= base_url('bag/sale/designated-cancel') ?>?sale_date=${encodeURIComponent(val)}`;
|
||||
});
|
||||
|
||||
cancelForm?.addEventListener('submit', (e) => {
|
||||
if (!canCancel) {
|
||||
e.preventDefault();
|
||||
alert('과거 판매일자는 취소 처리할 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
if (selectedCodeSet.size === 0) {
|
||||
e.preventDefault();
|
||||
alert('취소할 품목/봉투코드를 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (!confirm('선택한 품목을 취소 처리 하시겠습니까?')) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
syncSelectedCodesField();
|
||||
});
|
||||
|
||||
const firstShop = document.querySelector('.shop-row');
|
||||
if (firstShop) {
|
||||
firstShop.classList.add('bg-blue-100');
|
||||
currentDsIdx = Number(firstShop.dataset.dsIdx || 0) || null;
|
||||
renderItems();
|
||||
renderCodes();
|
||||
syncSelectedCodesField();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?= view('bag/_dev_all_sales_panel') ?>
|
||||
260
app/Views/bag/designated_shop_return_cancel.php
Normal file
260
app/Views/bag/designated_shop_return_cancel.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
$returnDate = (string) ($returnDate ?? date('Y-m-d'));
|
||||
$shops = is_array($shops ?? null) ? $shops : [];
|
||||
$returns = is_array($returns ?? null) ? $returns : [];
|
||||
?>
|
||||
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">지정판매소 반품 취소</span>
|
||||
</section>
|
||||
|
||||
<div class="border border-gray-300 p-3 mt-2 bg-white space-y-3">
|
||||
<form id="return-cancel-form" action="<?= base_url('bag/sale/designated-return-cancel/save') ?>" method="POST" class="flex flex-wrap items-end gap-2">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="return_date" id="return-date-hidden" value="<?= esc($returnDate) ?>"/>
|
||||
<input type="hidden" name="selected_codes_json" id="selected-codes-json" value="[]"/>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-700 mb-1">반품 일자</label>
|
||||
<input type="date" id="return-date" value="<?= esc($returnDate) ?>" class="border border-gray-300 rounded px-2 py-1.5 text-sm w-44"/>
|
||||
</div>
|
||||
<button type="button" id="btn-search" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">조회</button>
|
||||
<button type="submit" class="bg-btn-exit text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">반품 취소 저장</button>
|
||||
</form>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-5 gap-3">
|
||||
<section class="xl:col-span-2 border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">지정판매소 리스트</div>
|
||||
<div class="max-h-64 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>지정판매소 상호명</th>
|
||||
<th class="w-24">대표자명</th>
|
||||
<th class="w-28">반품일자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="shop-list-body">
|
||||
<?php if ($shops !== []): ?>
|
||||
<?php foreach ($shops as $s): ?>
|
||||
<tr class="shop-row cursor-pointer hover:bg-blue-50" data-ds-idx="<?= esc((string) ($s['brsc_ds_idx'] ?? 0), 'attr') ?>">
|
||||
<td class="text-left pl-2"><?= esc((string) ($s['ds_name'] ?? ('판매소#' . (string) ($s['brsc_ds_idx'] ?? 0)))) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($s['ds_rep_name'] ?? '-')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($s['brsc_return_date'] ?? $returnDate)) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="3" class="text-center text-gray-400 py-6">조회된 반품 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="xl:col-span-3 border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">반품 리스트</div>
|
||||
<div class="max-h-64 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>품목</th>
|
||||
<th class="w-20">단가</th>
|
||||
<th class="w-24">판매액</th>
|
||||
<th class="w-14">취소</th>
|
||||
<th class="w-24">취소액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="item-list-body">
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-6">지정판매소를 선택해 주세요.</td></tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-semibold bg-gray-50">
|
||||
<td colspan="2" class="text-right px-2 py-1">판매 합계</td>
|
||||
<td class="text-right px-2 py-1" id="sum-sale-amount">0</td>
|
||||
<td class="text-center px-2 py-1">-</td>
|
||||
<td class="text-right px-2 py-1" id="sum-cancel-amount">0</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">반품 취소 대상 봉투 코드</div>
|
||||
<div class="max-h-72 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-36">봉투코드</th>
|
||||
<th class="w-16">수량</th>
|
||||
<th class="w-20">단가</th>
|
||||
<th class="w-24">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="code-list-body">
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-6">반품 리스트에서 취소할 품목(취소 컬럼)을 선택해 주세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const rows = <?= json_encode($returns, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||
const shopListBody = document.getElementById('shop-list-body');
|
||||
const itemListBody = document.getElementById('item-list-body');
|
||||
const codeListBody = document.getElementById('code-list-body');
|
||||
const selectedCodesInput = document.getElementById('selected-codes-json');
|
||||
const returnDateInput = document.getElementById('return-date');
|
||||
const returnDateHidden = document.getElementById('return-date-hidden');
|
||||
const selectedCodeSet = new Set();
|
||||
const selectedItemSet = new Set();
|
||||
let currentDsIdx = null;
|
||||
|
||||
function rowsByShop() {
|
||||
return rows.filter((r) => Number(r.ds_idx) === Number(currentDsIdx));
|
||||
}
|
||||
|
||||
function rowsByItem() {
|
||||
const map = new Map();
|
||||
rowsByShop().forEach((r) => {
|
||||
const key = String(r.bag_code || '');
|
||||
if (!map.has(key)) {
|
||||
map.set(key, { bag_code: key, bag_name: String(r.bag_name || ''), unit_price: Number(r.unit_price || 0), sale_amount: 0, cancel_amount: 0 });
|
||||
}
|
||||
const item = map.get(key);
|
||||
item.sale_amount += Number(r.amount || 0);
|
||||
if (selectedCodeSet.has(String(r.code || ''))) item.cancel_amount += Number(r.amount || 0);
|
||||
});
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
function syncHidden() {
|
||||
selectedCodesInput.value = JSON.stringify(Array.from(selectedCodeSet));
|
||||
returnDateHidden.value = returnDateInput.value || '';
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
if (!currentDsIdx) {
|
||||
itemListBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-400 py-6">지정판매소를 선택해 주세요.</td></tr>';
|
||||
document.getElementById('sum-sale-amount').textContent = '0';
|
||||
document.getElementById('sum-cancel-amount').textContent = '0';
|
||||
return;
|
||||
}
|
||||
const items = rowsByItem();
|
||||
if (items.length === 0) {
|
||||
itemListBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-400 py-6">반품 품목이 없습니다.</td></tr>';
|
||||
document.getElementById('sum-sale-amount').textContent = '0';
|
||||
document.getElementById('sum-cancel-amount').textContent = '0';
|
||||
return;
|
||||
}
|
||||
itemListBody.innerHTML = items.map((it) => {
|
||||
const key = String(it.bag_code || '');
|
||||
return `
|
||||
<tr>
|
||||
<td class="text-left pl-2">${key} ${it.bag_name || ''}</td>
|
||||
<td class="text-right pr-2">${nf(it.unit_price)}</td>
|
||||
<td class="text-right pr-2">${nf(it.sale_amount)}</td>
|
||||
<td class="text-center"><input type="checkbox" class="item-cancel-check" data-bag-code="${key}" ${selectedItemSet.has(key) ? 'checked' : ''}></td>
|
||||
<td class="text-right pr-2">${nf(it.cancel_amount)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
const sumSale = items.reduce((s, it) => s + Number(it.sale_amount || 0), 0);
|
||||
const sumCancel = items.reduce((s, it) => s + Number(it.cancel_amount || 0), 0);
|
||||
document.getElementById('sum-sale-amount').textContent = nf(sumSale);
|
||||
document.getElementById('sum-cancel-amount').textContent = nf(sumCancel);
|
||||
}
|
||||
|
||||
function renderCodes() {
|
||||
if (!currentDsIdx || selectedItemSet.size === 0) {
|
||||
codeListBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-400 py-6">반품 리스트에서 취소할 품목(취소 컬럼)을 선택해 주세요.</td></tr>';
|
||||
return;
|
||||
}
|
||||
const targetRows = rowsByShop().filter((r) => selectedItemSet.has(String(r.bag_code || '')));
|
||||
if (targetRows.length === 0) {
|
||||
codeListBody.innerHTML = '<tr><td colspan="5" class="text-center text-gray-400 py-6">반품 코드가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
codeListBody.innerHTML = targetRows.map((r) => `
|
||||
<tr>
|
||||
<td class="text-left pl-2">${r.bag_code || ''} ${r.bag_name || ''}</td>
|
||||
<td class="text-center">${String(r.code || '')}</td>
|
||||
<td class="text-right pr-2">${nf(r.qty)}</td>
|
||||
<td class="text-right pr-2">${nf(r.unit_price)}</td>
|
||||
<td class="text-right pr-2">${nf(r.amount)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function toggleItemCodes(bagCode, checked) {
|
||||
rowsByShop().forEach((r) => {
|
||||
if (String(r.bag_code || '') !== bagCode) return;
|
||||
const code = String(r.code || '');
|
||||
if (!code) return;
|
||||
if (checked) selectedCodeSet.add(code); else selectedCodeSet.delete(code);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btn-search')?.addEventListener('click', () => {
|
||||
const val = returnDateInput.value || '';
|
||||
if (!val) return;
|
||||
location.href = `<?= base_url('bag/sale/designated-return-cancel') ?>?return_date=${encodeURIComponent(val)}`;
|
||||
});
|
||||
|
||||
shopListBody?.addEventListener('click', (e) => {
|
||||
const tr = e.target.closest('.shop-row');
|
||||
if (!tr) return;
|
||||
document.querySelectorAll('.shop-row').forEach((row) => row.classList.remove('bg-blue-100'));
|
||||
tr.classList.add('bg-blue-100');
|
||||
currentDsIdx = Number(tr.dataset.dsIdx || 0) || null;
|
||||
selectedCodeSet.clear();
|
||||
selectedItemSet.clear();
|
||||
renderItems();
|
||||
renderCodes();
|
||||
syncHidden();
|
||||
});
|
||||
|
||||
itemListBody?.addEventListener('change', (e) => {
|
||||
const itemCheck = e.target.closest('.item-cancel-check');
|
||||
if (!itemCheck) return;
|
||||
const bagCode = String(itemCheck.dataset.bagCode || '');
|
||||
if (itemCheck.checked) selectedItemSet.add(bagCode); else selectedItemSet.delete(bagCode);
|
||||
toggleItemCodes(bagCode, itemCheck.checked);
|
||||
renderItems();
|
||||
renderCodes();
|
||||
syncHidden();
|
||||
});
|
||||
|
||||
document.getElementById('return-cancel-form')?.addEventListener('submit', (e) => {
|
||||
syncHidden();
|
||||
if ((returnDateHidden.value || '') === '') {
|
||||
e.preventDefault();
|
||||
alert('반품 일자를 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (selectedCodeSet.size === 0) {
|
||||
e.preventDefault();
|
||||
alert('반품 취소할 품목/봉투코드를 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (!confirm('선택한 반품 품목을 취소 처리 하시겠습니까?')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
const firstShop = document.querySelector('.shop-row');
|
||||
if (firstShop) {
|
||||
firstShop.classList.add('bg-blue-100');
|
||||
currentDsIdx = Number(firstShop.dataset.dsIdx || 0) || null;
|
||||
renderItems();
|
||||
renderCodes();
|
||||
syncHidden();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?= view('bag/_dev_all_sales_panel') ?>
|
||||
520
app/Views/bag/designated_shop_sale.php
Normal file
520
app/Views/bag/designated_shop_sale.php
Normal file
@@ -0,0 +1,520 @@
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">지정판매소 판매</span>
|
||||
</section>
|
||||
|
||||
<div class="border border-gray-300 p-3 mt-2 bg-white space-y-3">
|
||||
<form id="sale-save-form" action="<?= base_url('bag/sale/designated/save') ?>" method="POST">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="so_idx" id="save-so-idx" value=""/>
|
||||
<input type="hidden" name="ds_idx" id="save-ds-idx" value=""/>
|
||||
<input type="hidden" name="scans_json" id="save-scans-json" value="[]"/>
|
||||
|
||||
<div class="flex flex-wrap items-end gap-2">
|
||||
<div class="relative">
|
||||
<label class="block text-xs font-bold text-gray-700 mb-1">판매소 검색</label>
|
||||
<input id="shop-search" type="text" class="border border-gray-300 rounded px-2 py-1.5 text-sm w-[36rem] max-w-full" placeholder="코드/상호/대표자/전화/주소"/>
|
||||
<datalist id="shop-search-list" class="hidden">
|
||||
<?php foreach (($shops ?? []) as $shop): ?>
|
||||
<option value="<?= esc(trim(($shop->ds_shop_no ?? '') . ' ' . ($shop->ds_name ?? '') . ' ' . ($shop->ds_rep_name ?? '') . ' ' . ($shop->ds_tel ?? '') . ' ' . ($shop->ds_addr ?? ''))) ?>"></option>
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
<div id="shop-search-suggest" class="hidden absolute left-0 top-full mt-1 w-[36rem] max-w-full max-h-56 overflow-auto border border-gray-300 bg-white shadow-lg z-20"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-700 mb-1">봉투코드 입력(스캔)</label>
|
||||
<input id="barcode-input" type="text" class="border border-gray-300 rounded px-2 py-1.5 text-sm w-80" placeholder="박스/팩/낱장 바코드"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-700 mb-1">판매일</label>
|
||||
<div class="border border-gray-300 bg-gray-100 rounded px-2 py-1.5 text-sm w-36"><?= esc(date('Y-m-d')) ?></div>
|
||||
</div>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">판매 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="scan-message" class="text-sm text-gray-600"></div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-5 gap-3">
|
||||
<section class="xl:col-span-2 border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">지정판매소 정보</div>
|
||||
<table class="w-full text-sm">
|
||||
<tr><th class="text-left px-3 py-1.5 w-28">판매소 코드</th><td id="shop-info-code" class="px-3 py-1.5">-</td></tr>
|
||||
<tr><th class="text-left px-3 py-1.5">상호</th><td id="shop-info-name" class="px-3 py-1.5">-</td></tr>
|
||||
<tr><th class="text-left px-3 py-1.5">대표자명</th><td id="shop-info-rep" class="px-3 py-1.5">-</td></tr>
|
||||
<tr><th class="text-left px-3 py-1.5">대표전화</th><td id="shop-info-tel" class="px-3 py-1.5">-</td></tr>
|
||||
<tr><th class="text-left px-3 py-1.5">주소</th><td id="shop-info-addr" class="px-3 py-1.5">-</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="xl:col-span-3 border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">주문 접수 리스트</div>
|
||||
<div class="max-h-56 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">번호</th>
|
||||
<th>판매소</th>
|
||||
<th class="w-28">접수일</th>
|
||||
<th class="w-28">배달일</th>
|
||||
<th class="w-16">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="order-list-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section id="dev-saleable-panel" class="hidden border border-amber-400 bg-amber-50/50 p-3 rounded-sm">
|
||||
<p class="text-xs text-amber-900 mb-2 leading-relaxed">
|
||||
<strong class="text-amber-950">[개발용 임시]</strong>
|
||||
<strong>주문 접수 리스트</strong>에서 주문을 선택하면, 그 주문의 지정판매소(<code class="bg-amber-100 px-1 rounded">so_ds_idx</code>) 기준으로
|
||||
판매 테스트에 쓸 수 있는 바코드 후보를 표시합니다.
|
||||
(① 해당 판매소에 연결된 <code class="bg-amber-100 px-1 rounded">bag_sale_scan_code</code> 중 <code class="bg-amber-100 px-1 rounded">in_stock</code>,
|
||||
② 같은 판매소의 <strong>전화·정상 주문</strong> 품목 봉투코드(수령완료 포함, 주문 리스트와 동일 범위) 및 <strong>선택한 주문</strong> 품목에 맞는 <code class="bg-amber-100 px-1 rounded">bag_receiving_pack_code</code> <code class="bg-amber-100 px-1 rounded">in_stock</code> 팩 코드.
|
||||
입고 행의 <strong>수량</strong>은 팩에 담긴 <code class="bg-amber-100 px-1 rounded">brpc_sheet_qty</code>(낱장 수)입니다.)
|
||||
<strong>개발 완료 후 이 블록과 API 라우트를 제거</strong>해 주세요.
|
||||
</p>
|
||||
<div class="max-h-52 overflow-auto border border-amber-300 bg-white">
|
||||
<table class="w-full data-table text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-28">출처</th>
|
||||
<th class="w-40">바코드(대표)</th>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-16">포장</th>
|
||||
<th class="w-10">수량</th>
|
||||
<th class="w-12">주문</th>
|
||||
<th class="w-14">상태</th>
|
||||
<th>비고(낱장범위 등)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dev-saleable-tbody">
|
||||
<tr><td colspan="8" class="text-center text-gray-400 py-4">주문을 선택하면 목록이 표시됩니다.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-3">
|
||||
<section class="border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">판매 내역</div>
|
||||
<div class="max-h-72 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">선택</th>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-20">접수량</th>
|
||||
<th class="w-20">판매량</th>
|
||||
<th class="w-20">단가</th>
|
||||
<th class="w-24">판매금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sale-items-body">
|
||||
<tr><td colspan="6" class="text-center text-gray-400 py-6">주문을 선택해 주세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">판매 상세 내역</div>
|
||||
<div class="max-h-72 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-28">봉투 코드</th>
|
||||
<th class="w-16">수량</th>
|
||||
<th class="w-16">포장단위</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scan-detail-body">
|
||||
<tr><td colspan="4" class="text-center text-gray-400 py-6">바코드를 스캔해 주세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const shops = <?= json_encode(array_map(static function ($s): array {
|
||||
return [
|
||||
'ds_idx' => (int) ($s->ds_idx ?? 0),
|
||||
'ds_shop_no' => (string) ($s->ds_shop_no ?? ''),
|
||||
'ds_name' => (string) ($s->ds_name ?? ''),
|
||||
'ds_rep_name' => (string) ($s->ds_rep_name ?? ''),
|
||||
'ds_tel' => (string) ($s->ds_tel ?? ''),
|
||||
'ds_addr' => trim((string) ($s->ds_addr ?? '') . ' ' . (string) ($s->ds_addr_detail ?? '')),
|
||||
];
|
||||
}, $shops ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const orders = <?= json_encode($orders ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const scanApi = '<?= base_url('bag/sale/designated/scan') ?>';
|
||||
const devSaleableApi = '<?= base_url('bag/sale/designated/dev-saleable-barcodes') ?>';
|
||||
const csrfName = '<?= csrf_token() ?>';
|
||||
const csrfHash = '<?= csrf_hash() ?>';
|
||||
|
||||
const shopSearch = document.getElementById('shop-search');
|
||||
const shopSuggest = document.getElementById('shop-search-suggest');
|
||||
const barcodeInput = document.getElementById('barcode-input');
|
||||
const orderListBody = document.getElementById('order-list-body');
|
||||
const saleItemsBody = document.getElementById('sale-items-body');
|
||||
const scanDetailBody = document.getElementById('scan-detail-body');
|
||||
const saveForm = document.getElementById('sale-save-form');
|
||||
const saveSoIdx = document.getElementById('save-so-idx');
|
||||
const saveDsIdx = document.getElementById('save-ds-idx');
|
||||
const saveScansJson = document.getElementById('save-scans-json');
|
||||
const scanMessage = document.getElementById('scan-message');
|
||||
const devSaleablePanel = document.getElementById('dev-saleable-panel');
|
||||
const devSaleableTbody = document.getElementById('dev-saleable-tbody');
|
||||
|
||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||
let selectedShop = null;
|
||||
let selectedOrder = null;
|
||||
let selectedBagCode = '';
|
||||
const pendingScans = [];
|
||||
|
||||
const shopMap = new Map(shops.map((s) => [String(s.ds_idx), s]));
|
||||
|
||||
function setMessage(msg, isError = false) {
|
||||
scanMessage.textContent = msg || '';
|
||||
scanMessage.className = isError ? 'text-sm text-red-600' : 'text-sm text-emerald-700';
|
||||
}
|
||||
|
||||
function mergedShopText(shop) {
|
||||
return [shop.ds_shop_no, shop.ds_name, shop.ds_rep_name, shop.ds_tel, shop.ds_addr]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function hideSuggest() {
|
||||
if (shopSuggest) {
|
||||
shopSuggest.classList.add('hidden');
|
||||
shopSuggest.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSuggest(query) {
|
||||
if (!shopSuggest) return;
|
||||
const q = String(query || '').trim().toLowerCase();
|
||||
const matched = (q
|
||||
? shops.filter((s) => mergedShopText(s).toLowerCase().includes(q))
|
||||
: shops
|
||||
).slice(0, 30);
|
||||
if (matched.length === 0) {
|
||||
hideSuggest();
|
||||
return;
|
||||
}
|
||||
shopSuggest.innerHTML = matched.map((s) => `
|
||||
<button type="button" class="shop-suggest-item w-full text-left px-2 py-1.5 hover:bg-blue-50 text-xs border-b border-gray-100 whitespace-normal break-all" data-ds-idx="${s.ds_idx}">
|
||||
${mergedShopText(s)}
|
||||
</button>
|
||||
`).join('');
|
||||
shopSuggest.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideDevSaleablePanel() {
|
||||
if (devSaleablePanel) devSaleablePanel.classList.add('hidden');
|
||||
if (devSaleableTbody) {
|
||||
devSaleableTbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">주문을 선택하면 목록이 표시됩니다.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDevSaleableBarcodes(dsIdx) {
|
||||
if (!devSaleablePanel || !devSaleableTbody) return;
|
||||
const idx = Number(dsIdx || 0);
|
||||
if (!idx) {
|
||||
hideDevSaleablePanel();
|
||||
return;
|
||||
}
|
||||
devSaleablePanel.classList.remove('hidden');
|
||||
devSaleableTbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">불러오는 중…</td></tr>';
|
||||
try {
|
||||
let devUrl = `${devSaleableApi}?ds_idx=${encodeURIComponent(String(idx))}`;
|
||||
if (selectedOrder && Number(selectedOrder.so_idx || 0) > 0) {
|
||||
devUrl += `&so_idx=${encodeURIComponent(String(selectedOrder.so_idx))}`;
|
||||
}
|
||||
const res = await fetch(devUrl, { credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
devSaleableTbody.innerHTML = `<tr><td colspan="8" class="text-center text-red-600 py-4">${data.message || '조회 실패'}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
const rows = Array.isArray(data.rows) ? data.rows : [];
|
||||
if (rows.length === 0) {
|
||||
devSaleableTbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">표시할 바코드가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
devSaleableTbody.innerHTML = rows.map((r) => `
|
||||
<tr>
|
||||
<td class="text-left pl-1">${r.source || ''}</td>
|
||||
<td class="text-center font-mono">${r.code || ''}</td>
|
||||
<td class="text-left pl-1">${r.bag_code || ''} ${r.bag_name || ''}</td>
|
||||
<td class="text-center">${r.unit || ''}</td>
|
||||
<td class="text-right pr-1">${nf(r.qty || 0)}</td>
|
||||
<td class="text-center">${r.so_idx ? String(r.so_idx) : '-'}</td>
|
||||
<td class="text-center">${r.state || ''}</td>
|
||||
<td class="text-left pl-1">${r.extra || ''}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} catch {
|
||||
devSaleableTbody.innerHTML = '<tr><td colspan="8" class="text-center text-red-600 py-4">네트워크 오류</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateShopInfo(shop) {
|
||||
document.getElementById('shop-info-code').textContent = shop?.ds_shop_no || '-';
|
||||
document.getElementById('shop-info-name').textContent = shop?.ds_name || '-';
|
||||
document.getElementById('shop-info-rep').textContent = shop?.ds_rep_name || '-';
|
||||
document.getElementById('shop-info-tel').textContent = shop?.ds_tel || '-';
|
||||
document.getElementById('shop-info-addr').textContent = shop?.ds_addr || '-';
|
||||
}
|
||||
|
||||
function renderOrderList() {
|
||||
const rows = (selectedShop ? orders.filter((o) => Number(o.so_ds_idx) === Number(selectedShop.ds_idx)) : orders);
|
||||
if (rows.length === 0) {
|
||||
orderListBody.innerHTML = '<tr><td colspan="5" class="text-center py-6 text-gray-400">조건에 맞는 주문이 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
orderListBody.innerHTML = rows.map((o) => `
|
||||
<tr class="order-row cursor-pointer hover:bg-blue-50 ${selectedOrder && Number(selectedOrder.so_idx) === Number(o.so_idx) ? 'bg-blue-100' : ''}" data-order-id="${o.so_idx}">
|
||||
<td class="text-center">${o.so_idx}</td>
|
||||
<td class="text-left pl-2">${o.so_ds_name || ''}</td>
|
||||
<td class="text-center">${o.so_order_date || ''}</td>
|
||||
<td class="text-center">${o.so_delivery_date || ''}</td>
|
||||
<td class="text-center">${o.so_status === 'cancelled' ? '주문 취소' : (Number(o.so_received || 0) === 1 ? '판매 완료' : '판매 진행')}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function pendingQtyForBag(code) {
|
||||
return pendingScans.filter((s) => s.bag_code === code).reduce((sum, s) => sum + (Number(s.qty) || 0), 0);
|
||||
}
|
||||
|
||||
function renderSaleItems() {
|
||||
if (!selectedOrder) {
|
||||
saleItemsBody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-400 py-6">주문을 선택해 주세요.</td></tr>';
|
||||
return;
|
||||
}
|
||||
const items = Array.isArray(selectedOrder.items) ? selectedOrder.items : [];
|
||||
if (items.length === 0) {
|
||||
saleItemsBody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-400 py-6">품목 정보가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
saleItemsBody.innerHTML = items.map((it) => {
|
||||
const bagCode = String(it.soi_bag_code || '');
|
||||
const sold = Number(it.sold_qty || 0) + pendingQtyForBag(bagCode);
|
||||
const unitPrice = Number(it.soi_unit_price || 0);
|
||||
const amount = sold * unitPrice;
|
||||
const checked = selectedBagCode === bagCode ? 'checked' : '';
|
||||
return `
|
||||
<tr>
|
||||
<td class="text-center"><input type="radio" name="pick-bag" value="${bagCode}" ${checked}></td>
|
||||
<td class="text-left pl-2">${bagCode} ${it.soi_bag_name || ''}</td>
|
||||
<td class="text-right pr-2">${nf(it.soi_qty || 0)}</td>
|
||||
<td class="text-right pr-2">${nf(sold)}</td>
|
||||
<td class="text-right pr-2">${nf(unitPrice)}</td>
|
||||
<td class="text-right pr-2">${nf(amount)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderScanDetails() {
|
||||
const rows = pendingScans.filter((s) => !selectedBagCode || s.bag_code === selectedBagCode);
|
||||
if (rows.length === 0) {
|
||||
scanDetailBody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-400 py-6">바코드를 스캔해 주세요.</td></tr>';
|
||||
return;
|
||||
}
|
||||
scanDetailBody.innerHTML = rows.map((r) => `
|
||||
<tr>
|
||||
<td class="text-left pl-2">${r.bag_code} ${r.bag_name || ''}</td>
|
||||
<td class="text-center">${r.barcode}</td>
|
||||
<td class="text-right pr-2">${nf(r.qty)}</td>
|
||||
<td class="text-center">${r.unit}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function selectOrder(orderId) {
|
||||
selectedOrder = orders.find((o) => Number(o.so_idx) === Number(orderId)) || null;
|
||||
selectedBagCode = '';
|
||||
pendingScans.length = 0;
|
||||
saveSoIdx.value = selectedOrder ? String(selectedOrder.so_idx) : '';
|
||||
saveDsIdx.value = selectedOrder ? String(selectedOrder.so_ds_idx || '') : (selectedShop ? String(selectedShop.ds_idx) : '');
|
||||
saveScansJson.value = '[]';
|
||||
|
||||
// 주문이 선택되면 그 주문의 지정판매소를 「지정판매소 정보」 표·검색 input 에도 자동 반영한다.
|
||||
if (selectedOrder) {
|
||||
const matchedShop = shopMap.get(String(selectedOrder.so_ds_idx)) || null;
|
||||
if (matchedShop) {
|
||||
selectedShop = matchedShop;
|
||||
if (shopSearch) shopSearch.value = mergedShopText(matchedShop);
|
||||
updateShopInfo(matchedShop);
|
||||
}
|
||||
}
|
||||
|
||||
renderOrderList();
|
||||
renderSaleItems();
|
||||
renderScanDetails();
|
||||
if (selectedOrder && Number(selectedOrder.so_ds_idx || 0) > 0) {
|
||||
loadDevSaleableBarcodes(selectedOrder.so_ds_idx);
|
||||
} else {
|
||||
hideDevSaleablePanel();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitScan() {
|
||||
const code = (barcodeInput.value || '').trim();
|
||||
if (!code) return;
|
||||
if (!selectedOrder) {
|
||||
setMessage('주문 접수 리스트에서 주문을 먼저 선택해 주세요.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
payload.set(csrfName, csrfHash);
|
||||
payload.set('so_idx', String(selectedOrder.so_idx));
|
||||
payload.set('barcode', code);
|
||||
|
||||
const pendingByBag = {};
|
||||
pendingScans.forEach((s) => {
|
||||
const k = String(s.bag_code || '');
|
||||
pendingByBag[k] = (pendingByBag[k] || 0) + (Number(s.qty || 0));
|
||||
});
|
||||
payload.set('pending_by_bag', JSON.stringify(pendingByBag));
|
||||
|
||||
const res = await fetch(scanApi, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: payload.toString(),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
setMessage(data.message || '스캔 처리 실패', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedBagCode) {
|
||||
selectedBagCode = String(data.bag_code || '');
|
||||
}
|
||||
pendingScans.push({
|
||||
barcode: code,
|
||||
bag_code: data.bag_code,
|
||||
bag_name: data.bag_name,
|
||||
qty: Number(data.qty || 0),
|
||||
unit: data.unit,
|
||||
pack_ids: Array.isArray(data.pack_ids) ? data.pack_ids : [],
|
||||
});
|
||||
saveScansJson.value = JSON.stringify(pendingScans);
|
||||
barcodeInput.value = '';
|
||||
setMessage(`등록 완료: ${data.bag_code} / ${data.unit} / 수량 ${nf(data.qty)}`);
|
||||
renderSaleItems();
|
||||
renderScanDetails();
|
||||
}
|
||||
|
||||
shopSearch?.addEventListener('change', (e) => {
|
||||
const q = String(e.target.value || '').trim().toLowerCase();
|
||||
selectedShop = shops.find((s) => {
|
||||
const merged = [s.ds_shop_no, s.ds_name, s.ds_rep_name, s.ds_tel, s.ds_addr].join(' ').toLowerCase();
|
||||
return merged.includes(q);
|
||||
}) || null;
|
||||
selectedOrder = null;
|
||||
selectedBagCode = '';
|
||||
pendingScans.length = 0;
|
||||
saveSoIdx.value = '';
|
||||
saveDsIdx.value = selectedShop ? String(selectedShop.ds_idx) : '';
|
||||
saveScansJson.value = '[]';
|
||||
updateShopInfo(selectedShop);
|
||||
hideDevSaleablePanel();
|
||||
renderOrderList();
|
||||
renderSaleItems();
|
||||
renderScanDetails();
|
||||
setMessage('');
|
||||
hideSuggest();
|
||||
});
|
||||
|
||||
shopSearch?.addEventListener('input', (e) => {
|
||||
renderSuggest(e.target.value || '');
|
||||
});
|
||||
|
||||
shopSearch?.addEventListener('focus', (e) => {
|
||||
renderSuggest(e.target.value || '');
|
||||
});
|
||||
|
||||
shopSearch?.addEventListener('click', (e) => {
|
||||
renderSuggest(e.target.value || '');
|
||||
});
|
||||
|
||||
shopSuggest?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.shop-suggest-item');
|
||||
if (!btn) return;
|
||||
const dsIdx = Number(btn.dataset.dsIdx || 0);
|
||||
const shop = shops.find((s) => Number(s.ds_idx) === dsIdx) || null;
|
||||
if (!shop) return;
|
||||
shopSearch.value = mergedShopText(shop);
|
||||
selectedShop = shop;
|
||||
selectedOrder = null;
|
||||
selectedBagCode = '';
|
||||
pendingScans.length = 0;
|
||||
saveSoIdx.value = '';
|
||||
saveDsIdx.value = selectedShop ? String(selectedShop.ds_idx) : '';
|
||||
saveScansJson.value = '[]';
|
||||
updateShopInfo(selectedShop);
|
||||
hideDevSaleablePanel();
|
||||
renderOrderList();
|
||||
renderSaleItems();
|
||||
renderScanDetails();
|
||||
setMessage('');
|
||||
hideSuggest();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!shopSuggest || !shopSearch) return;
|
||||
if (e.target === shopSearch || shopSuggest.contains(e.target)) return;
|
||||
hideSuggest();
|
||||
});
|
||||
|
||||
barcodeInput?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
submitScan();
|
||||
}
|
||||
});
|
||||
|
||||
orderListBody?.addEventListener('click', (e) => {
|
||||
const tr = e.target.closest('.order-row');
|
||||
if (!tr) return;
|
||||
selectOrder(tr.dataset.orderId);
|
||||
});
|
||||
|
||||
saleItemsBody?.addEventListener('change', (e) => {
|
||||
const radio = e.target.closest('input[name="pick-bag"]');
|
||||
if (!radio) return;
|
||||
selectedBagCode = radio.value || '';
|
||||
renderScanDetails();
|
||||
});
|
||||
|
||||
saveForm?.addEventListener('submit', (e) => {
|
||||
if (!selectedOrder) {
|
||||
e.preventDefault();
|
||||
alert('주문을 먼저 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (pendingScans.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('없는 바코드이거나 유효한 스캔 내역이 없습니다.');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
renderOrderList();
|
||||
renderSaleItems();
|
||||
renderScanDetails();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?= view('bag/_dev_all_sales_panel') ?>
|
||||
424
app/Views/bag/designated_shop_sale_return.php
Normal file
424
app/Views/bag/designated_shop_sale_return.php
Normal file
@@ -0,0 +1,424 @@
|
||||
<?php
|
||||
$devSoldScans = is_array($devSoldScans ?? null) ? $devSoldScans : [];
|
||||
?>
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">지정판매소 반품</span>
|
||||
</section>
|
||||
|
||||
<div class="border border-gray-300 p-3 mt-2 bg-white space-y-3">
|
||||
<form id="return-save-form" action="<?= base_url('bag/sale/designated-return/save') ?>" method="POST">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="ds_idx" id="save-ds-idx" value=""/>
|
||||
<input type="hidden" name="scans_json" id="save-scans-json" value="[]"/>
|
||||
|
||||
<div class="flex flex-wrap items-end gap-2">
|
||||
<div class="relative">
|
||||
<label class="block text-xs font-bold text-gray-700 mb-1">판매소 검색</label>
|
||||
<input id="shop-search" type="text" class="border border-gray-300 rounded px-2 py-1.5 text-sm w-[36rem] max-w-full" placeholder="코드/상호/대표자/전화/주소"/>
|
||||
<div id="shop-search-suggest" class="hidden absolute left-0 top-full mt-1 w-[36rem] max-w-full max-h-56 overflow-auto border border-gray-300 bg-white shadow-lg z-20"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-700 mb-1">봉투코드 입력</label>
|
||||
<input id="barcode-input" type="text" class="border border-gray-300 rounded px-2 py-1.5 text-sm w-80" placeholder="박스/팩/낱장 바코드"/>
|
||||
</div>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90">반품 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="scan-message" class="text-sm text-gray-600"></div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-3">
|
||||
<section class="border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">지정판매소 정보</div>
|
||||
<table class="w-full text-sm">
|
||||
<tr><th class="text-left px-3 py-1.5 w-28">판매소 코드</th><td id="shop-info-code" class="px-3 py-1.5">-</td></tr>
|
||||
<tr><th class="text-left px-3 py-1.5">상호</th><td id="shop-info-name" class="px-3 py-1.5">-</td></tr>
|
||||
<tr><th class="text-left px-3 py-1.5">대표자명</th><td id="shop-info-rep" class="px-3 py-1.5">-</td></tr>
|
||||
<tr><th class="text-left px-3 py-1.5">대표전화</th><td id="shop-info-tel" class="px-3 py-1.5">-</td></tr>
|
||||
<tr><th class="text-left px-3 py-1.5">주소</th><td id="shop-info-addr" class="px-3 py-1.5">-</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">반품 리스트</div>
|
||||
<div class="max-h-64 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">선택</th>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-20">수량</th>
|
||||
<th class="w-20">단가</th>
|
||||
<th class="w-24">금액</th>
|
||||
<th class="w-16">제거</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="return-list-body">
|
||||
<tr><td colspan="6" class="text-center text-gray-400 py-6">판매소를 선택하고 바코드를 스캔해 주세요.</td></tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-semibold bg-gray-50">
|
||||
<td colspan="2" class="text-right px-2 py-1">합계</td>
|
||||
<td class="text-right px-2 py-1" id="sum-qty">0</td>
|
||||
<td class="text-right px-2 py-1">-</td>
|
||||
<td class="text-right px-2 py-1" id="sum-amount">0</td>
|
||||
<td class="px-2 py-1"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="border border-gray-300">
|
||||
<div class="px-3 py-2 border-b bg-gray-50 text-sm font-semibold text-gray-700">반품 봉투 코드</div>
|
||||
<div class="max-h-72 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-36">봉투 코드</th>
|
||||
<th class="w-16">수량</th>
|
||||
<th class="w-20">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="return-code-body">
|
||||
<tr><td colspan="4" class="text-center text-gray-400 py-6">품목을 선택해 주세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="border border-amber-400 bg-amber-50/50 p-3 mt-3 rounded-sm">
|
||||
<p class="text-xs text-amber-900 mb-2 leading-relaxed">
|
||||
<strong class="text-amber-950">[개발용 임시 표]</strong>
|
||||
반품 스캔 테스트를 위해, 현재 지자체에서 <code class="bg-amber-100 px-1 rounded">bag_sale_scan_code</code> 상태가
|
||||
<strong>판매(sold)</strong>인 봉투 바코드 일부(최대 200건, 최근 등록순)를 표시합니다.
|
||||
운영 화면에는 포함되지 않아야 하므로 <strong>개발이 끝나면 이 블록 전체를 제거</strong>해 주세요.
|
||||
</p>
|
||||
<div class="max-h-56 overflow-auto border border-amber-300 bg-white">
|
||||
<table class="w-full data-table text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>지정판매소</th>
|
||||
<th class="w-14">주문</th>
|
||||
<th>봉투 종류</th>
|
||||
<th class="w-40">봉투 바코드</th>
|
||||
<th class="w-14">포장</th>
|
||||
<th class="w-12">수량</th>
|
||||
<th class="w-16">상태</th>
|
||||
<th class="w-36">등록일시</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($devSoldScans === []): ?>
|
||||
<tr><td colspan="8" class="text-center text-gray-400 py-4">판매(sold) 상태인 스캔 코드가 없습니다.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($devSoldScans as $r): ?>
|
||||
<tr>
|
||||
<td class="text-left pl-1"><?= esc(trim((string) ($r['ds_name'] ?? '')) !== '' ? (string) $r['ds_name'] : ('판매소#' . (string) ($r['bssc_ds_idx'] ?? '0'))) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($r['bssc_so_idx'] ?? '')) ?></td>
|
||||
<td class="text-left pl-1"><?= esc((string) ($r['bssc_bag_code'] ?? '')) ?> <?= esc((string) ($r['bssc_bag_name'] ?? '')) ?></td>
|
||||
<td class="text-center font-mono"><?= esc((string) ($r['bssc_code'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($r['bssc_unit'] ?? '')) ?></td>
|
||||
<td class="text-right pr-1"><?= esc((string) ($r['bssc_qty'] ?? '0')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($r['bssc_state'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($r['bssc_regdate'] ?? '')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const shops = <?= json_encode(array_map(static function ($s): array {
|
||||
return [
|
||||
'ds_idx' => (int) ($s->ds_idx ?? 0),
|
||||
'ds_shop_no' => (string) ($s->ds_shop_no ?? ''),
|
||||
'ds_name' => (string) ($s->ds_name ?? ''),
|
||||
'ds_rep_name' => (string) ($s->ds_rep_name ?? ''),
|
||||
'ds_tel' => (string) ($s->ds_tel ?? ''),
|
||||
'ds_addr' => trim((string) ($s->ds_addr ?? '') . ' ' . (string) ($s->ds_addr_detail ?? '')),
|
||||
];
|
||||
}, $shops ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const scanApi = '<?= base_url('bag/sale/designated-return/scan') ?>';
|
||||
const csrfName = '<?= csrf_token() ?>';
|
||||
const csrfHash = '<?= csrf_hash() ?>';
|
||||
|
||||
const shopSearch = document.getElementById('shop-search');
|
||||
const shopSuggest = document.getElementById('shop-search-suggest');
|
||||
const barcodeInput = document.getElementById('barcode-input');
|
||||
const returnListBody = document.getElementById('return-list-body');
|
||||
const returnCodeBody = document.getElementById('return-code-body');
|
||||
const saveForm = document.getElementById('return-save-form');
|
||||
const saveDsIdx = document.getElementById('save-ds-idx');
|
||||
const saveScansJson = document.getElementById('save-scans-json');
|
||||
const scanMessage = document.getElementById('scan-message');
|
||||
|
||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||
let selectedShop = null;
|
||||
let selectedBagCode = '';
|
||||
const scannedRows = [];
|
||||
|
||||
function mergedShopText(shop) {
|
||||
return [shop.ds_shop_no, shop.ds_name, shop.ds_rep_name, shop.ds_tel, shop.ds_addr].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function setMessage(msg, isError = false) {
|
||||
scanMessage.textContent = msg || '';
|
||||
scanMessage.className = isError ? 'text-sm text-red-600' : 'text-sm text-emerald-700';
|
||||
}
|
||||
|
||||
function updateShopInfo(shop) {
|
||||
document.getElementById('shop-info-code').textContent = shop?.ds_shop_no || '-';
|
||||
document.getElementById('shop-info-name').textContent = shop?.ds_name || '-';
|
||||
document.getElementById('shop-info-rep').textContent = shop?.ds_rep_name || '-';
|
||||
document.getElementById('shop-info-tel').textContent = shop?.ds_tel || '-';
|
||||
document.getElementById('shop-info-addr').textContent = shop?.ds_addr || '-';
|
||||
}
|
||||
|
||||
function hideSuggest() {
|
||||
if (!shopSuggest) return;
|
||||
shopSuggest.classList.add('hidden');
|
||||
shopSuggest.innerHTML = '';
|
||||
}
|
||||
|
||||
function renderSuggest(query) {
|
||||
if (!shopSuggest) return;
|
||||
const q = String(query || '').trim().toLowerCase();
|
||||
const matched = (q ? shops.filter((s) => mergedShopText(s).toLowerCase().includes(q)) : shops).slice(0, 30);
|
||||
if (matched.length === 0) {
|
||||
hideSuggest();
|
||||
return;
|
||||
}
|
||||
shopSuggest.innerHTML = matched.map((s) => `
|
||||
<button type="button" class="shop-suggest-item w-full text-left px-2 py-1.5 hover:bg-blue-50 text-xs border-b border-gray-100 whitespace-normal break-all" data-ds-idx="${s.ds_idx}">
|
||||
${mergedShopText(s)}
|
||||
</button>
|
||||
`).join('');
|
||||
shopSuggest.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function aggregateByBag() {
|
||||
const map = new Map();
|
||||
scannedRows.forEach((r) => {
|
||||
const key = String(r.bag_code || '');
|
||||
if (!map.has(key)) {
|
||||
map.set(key, {
|
||||
bag_code: key,
|
||||
bag_name: String(r.bag_name || ''),
|
||||
unit_price: Number(r.unit_price || 0),
|
||||
qty: 0,
|
||||
amount: 0,
|
||||
});
|
||||
}
|
||||
const item = map.get(key);
|
||||
item.qty += Number(r.qty || 0);
|
||||
item.amount += Number(r.amount || 0);
|
||||
});
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
function renderReturnList() {
|
||||
const rows = aggregateByBag();
|
||||
if (rows.length === 0) {
|
||||
returnListBody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-400 py-6">판매소를 선택하고 바코드를 스캔해 주세요.</td></tr>';
|
||||
document.getElementById('sum-qty').textContent = '0';
|
||||
document.getElementById('sum-amount').textContent = '0';
|
||||
return;
|
||||
}
|
||||
returnListBody.innerHTML = rows.map((r) => `
|
||||
<tr>
|
||||
<td class="text-center"><input type="radio" name="pick-return-bag" value="${r.bag_code}" ${selectedBagCode === r.bag_code ? 'checked' : ''}></td>
|
||||
<td class="text-left pl-2">${r.bag_code} ${r.bag_name || ''}</td>
|
||||
<td class="text-right pr-2">${nf(r.qty)}</td>
|
||||
<td class="text-right pr-2">${nf(r.unit_price)}</td>
|
||||
<td class="text-right pr-2">${nf(r.amount)}</td>
|
||||
<td class="text-center"><button type="button" class="btn-remove-bag text-red-600 hover:underline text-xs" data-bag-code="${r.bag_code}">제거</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
const sumQty = rows.reduce((s, r) => s + Number(r.qty || 0), 0);
|
||||
const sumAmount = rows.reduce((s, r) => s + Number(r.amount || 0), 0);
|
||||
document.getElementById('sum-qty').textContent = nf(sumQty);
|
||||
document.getElementById('sum-amount').textContent = nf(sumAmount);
|
||||
}
|
||||
|
||||
function renderCodeTable() {
|
||||
if (!selectedBagCode) {
|
||||
returnCodeBody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-400 py-6">품목을 선택해 주세요.</td></tr>';
|
||||
return;
|
||||
}
|
||||
const rows = scannedRows.filter((r) => String(r.bag_code || '') === selectedBagCode);
|
||||
if (rows.length === 0) {
|
||||
returnCodeBody.innerHTML = '<tr><td colspan="4" class="text-center text-gray-400 py-6">반품 봉투 코드가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
returnCodeBody.innerHTML = rows.map((r) => `
|
||||
<tr>
|
||||
<td class="text-left pl-2">${r.bag_code || ''} ${r.bag_name || ''}</td>
|
||||
<td class="text-center">${r.code || ''}</td>
|
||||
<td class="text-right pr-2">${nf(r.qty)}</td>
|
||||
<td class="text-right pr-2">${nf(r.amount)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function removeScannedByBagCode(bagCode) {
|
||||
if (!bagCode) return;
|
||||
for (let i = scannedRows.length - 1; i >= 0; i -= 1) {
|
||||
if (String(scannedRows[i].bag_code || '') === bagCode) scannedRows.splice(i, 1);
|
||||
}
|
||||
if (selectedBagCode === bagCode) selectedBagCode = '';
|
||||
saveScansJson.value = JSON.stringify(scannedRows);
|
||||
}
|
||||
|
||||
function applyResolvedShopFromScan(data) {
|
||||
const dsIdx = Number(data.ds_idx || 0);
|
||||
if (!dsIdx) return;
|
||||
let shop = shops.find((s) => Number(s.ds_idx) === dsIdx) || null;
|
||||
if (!shop) {
|
||||
shop = {
|
||||
ds_idx: dsIdx,
|
||||
ds_shop_no: String(data.ds_shop_no || ''),
|
||||
ds_name: String(data.ds_name || ''),
|
||||
ds_rep_name: String(data.ds_rep_name || ''),
|
||||
ds_tel: String(data.ds_tel || ''),
|
||||
ds_addr: String(data.ds_addr || ''),
|
||||
};
|
||||
}
|
||||
const prevIdx = selectedShop ? Number(selectedShop.ds_idx) : 0;
|
||||
if (prevIdx && prevIdx !== dsIdx && scannedRows.length > 0) {
|
||||
scannedRows.length = 0;
|
||||
saveScansJson.value = '[]';
|
||||
selectedBagCode = '';
|
||||
setMessage('바코드에 해당하는 지정판매소로 변경되어 이전 스캔 목록은 초기화되었습니다.', false);
|
||||
}
|
||||
selectedShop = shop;
|
||||
shopSearch.value = mergedShopText(shop);
|
||||
saveDsIdx.value = String(dsIdx);
|
||||
updateShopInfo(shop);
|
||||
}
|
||||
|
||||
async function submitScan() {
|
||||
const code = (barcodeInput.value || '').trim();
|
||||
if (!code) return;
|
||||
if (scannedRows.some((r) => String(r.code || '') === code)) {
|
||||
setMessage('이미 스캔한 바코드입니다.', true);
|
||||
barcodeInput.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
payload.set(csrfName, csrfHash);
|
||||
payload.set('ds_idx', selectedShop ? String(selectedShop.ds_idx) : '0');
|
||||
payload.set('barcode', code);
|
||||
|
||||
const res = await fetch(scanApi, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: payload.toString(),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
setMessage(data.message || '스캔 실패', true);
|
||||
return;
|
||||
}
|
||||
|
||||
applyResolvedShopFromScan(data);
|
||||
|
||||
scannedRows.push({
|
||||
code: data.code,
|
||||
bag_code: data.bag_code,
|
||||
bag_name: data.bag_name,
|
||||
qty: Number(data.qty || 0),
|
||||
unit: data.unit || '',
|
||||
unit_price: Number(data.unit_price || 0),
|
||||
amount: Number(data.amount || 0),
|
||||
so_idx: Number(data.so_idx || 0),
|
||||
});
|
||||
saveScansJson.value = JSON.stringify(scannedRows);
|
||||
barcodeInput.value = '';
|
||||
if (!selectedBagCode) selectedBagCode = String(data.bag_code || '');
|
||||
setMessage(`반품 코드 등록: ${data.code} / 수량 ${nf(data.qty)}`);
|
||||
renderReturnList();
|
||||
renderCodeTable();
|
||||
}
|
||||
|
||||
shopSearch?.addEventListener('input', (e) => renderSuggest(e.target.value || ''));
|
||||
shopSearch?.addEventListener('focus', (e) => renderSuggest(e.target.value || ''));
|
||||
shopSearch?.addEventListener('click', (e) => renderSuggest(e.target.value || ''));
|
||||
|
||||
shopSuggest?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.shop-suggest-item');
|
||||
if (!btn) return;
|
||||
const dsIdx = Number(btn.dataset.dsIdx || 0);
|
||||
const shop = shops.find((s) => Number(s.ds_idx) === dsIdx) || null;
|
||||
if (!shop) return;
|
||||
selectedShop = shop;
|
||||
shopSearch.value = mergedShopText(shop);
|
||||
saveDsIdx.value = String(shop.ds_idx);
|
||||
selectedBagCode = '';
|
||||
scannedRows.length = 0;
|
||||
saveScansJson.value = '[]';
|
||||
updateShopInfo(shop);
|
||||
renderReturnList();
|
||||
renderCodeTable();
|
||||
setMessage('');
|
||||
hideSuggest();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!shopSuggest || !shopSearch) return;
|
||||
if (e.target === shopSearch || shopSuggest.contains(e.target)) return;
|
||||
hideSuggest();
|
||||
});
|
||||
|
||||
barcodeInput?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
submitScan();
|
||||
}
|
||||
});
|
||||
|
||||
returnListBody?.addEventListener('change', (e) => {
|
||||
const radio = e.target.closest('input[name="pick-return-bag"]');
|
||||
if (!radio) return;
|
||||
selectedBagCode = radio.value || '';
|
||||
renderCodeTable();
|
||||
});
|
||||
|
||||
returnListBody?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.btn-remove-bag');
|
||||
if (!btn) return;
|
||||
const bagCode = String(btn.dataset.bagCode || '');
|
||||
if (!bagCode) return;
|
||||
if (!confirm(`'${bagCode}' 봉투종류의 스캔 내역을 모두 제거할까요?`)) return;
|
||||
removeScannedByBagCode(bagCode);
|
||||
renderReturnList();
|
||||
renderCodeTable();
|
||||
setMessage(`반품 리스트에서 ${bagCode} 봉투종류를 제거했습니다.`);
|
||||
});
|
||||
|
||||
saveForm?.addEventListener('submit', (e) => {
|
||||
if (!selectedShop) {
|
||||
e.preventDefault();
|
||||
alert('판매소를 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (scannedRows.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('반품할 바코드를 스캔해 주세요.');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
renderReturnList();
|
||||
renderCodeTable();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?= view('bag/_dev_all_sales_panel') ?>
|
||||
@@ -1,93 +1,288 @@
|
||||
<div class="space-y-1">
|
||||
<form method="get" class="flex items-center gap-3 text-sm mb-3">
|
||||
<label class="font-bold text-gray-700">조회기간</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<span>~</span>
|
||||
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/flow') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
</form>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<a href="<?= base_url('bag/receiving/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">입고 처리</a>
|
||||
<a href="<?= base_url('bag/sale/create') ?>" class="bg-white text-blue-600 border border-blue-300 px-3 py-1.5 rounded-sm text-sm">판매 등록</a>
|
||||
<a href="<?= base_url('bag/issue/create') ?>" class="bg-white text-blue-600 border border-blue-300 px-3 py-1.5 rounded-sm text-sm">불출 처리</a>
|
||||
</div>
|
||||
<?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 : [];
|
||||
$exportQuery = (string) ($exportQuery ?? 'search=1');
|
||||
$queried = (bool) ($queried ?? false);
|
||||
$fmt = static fn ($n): string => number_format((int) $n);
|
||||
|
||||
<!-- 수불 요약 -->
|
||||
<table class="data-table">
|
||||
$printExtraLines = [];
|
||||
if ($queried) {
|
||||
$aggLabel = $aggMode === 'daily' ? '일자별' : '기간별';
|
||||
$printExtraLines[] = '조회기간: ' . $startDate . ' ~ ' . $endDate . ' (' . $aggLabel . ')';
|
||||
}
|
||||
?>
|
||||
<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">기간별 봉투 수불 현황</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="<?= base_url('bag/flow/export?' . esc($exportQuery, 'attr')) ?>" class="bg-green-700 text-white px-3 py-1 rounded-sm text-sm hover:bg-green-800">엑셀저장</a>
|
||||
<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>
|
||||
<p class="text-xs text-gray-500 mt-1">전일재고 = 조회 시작일 전날 기준 품목별 재고(입고·반품·기타 − 출고 누적). 대행소 선택 시 <strong>판매</strong>만 해당 대행소 소속 판매소 기준입니다.</p>
|
||||
</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">봉투코드</th>
|
||||
<th rowspan="2">봉투명</th>
|
||||
<th rowspan="2">현재재고</th>
|
||||
<th colspan="2">입고</th>
|
||||
<th colspan="2">출고</th>
|
||||
<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>입고수량</th><th>반품수량</th>
|
||||
<th>판매수량</th><th>불출수량</th>
|
||||
<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>
|
||||
<?php
|
||||
// 봉투코드별 수불 집계
|
||||
$summary = [];
|
||||
// 재고
|
||||
foreach ($inventory as $inv) {
|
||||
$code = $inv->bi_bag_code ?? '';
|
||||
if (! isset($summary[$code])) {
|
||||
$summary[$code] = ['name' => $inv->bi_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
|
||||
}
|
||||
$summary[$code]['stock'] += (int)($inv->bi_qty_sheet ?? 0);
|
||||
}
|
||||
// 입고
|
||||
foreach ($receiving as $r) {
|
||||
$code = $r->br_bag_code ?? '';
|
||||
if (! isset($summary[$code])) {
|
||||
$summary[$code] = ['name' => $r->br_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
|
||||
}
|
||||
$summary[$code]['recv'] += (int)($r->br_qty_sheet ?? 0);
|
||||
}
|
||||
// 판매/반품
|
||||
foreach ($sales as $s) {
|
||||
$code = $s->bs_bag_code ?? '';
|
||||
if (! isset($summary[$code])) {
|
||||
$summary[$code] = ['name' => $s->bs_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
|
||||
}
|
||||
$type = $s->bs_type ?? 'sale';
|
||||
if ($type === 'return') {
|
||||
$summary[$code]['return'] += (int)($s->bs_qty ?? 0);
|
||||
} else {
|
||||
$summary[$code]['sale'] += (int)($s->bs_qty ?? 0);
|
||||
}
|
||||
}
|
||||
// 불출
|
||||
foreach ($issues as $iss) {
|
||||
$code = $iss->bi2_bag_code ?? '';
|
||||
if (! isset($summary[$code])) {
|
||||
$summary[$code] = ['name' => $iss->bi2_bag_name ?? '', 'stock' => 0, 'recv' => 0, 'return' => 0, 'sale' => 0, 'issue' => 0];
|
||||
}
|
||||
if (($iss->bi2_status ?? 'normal') === 'normal') {
|
||||
$summary[$code]['issue'] += (int)($iss->bi2_qty ?? 0);
|
||||
}
|
||||
}
|
||||
ksort($summary);
|
||||
?>
|
||||
<?php if (! empty($summary)): ?>
|
||||
<?php $idx = 0; foreach ($summary as $code => $s): $idx++; ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= esc($code) ?></td>
|
||||
<td><?= esc($s['name']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['stock']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['recv']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['return']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['sale']) ?></td>
|
||||
<td class="text-right"><?= number_format($s['issue']) ?></td>
|
||||
<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="7" class="text-center text-gray-400 py-4">수불 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?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>
|
||||
.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>
|
||||
|
||||
@@ -1,27 +1,122 @@
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span></span>
|
||||
<a href="<?= base_url('bag/inventory/adjust') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">재고 조정</a>
|
||||
<?= view('components/print_header', [
|
||||
'printTitle' => '재고 현황',
|
||||
'printDate' => (string) ($baseDate ?? date('Y-m-d')),
|
||||
'printExtraLines' => [
|
||||
'기준일자: ' . (string) ($baseDate ?? date('Y-m-d')),
|
||||
],
|
||||
]) ?>
|
||||
<?php
|
||||
$baseDate = (string) ($baseDate ?? date('Y-m-d'));
|
||||
$agencyIdx = (int) ($agencyIdx ?? 0);
|
||||
$rows = is_array($rows ?? null) ? $rows : [];
|
||||
$subtotals = is_array($subtotals ?? null) ? $subtotals : [];
|
||||
$grandTotals = is_array($grandTotals ?? null) ? $grandTotals : ['total' => 0, 'gugun' => 0, 'agency' => 0];
|
||||
$agencyOptions = is_array($agencyOptions ?? null) ? $agencyOptions : [];
|
||||
$subtotalByGroup = [];
|
||||
foreach ($subtotals as $subtotal) {
|
||||
$group = (string) ($subtotal['group'] ?? '');
|
||||
if ($group !== '') {
|
||||
$subtotalByGroup[$group] = $subtotal;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<form method="get" class="flex flex-wrap items-end justify-between gap-2">
|
||||
<div class="flex flex-wrap items-end gap-2 text-sm">
|
||||
<label class="font-bold text-gray-700">기준일자</label>
|
||||
<input type="date" name="base_date" value="<?= esc($baseDate) ?>" class="border border-gray-300 rounded px-2 py-1 min-w-[10rem]">
|
||||
|
||||
<label class="font-bold text-gray-700">대행소</label>
|
||||
<select name="agency_idx" class="border border-gray-300 rounded px-2 py-1 min-w-[12rem]">
|
||||
<option value="0">전체</option>
|
||||
<?php foreach ($agencyOptions as $agency): ?>
|
||||
<?php $idx = (int) ($agency->sa_idx ?? 0); ?>
|
||||
<option value="<?= esc((string) $idx) ?>" <?= $agencyIdx === $idx ? 'selected' : '' ?>>
|
||||
<?= esc((string) ($agency->sa_name ?? '')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/inventory/export?' . http_build_query(['base_date' => $baseDate, 'agency_idx' => $agencyIdx])) ?>" class="no-print border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||||
<button type="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>
|
||||
<a href="<?= base_url('bag/inventory/inspection-select') ?>" class="no-print border border-blue-300 text-blue-700 px-3 py-1 rounded-sm text-sm hover:bg-blue-50 transition">실사 선별 조회</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="mt-2 border border-gray-300 bg-white p-2 print:p-0 print:border-0">
|
||||
<div class="overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-36">품 목 구 분</th>
|
||||
<th>봉투/스티커 종류</th>
|
||||
<th class="w-32">계</th>
|
||||
<th class="w-32">시군구 재고</th>
|
||||
<th class="w-32">대행소 재고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($rows !== []): ?>
|
||||
<?php
|
||||
$groupRowCount = [];
|
||||
foreach ($rows as $row) {
|
||||
$group = (string) ($row['group'] ?? '');
|
||||
if (! isset($groupRowCount[$group])) {
|
||||
$groupRowCount[$group] = 0;
|
||||
}
|
||||
$groupRowCount[$group]++;
|
||||
}
|
||||
$printedGroupCount = [];
|
||||
?>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
<?php
|
||||
$group = (string) ($row['group'] ?? '');
|
||||
if (! isset($printedGroupCount[$group])) {
|
||||
$printedGroupCount[$group] = 0;
|
||||
}
|
||||
$printedGroupCount[$group]++;
|
||||
$isFirst = $printedGroupCount[$group] === 1;
|
||||
$isLast = $printedGroupCount[$group] === (int) ($groupRowCount[$group] ?? 0);
|
||||
?>
|
||||
<tr>
|
||||
<?php if ($isFirst): ?>
|
||||
<td class="text-center font-semibold bg-gray-50" rowspan="<?= esc((string) ($groupRowCount[$group] ?? 1)) ?>"><?= esc($group) ?></td>
|
||||
<?php endif; ?>
|
||||
<td class="pl-2"><?= esc((string) ($row['name'] ?? '')) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($row['total_qty'] ?? 0)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($row['gugun_qty'] ?? 0)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($row['agency_qty'] ?? 0)) ?></td>
|
||||
</tr>
|
||||
<?php if ($isLast && isset($subtotalByGroup[$group])): ?>
|
||||
<?php $s = $subtotalByGroup[$group]; ?>
|
||||
<tr class="bg-blue-50 font-semibold">
|
||||
<td class="text-center">소계</td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($s['total_qty'] ?? 0)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($s['gugun_qty'] ?? 0)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($s['agency_qty'] ?? 0)) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-4">조회 결과가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="bg-gray-100 font-bold">
|
||||
<td class="text-center" colspan="2">합계</td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($grandTotals['total'] ?? 0)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($grandTotals['gugun'] ?? 0)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($grandTotals['agency'] ?? 0)) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
<th class="w-16">번호</th><th>봉투코드</th><th>봉투명</th><th>현재재고(낱장)</th><th>최종갱신</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($list)): ?>
|
||||
<?php foreach ($list as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="text-center"><?= esc($row->bi_bag_code ?? '') ?></td>
|
||||
<td><?= esc($row->bi_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->bi_qty ?? 0)) ?></td>
|
||||
<td class="text-center"><?= esc($row->bi_updated_at ?? '') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="5" class="text-center text-gray-400 py-4">재고 데이터가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
※ 기준일자까지 갱신된 재고를 집계합니다. 대행소 재고는 별도 재고 연계 전까지 0으로 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<div class="max-w-lg">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-base font-bold text-gray-700">재고 수량 조정 (실사)</h3>
|
||||
</div>
|
||||
|
||||
<form action="<?= base_url('bag/inventory/adjust') ?>" method="POST" class="space-y-4">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">봉투코드 <span class="text-red-500">*</span></label>
|
||||
<select name="bag_code" required class="w-full border border-gray-300 rounded px-3 py-2 text-sm">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($inventory as $item): ?>
|
||||
<option value="<?= esc($item->bi_bag_code) ?>"><?= esc($item->bi_bag_code) ?> — <?= esc($item->bi_bag_name) ?> (현재: <?= number_format((int)$item->bi_qty) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">조정 유형 <span class="text-red-500">*</span></label>
|
||||
<select name="adjust_type" required class="w-full border border-gray-300 rounded px-3 py-2 text-sm">
|
||||
<option value="set">실사 수량으로 설정</option>
|
||||
<option value="add">증가 (+)</option>
|
||||
<option value="sub">감소 (-)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">수량 <span class="text-red-500">*</span></label>
|
||||
<input type="number" name="qty" required min="0" value="0" class="w-full border border-gray-300 rounded px-3 py-2 text-sm"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-1">사유</label>
|
||||
<input type="text" name="reason" placeholder="실사 조정, 오류 수정 등" class="w-full border border-gray-300 rounded px-3 py-2 text-sm"/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-2 rounded-sm text-sm">조정</button>
|
||||
<a href="<?= base_url('bag/inventory') ?>" class="bg-gray-200 text-gray-700 px-6 py-2 rounded-sm text-sm">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>스마트 폐기물 관리 시스템 - 재고 관리</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<style data-purpose="global-font-scale">html{font-size:18px}.app-brand,.app-brand *{font-size:16px}</style>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<style data-purpose="custom-styles">
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
|
||||
|
||||
90
app/Views/bag/inventory_inspection_detail.php
Normal file
90
app/Views/bag/inventory_inspection_detail.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
$inspection = is_array($inspection ?? null) ? $inspection : [];
|
||||
$items = is_array($items ?? null) ? $items : [];
|
||||
$status = (string) ($inspection['bis_status'] ?? '');
|
||||
?>
|
||||
<div class="space-y-3">
|
||||
<section class="border border-gray-300 bg-white p-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="text-sm text-gray-700">
|
||||
<span class="font-bold">실사번호:</span> <?= esc((string) ($inspection['bis_idx'] ?? '')) ?>
|
||||
<span class="mx-2">|</span>
|
||||
<span class="font-bold">작업일자:</span> <?= esc((string) ($inspection['bis_work_date'] ?? '')) ?>
|
||||
<span class="mx-2">|</span>
|
||||
<span class="font-bold">상태:</span> <?= esc($status) ?>
|
||||
</div>
|
||||
<a href="<?= base_url('bag/inventory/inspection-select') ?>" class="text-sm text-blue-700 hover:underline">실사 선별 조회로 돌아가기</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="border border-gray-300 bg-white p-3">
|
||||
<form method="post" action="<?= base_url('bag/inventory/inspection/' . (int) ($inspection['bis_idx'] ?? 0) . '/save') ?>" class="space-y-3">
|
||||
<?= csrf_field() ?>
|
||||
<div class="overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">번호</th>
|
||||
<th>품목명</th>
|
||||
<th class="w-24">전산수량</th>
|
||||
<th class="w-24">실사수량</th>
|
||||
<th class="w-24">차이수량</th>
|
||||
<th class="w-20">반영여부</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($items !== []): ?>
|
||||
<?php foreach ($items as $i => $row): ?>
|
||||
<?php
|
||||
$itemId = (int) ($row['bisi_idx'] ?? 0);
|
||||
$systemQty = (int) ($row['bisi_system_qty'] ?? 0);
|
||||
$actualQty = $row['bisi_actual_qty'];
|
||||
$actualQty = $actualQty === null ? '' : (string) ((int) $actualQty);
|
||||
$diffQty = (int) ($row['bisi_diff_qty'] ?? 0);
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="pl-2"><?= esc((string) ($row['bisi_bag_name'] ?? '')) ?> <span class="text-xs text-gray-400">(<?= esc((string) ($row['bisi_bag_code'] ?? '')) ?>)</span></td>
|
||||
<td class="text-right pr-2"><?= number_format($systemQty) ?></td>
|
||||
<td class="text-right pr-2">
|
||||
<input type="number" min="0" name="actual_qty[<?= esc((string) $itemId, 'attr') ?>]" value="<?= esc($actualQty) ?>" class="border border-gray-300 rounded px-1 py-0.5 w-24 text-right">
|
||||
</td>
|
||||
<td class="text-right pr-2 <?= $diffQty === 0 ? '' : ($diffQty > 0 ? 'text-blue-700' : 'text-red-700') ?>"><?= number_format($diffQty) ?></td>
|
||||
<td class="text-center"><?= ((string) ($row['bisi_apply_yn'] ?? 'N')) === 'Y' ? 'Y' : 'N' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="6" class="text-center text-gray-400 py-4">실사 품목이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-5 py-1.5 rounded-sm text-sm">실사 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="border border-gray-300 bg-white p-3">
|
||||
<form method="post" action="<?= base_url('bag/inventory/inspection/' . (int) ($inspection['bis_idx'] ?? 0) . '/apply') ?>" id="inspection-apply-form">
|
||||
<?= csrf_field() ?>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-gray-600">실사 저장 후 확정 시 차이수량이 현재 재고에 반영됩니다.</p>
|
||||
<button type="submit" class="bg-green-600 text-white px-5 py-1.5 rounded-sm text-sm">실사 확정(재고 반영)</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const form = document.getElementById('inspection-apply-form');
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', (event) => {
|
||||
const ok = window.confirm('실사 결과를 재고에 반영하시겠습니까?');
|
||||
if (!ok) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -63,11 +63,7 @@ $sheetTotalDiff = $sheetTotalActual - $sheetTotalQty;
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<label class="font-bold text-gray-700 ml-2">조회구분</label>
|
||||
<select name="view_type" class="border border-gray-300 rounded px-2 py-1 min-w-[7rem]">
|
||||
<option value="box" <?= $viewType === 'box' ? 'selected' : '' ?>>박스별</option>
|
||||
<option value="pack" <?= $viewType === 'pack' ? 'selected' : '' ?>>팩별</option>
|
||||
</select>
|
||||
<input type="hidden" name="view_type" value="<?= esc($viewType) ?>">
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-3 py-1 rounded-sm">조회</button>
|
||||
|
||||
@@ -1,54 +1,409 @@
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<form method="get" class="flex items-center gap-3 text-sm">
|
||||
<label class="font-bold text-gray-700">불출일</label>
|
||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<span>~</span>
|
||||
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
||||
<a href="<?= base_url('bag/issue') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
<?= view('components/print_header', ['printTitle' => '무료용 불출 취소']) ?>
|
||||
<?php
|
||||
$filters = is_array($filters ?? null) ? $filters : [];
|
||||
$issueMonth = (string) ($filters['issue_month'] ?? '');
|
||||
$destName = (string) ($filters['dest_name'] ?? '');
|
||||
$issueType = (string) ($filters['issue_type'] ?? '');
|
||||
$bagCode = (string) ($filters['bag_code'] ?? '');
|
||||
$selectedGroupDate = (string) ($selectedGroupDate ?? '');
|
||||
$selectedGroupDest = (string) ($selectedGroupDest ?? '');
|
||||
$selectedIssueId = (int) ($selectedIssueId ?? 0);
|
||||
$selectedBagCode = (string) ($selectedBagCode ?? '');
|
||||
?>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="border border-gray-300 bg-white p-2">
|
||||
<form method="get" class="flex flex-wrap items-end gap-2 text-sm">
|
||||
<label class="font-bold text-gray-700">불출월</label>
|
||||
<select name="issue_month" class="border border-gray-300 rounded px-2 py-1 min-w-[9rem]">
|
||||
<option value="">전체</option>
|
||||
<?php foreach (($monthOptions ?? []) as $month): ?>
|
||||
<option value="<?= esc($month) ?>" <?= $issueMonth === $month ? 'selected' : '' ?>><?= esc($month) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<label class="font-bold text-gray-700">불출처</label>
|
||||
<select name="dest_name" class="border border-gray-300 rounded px-2 py-1 min-w-[10rem]">
|
||||
<option value="">전체</option>
|
||||
<?php foreach (($destOptions ?? []) as $opt): ?>
|
||||
<option value="<?= esc($opt) ?>" <?= $destName === $opt ? 'selected' : '' ?>><?= esc($opt) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<label class="font-bold text-gray-700">불출구분</label>
|
||||
<select name="issue_type" class="border border-gray-300 rounded px-2 py-1 min-w-[8rem]">
|
||||
<option value="">전체</option>
|
||||
<?php foreach (($typeOptions ?? []) as $opt): ?>
|
||||
<option value="<?= esc($opt) ?>" <?= $issueType === $opt ? 'selected' : '' ?>><?= esc($opt) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<label class="font-bold text-gray-700">봉투종류</label>
|
||||
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 min-w-[12rem]">
|
||||
<option value="">전체</option>
|
||||
<?php foreach (($bagOptions ?? []) as $opt): ?>
|
||||
<?php $code = (string) ($opt['code'] ?? ''); ?>
|
||||
<option value="<?= esc($code) ?>" <?= $bagCode === $code ? 'selected' : '' ?>>
|
||||
<?= esc($code) ?> - <?= esc((string) ($opt['name'] ?? '')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<input type="hidden" name="sel_date" value="<?= esc($selectedGroupDate) ?>"/>
|
||||
<input type="hidden" name="sel_dest" value="<?= esc($selectedGroupDest) ?>"/>
|
||||
<input type="hidden" name="sel_issue_id" value="<?= esc((string) $selectedIssueId) ?>"/>
|
||||
<input type="hidden" name="sel_bag_code" value="<?= esc($selectedBagCode) ?>"/>
|
||||
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
||||
<a href="<?= base_url('bag/issue/cancel') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
<button type="button" onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-4 py-1 rounded-sm hover:bg-gray-50">인쇄</button>
|
||||
<a href="<?= base_url('bag/issue/create') ?>" class="bg-btn-search text-white px-3 py-1 rounded-sm">불출 처리</a>
|
||||
</form>
|
||||
<a href="<?= base_url('bag/issue/create') ?>" class="bg-btn-search text-white px-3 py-1.5 rounded-sm text-sm">불출 처리</a>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
<thead><tr>
|
||||
<th class="w-16">번호</th><th>연도</th><th>분기</th><th>구분</th><th>불출일</th><th>불출처</th><th>봉투코드</th><th>봉투명</th><th>수량</th><th>상태</th><th>작업</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php if (! empty($list)): ?>
|
||||
<?php foreach ($list as $i => $row): ?>
|
||||
<tr>
|
||||
<td class="text-center"><?= $i + 1 ?></td>
|
||||
<td class="text-center"><?= esc($row->bi2_year ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bi2_quarter ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bi2_issue_type ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bi2_issue_date ?? '') ?></td>
|
||||
<td><?= esc($row->bi2_dest_name ?? '') ?></td>
|
||||
<td class="text-center"><?= esc($row->bi2_bag_code ?? '') ?></td>
|
||||
<td><?= esc($row->bi2_bag_name ?? '') ?></td>
|
||||
<td class="text-right"><?= number_format((int)($row->bi2_qty ?? 0)) ?></td>
|
||||
<td class="text-center">
|
||||
<?php
|
||||
$st = $row->bi2_status ?? 'normal';
|
||||
echo match($st) { 'normal' => '정상', 'cancelled' => '<span class="text-orange-600">취소</span>', default => esc($st) };
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php if (($row->bi2_status ?? '') === 'normal'): ?>
|
||||
<form method="post" action="<?= base_url('bag/issue/cancel/' . $row->bi2_idx) ?>" class="inline" onsubmit="return confirm('취소하시겠습니까?')">
|
||||
<?= csrf_field() ?>
|
||||
<button class="text-orange-600 hover:underline text-xs">취소</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="11" class="text-center text-gray-400 py-4">등록된 불출이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<form method="post" action="<?= base_url('bag/issue/cancel-save') ?>" id="issue-cancel-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="issue_month" value="<?= esc($issueMonth) ?>"/>
|
||||
<input type="hidden" name="dest_name" value="<?= esc($destName) ?>"/>
|
||||
<input type="hidden" name="issue_type" value="<?= esc($issueType) ?>"/>
|
||||
<input type="hidden" name="bag_code" value="<?= esc($bagCode) ?>"/>
|
||||
<input type="hidden" name="sel_date" value="<?= esc($selectedGroupDate) ?>"/>
|
||||
<input type="hidden" name="sel_dest" value="<?= esc($selectedGroupDest) ?>"/>
|
||||
<input type="hidden" name="sel_issue_id" value="<?= esc((string) $selectedIssueId) ?>"/>
|
||||
<input type="hidden" name="sel_bag_code" value="<?= esc($selectedBagCode) ?>"/>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-4 gap-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-[560px]">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-20">불출일자</th>
|
||||
<th>불출처</th>
|
||||
<th class="w-16">건수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (($issueGroups ?? []) !== []): ?>
|
||||
<?php foreach (($issueGroups ?? []) as $row): ?>
|
||||
<?php
|
||||
$date = (string) ($row['bi2_issue_date'] ?? '');
|
||||
$dest = (string) ($row['bi2_dest_name'] ?? '');
|
||||
$isSelected = ($date === $selectedGroupDate && $dest === $selectedGroupDest);
|
||||
$url = base_url('bag/issue/cancel?' . http_build_query([
|
||||
'issue_month' => $issueMonth,
|
||||
'dest_name' => $destName,
|
||||
'issue_type' => $issueType,
|
||||
'bag_code' => $bagCode,
|
||||
'sel_date' => $date,
|
||||
'sel_dest' => $dest,
|
||||
]));
|
||||
?>
|
||||
<tr
|
||||
class="<?= $isSelected ? 'bg-blue-100 font-semibold' : '' ?> cursor-pointer hover:bg-blue-50"
|
||||
onclick="window.location.href='<?= esc($url, 'attr') ?>'"
|
||||
>
|
||||
<td class="text-center <?= $isSelected ? 'border-l-4 border-blue-600' : '' ?>">
|
||||
<?= esc($date) ?>
|
||||
</td>
|
||||
<td class="pl-2"><?= esc($dest) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($row['row_count'] ?? 0)) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="3" class="text-center text-gray-400 py-4">조회 결과가 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="xl:col-span-3 grid grid-cols-1 gap-2">
|
||||
<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-[270px]">
|
||||
<table class="w-full data-table text-sm" id="detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">취소</th>
|
||||
<th class="w-12">No</th>
|
||||
<th class="w-24">일자</th>
|
||||
<th class="w-20">구분</th>
|
||||
<th>봉투종류</th>
|
||||
<th class="w-24">수량</th>
|
||||
<th class="w-24">취소수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$detailTotal = 0;
|
||||
$detailCancelTotal = 0;
|
||||
?>
|
||||
<?php if (($detailRows ?? []) !== []): ?>
|
||||
<?php foreach (($detailRows ?? []) as $idx => $row): ?>
|
||||
<?php
|
||||
$qty = (int) ($row['base_qty'] ?? 0);
|
||||
$cancelQty = (int) ($row['cancel_qty'] ?? 0);
|
||||
$detailTotal += $qty;
|
||||
$detailCancelTotal += $cancelQty;
|
||||
$isChecked = $cancelQty >= $qty && $qty > 0;
|
||||
$rowBagCode = (string) ($row['bi2_bag_code'] ?? '');
|
||||
$selectedLink = base_url('bag/issue/cancel?' . http_build_query([
|
||||
'issue_month' => $issueMonth,
|
||||
'dest_name' => $destName,
|
||||
'issue_type' => $issueType,
|
||||
'bag_code' => $bagCode,
|
||||
'sel_date' => $selectedGroupDate,
|
||||
'sel_dest' => $selectedGroupDest,
|
||||
'sel_issue_id' => $selectedIssueId,
|
||||
'sel_bag_code' => $rowBagCode,
|
||||
]));
|
||||
?>
|
||||
<tr data-bag-code="<?= esc($rowBagCode, 'attr') ?>" class="<?= $selectedBagCode === $rowBagCode ? 'bg-blue-50' : '' ?>">
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="detail-check" value="1" <?= $isChecked ? 'checked' : '' ?>/>
|
||||
</td>
|
||||
<td class="text-center"><?= $idx + 1 ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['bi2_issue_date'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['bi2_issue_type'] ?? '')) ?></td>
|
||||
<td class="pl-2">
|
||||
<a href="<?= esc($selectedLink) ?>" class="text-blue-700 hover:underline">
|
||||
<?= esc((string) ($row['bi2_bag_name'] ?? '')) ?> (<?= esc((string) ($row['bi2_bag_code'] ?? '')) ?>)
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right pr-2 detail-qty"><?= number_format($qty) ?></td>
|
||||
<td class="text-right pr-2">
|
||||
<input type="number" min="0" max="<?= esc((string) $qty) ?>" class="detail-cancel-input border border-gray-300 rounded px-1 py-0.5 w-24 text-right"
|
||||
data-max="<?= esc((string) $qty) ?>"
|
||||
data-bag-code="<?= esc($rowBagCode, 'attr') ?>"
|
||||
value="<?= esc((string) $cancelQty) ?>"/>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="7" class="text-center text-gray-400 py-4">불출 품목 내역이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="bg-gray-50 font-semibold">
|
||||
<td colspan="5" class="text-center">합계</td>
|
||||
<td class="text-right pr-2" id="detail-total-qty"><?= number_format($detailTotal) ?></td>
|
||||
<td class="text-right pr-2" id="detail-total-cancel"><?= number_format($detailCancelTotal) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</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-[250px]">
|
||||
<table class="w-full data-table text-sm" id="code-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">취소</th>
|
||||
<th class="w-12">No</th>
|
||||
<th class="w-20">불출번호</th>
|
||||
<th>봉투코드</th>
|
||||
<th class="w-24">수량</th>
|
||||
<th class="w-24">취소수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$codeTotal = 0;
|
||||
$codeCancelTotal = 0;
|
||||
?>
|
||||
<?php if (($codeRows ?? []) !== []): ?>
|
||||
<?php foreach (($codeRows ?? []) as $idx => $row): ?>
|
||||
<?php
|
||||
$bicIdx = (int) ($row['bic_idx'] ?? 0);
|
||||
$qty = (int) ($row['bic_qty'] ?? 0);
|
||||
$cancelQty = (int) ($row['bic_cancel_qty'] ?? 0);
|
||||
$codeTotal += $qty;
|
||||
$codeCancelTotal += $cancelQty;
|
||||
$isChecked = $cancelQty >= $qty && $qty > 0;
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="code-check"
|
||||
<?= $bicIdx > 0 ? 'name="code_cancel_check[' . esc((string) $bicIdx, 'attr') . ']"' : '' ?>
|
||||
value="1" <?= $isChecked ? 'checked' : '' ?>/>
|
||||
</td>
|
||||
<td class="text-center"><?= $idx + 1 ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['bic_bi2_idx'] ?? '')) ?></td>
|
||||
<td class="pl-2"><?= esc((string) ($row['bic_issue_code'] ?? '')) ?></td>
|
||||
<td class="text-right pr-2 code-qty"><?= number_format($qty) ?></td>
|
||||
<td class="text-right pr-2">
|
||||
<input type="number" min="0" max="<?= esc((string) $qty) ?>" class="code-cancel-input border border-gray-300 rounded px-1 py-0.5 w-24 text-right"
|
||||
data-max="<?= esc((string) $qty) ?>"
|
||||
data-bag-code="<?= esc($selectedBagCode, 'attr') ?>"
|
||||
<?= $bicIdx > 0 ? 'name="code_cancel_qty[' . esc((string) $bicIdx, 'attr') . ']"' : '' ?>
|
||||
value="<?= esc((string) $cancelQty) ?>"/>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr><td colspan="6" class="text-center text-gray-400 py-4">불출 품목 코드 내역이 없습니다.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="bg-gray-50 font-semibold">
|
||||
<td colspan="4" class="text-center">합계</td>
|
||||
<td class="text-right pr-2" id="code-total-qty"><?= number_format($codeTotal) ?></td>
|
||||
<td class="text-right pr-2" id="code-total-cancel"><?= number_format($codeCancelTotal) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-end gap-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const numberFormat = new Intl.NumberFormat('ko-KR');
|
||||
const detailInputs = Array.from(document.querySelectorAll('.detail-cancel-input'));
|
||||
const detailChecks = Array.from(document.querySelectorAll('.detail-check'));
|
||||
const codeInputs = Array.from(document.querySelectorAll('.code-cancel-input'));
|
||||
const codeChecks = Array.from(document.querySelectorAll('.code-check'));
|
||||
|
||||
const clamp = (value, max) => {
|
||||
const n = parseInt(String(value || '0'), 10) || 0;
|
||||
return Math.max(0, Math.min(max, n));
|
||||
};
|
||||
|
||||
const syncDetailTotals = () => {
|
||||
let qtySum = 0;
|
||||
let cancelSum = 0;
|
||||
document.querySelectorAll('#detail-table tbody tr').forEach((tr) => {
|
||||
const qtyCell = tr.querySelector('.detail-qty');
|
||||
const input = tr.querySelector('.detail-cancel-input');
|
||||
if (!qtyCell || !input) return;
|
||||
const qty = parseInt(qtyCell.textContent.replace(/,/g, ''), 10) || 0;
|
||||
const cancel = clamp(input.value, parseInt(input.dataset.max || '0', 10) || 0);
|
||||
qtySum += qty;
|
||||
cancelSum += cancel;
|
||||
});
|
||||
const totalQty = document.getElementById('detail-total-qty');
|
||||
const totalCancel = document.getElementById('detail-total-cancel');
|
||||
if (totalQty) totalQty.textContent = numberFormat.format(qtySum);
|
||||
if (totalCancel) totalCancel.textContent = numberFormat.format(cancelSum);
|
||||
};
|
||||
|
||||
const syncCodeTotals = () => {
|
||||
let qtySum = 0;
|
||||
let cancelSum = 0;
|
||||
document.querySelectorAll('#code-table tbody tr').forEach((tr) => {
|
||||
const qtyCell = tr.querySelector('.code-qty');
|
||||
if (!qtyCell) return;
|
||||
const qty = parseInt(qtyCell.textContent.replace(/,/g, ''), 10) || 0;
|
||||
qtySum += qty;
|
||||
const input = tr.querySelector('.code-cancel-input');
|
||||
if (input) {
|
||||
cancelSum += clamp(input.value, parseInt(input.dataset.max || '0', 10) || 0);
|
||||
}
|
||||
});
|
||||
const totalQty = document.getElementById('code-total-qty');
|
||||
const totalCancel = document.getElementById('code-total-cancel');
|
||||
if (totalQty) totalQty.textContent = numberFormat.format(qtySum);
|
||||
if (totalCancel) totalCancel.textContent = numberFormat.format(cancelSum);
|
||||
};
|
||||
|
||||
const syncSelectedBagCancelFromCodes = () => {
|
||||
const codeInput = codeInputs[0];
|
||||
if (!codeInput) return;
|
||||
const bagCode = codeInput.dataset.bagCode || '';
|
||||
if (bagCode === '') return;
|
||||
|
||||
let cancelSum = 0;
|
||||
codeInputs.forEach((input) => {
|
||||
const max = parseInt(input.dataset.max || '0', 10) || 0;
|
||||
const value = clamp(input.value, max);
|
||||
input.value = String(value);
|
||||
cancelSum += value;
|
||||
});
|
||||
const detailInput = document.querySelector(`.detail-cancel-input[data-bag-code="${bagCode}"]`);
|
||||
if (detailInput) {
|
||||
const max = parseInt(detailInput.dataset.max || '0', 10) || 0;
|
||||
detailInput.value = String(clamp(cancelSum, max));
|
||||
const check = detailInput.closest('tr')?.querySelector('.detail-check');
|
||||
if (check) {
|
||||
check.checked = (clamp(cancelSum, max) >= max && max > 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
detailInputs.forEach((input) => {
|
||||
input.addEventListener('input', () => {
|
||||
const max = parseInt(input.dataset.max || '0', 10) || 0;
|
||||
input.value = String(clamp(input.value, max));
|
||||
syncDetailTotals();
|
||||
});
|
||||
});
|
||||
|
||||
detailChecks.forEach((check) => {
|
||||
check.addEventListener('change', () => {
|
||||
const tr = check.closest('tr');
|
||||
const input = tr?.querySelector('.detail-cancel-input');
|
||||
if (!input) return;
|
||||
const max = parseInt(input.dataset.max || '0', 10) || 0;
|
||||
input.value = check.checked ? String(max) : '0';
|
||||
|
||||
const bagCode = input.dataset.bagCode || '';
|
||||
if (bagCode !== '') {
|
||||
codeInputs.forEach((codeInput) => {
|
||||
if ((codeInput.dataset.bagCode || '') !== bagCode) return;
|
||||
const codeMax = parseInt(codeInput.dataset.max || '0', 10) || 0;
|
||||
codeInput.value = check.checked ? String(codeMax) : '0';
|
||||
const codeCheck = codeInput.closest('tr')?.querySelector('.code-check');
|
||||
if (codeCheck) {
|
||||
codeCheck.checked = check.checked;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
syncCodeTotals();
|
||||
syncSelectedBagCancelFromCodes();
|
||||
syncDetailTotals();
|
||||
});
|
||||
});
|
||||
|
||||
codeInputs.forEach((input) => {
|
||||
input.addEventListener('input', () => {
|
||||
const max = parseInt(input.dataset.max || '0', 10) || 0;
|
||||
input.value = String(clamp(input.value, max));
|
||||
const check = input.closest('tr')?.querySelector('.code-check');
|
||||
if (check) {
|
||||
check.checked = clamp(input.value, max) >= max && max > 0;
|
||||
}
|
||||
syncCodeTotals();
|
||||
syncSelectedBagCancelFromCodes();
|
||||
syncDetailTotals();
|
||||
});
|
||||
});
|
||||
|
||||
codeChecks.forEach((check) => {
|
||||
check.addEventListener('change', () => {
|
||||
const input = check.closest('tr')?.querySelector('.code-cancel-input');
|
||||
if (!input) return;
|
||||
const max = parseInt(input.dataset.max || '0', 10) || 0;
|
||||
input.value = check.checked ? String(max) : '0';
|
||||
syncCodeTotals();
|
||||
syncSelectedBagCancelFromCodes();
|
||||
syncDetailTotals();
|
||||
});
|
||||
});
|
||||
|
||||
syncCodeTotals();
|
||||
syncSelectedBagCancelFromCodes();
|
||||
syncDetailTotals();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -42,6 +42,12 @@ tailwind.config = {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style data-purpose="global-font-scale">
|
||||
/* 전체 텍스트 +2px 확대 (요청). rem 기반 텍스트는 비례 확대된다.
|
||||
다만 헤더 로고(.app-brand)는 원래 크기 유지하기 위해 root 기준 16px 로 reset. */
|
||||
html { font-size: 18px; }
|
||||
.app-brand, .app-brand * { font-size: 16px; }
|
||||
</style>
|
||||
<style data-purpose="table-layout">
|
||||
.data-table { width: 100%; border-collapse: collapse; font-family: 'Malgun Gothic', 'Noto Sans KR', sans-serif; }
|
||||
.data-table th, .data-table td { border: 1px solid #ccc; padding: 4px 8px; white-space: nowrap; font-size: 13px; }
|
||||
@@ -60,7 +66,7 @@ body { overflow: hidden; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
|
||||
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased">
|
||||
<!-- BEGIN: Top Navigation -->
|
||||
<header class="relative bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-[100]">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -136,5 +142,44 @@ body { overflow: hidden; }
|
||||
<span>종량제 시스템</span>
|
||||
<span><?= date('Y.m.d (D) g:i:sA') ?></span>
|
||||
</footer>
|
||||
<script>
|
||||
(() => {
|
||||
const normalize = (s) => String(s || '').replace(/\s+/g, '').trim();
|
||||
const renumberTable = (table) => {
|
||||
const headRow = table.querySelector('thead tr');
|
||||
if (!headRow) return;
|
||||
const headers = Array.from(headRow.querySelectorAll('th'));
|
||||
const numberCol = headers.findIndex((th) => normalize(th.textContent) === '번호');
|
||||
if (numberCol < 0) return;
|
||||
|
||||
const body = table.querySelector('tbody');
|
||||
if (!body) return;
|
||||
const rows = Array.from(body.querySelectorAll(':scope > tr')).filter((tr) => {
|
||||
const cells = tr.querySelectorAll('td');
|
||||
if (cells.length === 0) return false;
|
||||
if (cells.length === 1 && Number(cells[0].getAttribute('colspan') || '1') > 1) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
let no = rows.length;
|
||||
rows.forEach((tr) => {
|
||||
const cells = tr.querySelectorAll('td');
|
||||
if (cells[numberCol]) {
|
||||
cells[numberCol].textContent = String(no--);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const run = () => {
|
||||
document.querySelectorAll('table').forEach(renumberTable);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run, { once: true });
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -15,6 +15,7 @@ $mbName = session()->get('mb_name') ?? '담당자';
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>종량제 시스템 — 업무 현황</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<style data-purpose="global-font-scale">html{font-size:18px}.app-brand,.app-brand *{font-size:16px}</style>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
body {
|
||||
|
||||
@@ -20,6 +20,7 @@ $dashBlend = base_url('dashboard/blend');
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>종량제 시스템 — 통계·그래프 현황</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<style data-purpose="global-font-scale">html{font-size:18px}.app-brand,.app-brand *{font-size:16px}</style>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
|
||||
212
app/Views/bag/lg_dashboard_compact.php
Normal file
212
app/Views/bag/lg_dashboard_compact.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
/**
|
||||
* 로그인 후 메인 — 컴팩트 대시보드(중간 밀도)
|
||||
* /dashboard 대비 요소 수를 줄이고, /dashboard/simple 대비 정보량은 늘린 버전.
|
||||
*
|
||||
* @var string $lgLabel
|
||||
*/
|
||||
$lgLabel = $lgLabel ?? '북구';
|
||||
$mbName = session()->get('mb_name') ?? '담당자';
|
||||
|
||||
$kpi = [
|
||||
['label' => '미처리 구매신청', 'value' => '12건', 'hint' => '전일 대비 +2'],
|
||||
['label' => '재고 부족 품목', 'value' => '3종', 'hint' => '안전재고 미달'],
|
||||
['label' => '금주 입고 완료', 'value' => '8건', 'hint' => '예정 2건'],
|
||||
['label' => '승인 대기', 'value' => '4명', 'hint' => '판매소/회원'],
|
||||
];
|
||||
|
||||
$weeklyRequest = [8, 11, 9, 14, 10, 12, 7];
|
||||
$monthlyOut = [320, 340, 330, 355, 372, 361, 388, 396];
|
||||
$inventoryRows = [
|
||||
['일반 5L', '12,400', '안전'],
|
||||
['일반 10L', '8,200', '주의'],
|
||||
['일반 20L', '2,100', '부족'],
|
||||
['음식물 스티커', '15,000', '안전'],
|
||||
['특수규격 A', '890', '부족'],
|
||||
];
|
||||
$requestRows = [
|
||||
['2026-05-07 10:32', '행복마트 북구점', '일반 5L 2,000장', '접수'],
|
||||
['2026-05-07 09:40', 'OO슈퍼', '음식물 스티커 500매', '처리중'],
|
||||
['2026-05-07 08:55', 'XX상회', '일반 20L 1,000장', '발주확인'],
|
||||
['2026-05-06 17:21', 'YY마트', '재사용봉투 800장', '완료'],
|
||||
];
|
||||
?>
|
||||
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
|
||||
<div class="bg-[#f5f7fb] -mx-4 -my-4 p-3 min-h-full">
|
||||
<section class="bg-white border border-gray-200 rounded-lg shadow-sm px-4 py-3 mb-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-gray-900">업무 현황 컴팩트 뷰</h2>
|
||||
<p class="text-[11px] text-gray-500 mt-0.5">
|
||||
<?= esc($lgLabel) ?> · <?= esc($mbName) ?>님 기준 · 핵심 지표와 추이만 표시
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-[11px] text-gray-500">
|
||||
기준 시각 <?= date('Y-m-d H:i') ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-2 lg:grid-cols-4 gap-2 mb-3">
|
||||
<?php foreach ($kpi as $card): ?>
|
||||
<article class="bg-white border border-gray-200 rounded-lg shadow-sm p-3">
|
||||
<p class="text-[11px] text-gray-500"><?= esc($card['label']) ?></p>
|
||||
<p class="text-xl font-bold text-gray-900 mt-1"><?= esc($card['value']) ?></p>
|
||||
<p class="text-[10px] text-gray-400 mt-1"><?= esc($card['hint']) ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-1 xl:grid-cols-2 gap-3 mb-3">
|
||||
<article class="bg-white border border-gray-200 rounded-lg shadow-sm p-3">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-xs font-semibold text-gray-800">최근 7일 구매신청 추이</h3>
|
||||
<span class="text-[10px] text-gray-400">건수</span>
|
||||
</div>
|
||||
<div class="h-48">
|
||||
<canvas id="compactRequestChart"></canvas>
|
||||
</div>
|
||||
</article>
|
||||
<article class="bg-white border border-gray-200 rounded-lg shadow-sm p-3">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-xs font-semibold text-gray-800">월별 출고량 추이</h3>
|
||||
<span class="text-[10px] text-gray-400">천 장</span>
|
||||
</div>
|
||||
<div class="h-48">
|
||||
<canvas id="compactOutChart"></canvas>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-1 xl:grid-cols-2 gap-3">
|
||||
<article class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-3 py-2 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
|
||||
<h3 class="text-xs font-semibold text-gray-800">재고 상태 요약</h3>
|
||||
<a href="<?= base_url('bag/inventory') ?>" class="text-[10px] text-blue-600 hover:underline">상세</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="bg-gray-100 text-gray-600">
|
||||
<tr>
|
||||
<th class="text-left px-3 py-2">품목</th>
|
||||
<th class="text-right px-3 py-2">재고(장)</th>
|
||||
<th class="text-center px-3 py-2">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<?php foreach ($inventoryRows as $row): ?>
|
||||
<tr>
|
||||
<td class="px-3 py-2"><?= esc($row[0]) ?></td>
|
||||
<td class="px-3 py-2 text-right"><?= esc($row[1]) ?></td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<?php $stateClass = $row[2] === '부족' ? 'text-red-600' : ($row[2] === '주의' ? 'text-amber-600' : 'text-emerald-600'); ?>
|
||||
<span class="<?= $stateClass ?> font-medium"><?= esc($row[2]) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-3 py-2 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
|
||||
<h3 class="text-xs font-semibold text-gray-800">최근 구매신청 처리 현황</h3>
|
||||
<a href="<?= base_url('bag/order/create') ?>" class="text-[10px] text-blue-600 hover:underline">등록</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="bg-gray-100 text-gray-600">
|
||||
<tr>
|
||||
<th class="text-left px-3 py-2">시각</th>
|
||||
<th class="text-left px-3 py-2">판매소</th>
|
||||
<th class="text-left px-3 py-2">신청 내용</th>
|
||||
<th class="text-center px-3 py-2">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
<?php foreach ($requestRows as $row): ?>
|
||||
<tr>
|
||||
<td class="px-3 py-2 whitespace-nowrap"><?= esc($row[0]) ?></td>
|
||||
<td class="px-3 py-2"><?= esc($row[1]) ?></td>
|
||||
<td class="px-3 py-2"><?= esc($row[2]) ?></td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<span class="inline-flex px-2 py-0.5 rounded text-[10px] bg-slate-100 text-slate-700"><?= esc($row[3]) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const style = {
|
||||
blue: '#2563eb',
|
||||
blueLight: 'rgba(37, 99, 235, 0.15)',
|
||||
teal: '#0f766e',
|
||||
grid: 'rgba(0, 0, 0, 0.06)',
|
||||
};
|
||||
|
||||
Chart.defaults.font.family = "'Malgun Gothic','Apple SD Gothic Neo','Noto Sans KR',sans-serif";
|
||||
Chart.defaults.font.size = 11;
|
||||
Chart.defaults.color = '#4b5563';
|
||||
|
||||
new Chart(document.getElementById('compactRequestChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['D-6', 'D-5', 'D-4', 'D-3', 'D-2', 'D-1', '오늘'],
|
||||
datasets: [{
|
||||
data: <?= json_encode($weeklyRequest, JSON_UNESCAPED_UNICODE) ?>,
|
||||
backgroundColor: style.blue,
|
||||
borderRadius: 4,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { display: false } },
|
||||
y: { beginAtZero: true, grid: { color: style.grid } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('compactOutChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['10월', '11월', '12월', '1월', '2월', '3월', '4월', '5월'],
|
||||
datasets: [{
|
||||
label: '출고',
|
||||
data: <?= json_encode($monthlyOut, JSON_UNESCAPED_UNICODE) ?>,
|
||||
borderColor: style.teal,
|
||||
backgroundColor: style.blueLight,
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
pointRadius: 2.5,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: { boxWidth: 10, padding: 8 },
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: { grid: { display: false } },
|
||||
y: { beginAtZero: false, grid: { color: style.grid } },
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -75,6 +75,7 @@ $notices = [
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>종량제 시스템 — 종합 현황 (정보집약)</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<style data-purpose="global-font-scale">html{font-size:18px}.app-brand,.app-brand *{font-size:16px}</style>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
body {
|
||||
|
||||
@@ -20,6 +20,7 @@ $dashBlend = base_url('dashboard/blend');
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>종량제 시스템 — 업무 현황 (모던)</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<style data-purpose="global-font-scale">html{font-size:18px}.app-brand,.app-brand *{font-size:16px}</style>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
|
||||
201
app/Views/bag/lg_dashboard_simple.php
Normal file
201
app/Views/bag/lg_dashboard_simple.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
/**
|
||||
* 로그인 후 메인 — 단순형 요약 대시보드
|
||||
* 복잡한 표/그래프 대신, 핵심 지표 몇 개와 주요 화면으로 가는 버튼만 노출.
|
||||
*
|
||||
* @var string $lgLabel
|
||||
*/
|
||||
$lgLabel = $lgLabel ?? '북구';
|
||||
$mbName = session()->get('mb_name') ?? '담당자';
|
||||
$weeklyRequests = [7, 12, 9, 14, 8, 11, 10];
|
||||
$stockMix = [
|
||||
['name' => '일반용', 'value' => 52, 'color' => '#3b82f6'],
|
||||
['name' => '음식물', 'value' => 28, 'color' => '#10b981'],
|
||||
['name' => '특수', 'value' => 20, 'color' => '#f59e0b'],
|
||||
];
|
||||
$lowStock = [
|
||||
['name' => '일반 20L', 'percent' => 34],
|
||||
['name' => '특수규격 A', 'percent' => 22],
|
||||
['name' => '재사용봉투', 'percent' => 58],
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="h-full flex flex-col gap-4">
|
||||
<!-- 상단 요약 헤더 -->
|
||||
<section class="bg-white border border-gray-200 rounded-lg shadow-sm px-4 py-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-base font-semibold text-gray-900">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-blue-50 text-blue-700 text-sm font-bold">i</span>
|
||||
<span>업무 현황 요약</span>
|
||||
</span>
|
||||
</h1>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
<?= esc($lgLabel) ?> · <strong class="text-gray-700"><?= esc($mbName) ?></strong>님 기준으로 자주 보는 정보만 간단히 모아서 보여줍니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-2 text-xs text-gray-600 items-start sm:items-end">
|
||||
<div>
|
||||
<span class="inline-block text-[11px] text-gray-500 mb-1">기준일</span>
|
||||
<div class="px-2 py-1 rounded border border-gray-200 bg-gray-50 text-[11px]">
|
||||
<i class="fa-regular fa-calendar mr-1 text-gray-500"></i><?= date('Y.m.d (D)') ?>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="inline-flex items-center justify-center px-3 py-1.5 rounded bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium shadow">
|
||||
<i class="fa-solid fa-rotate mr-1"></i> 새로고침
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 핵심 숫자 3개만 -->
|
||||
<section class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm px-4 py-3 flex flex-col justify-between">
|
||||
<p class="text-xs text-gray-500 mb-1"><i class="fa-solid fa-boxes-stacked text-emerald-600 mr-1"></i>봉투 재고 상태</p>
|
||||
<p class="text-2xl font-bold text-gray-900">양호</p>
|
||||
<p class="mt-1 text-[11px] text-gray-500">대부분 품목이 안전재고 이상입니다.</p>
|
||||
</div>
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm px-4 py-3 flex flex-col justify-between">
|
||||
<p class="text-xs text-gray-500 mb-1"><i class="fa-solid fa-inbox text-sky-600 mr-1"></i>미처리 구매신청</p>
|
||||
<p class="text-2xl font-bold text-sky-700">12건</p>
|
||||
<p class="mt-1 text-[11px] text-gray-500">오늘 들어온 신청까지 포함한 개수입니다.</p>
|
||||
</div>
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm px-4 py-3 flex flex-col justify-between">
|
||||
<p class="text-xs text-gray-500 mb-1"><i class="fa-solid fa-user-check text-violet-600 mr-1"></i>승인 대기</p>
|
||||
<p class="text-2xl font-bold text-violet-700">4명</p>
|
||||
<p class="mt-1 text-[11px] text-gray-500">회원·판매소 가입 승인 요청입니다.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 작은 그래프 영역 -->
|
||||
<section class="grid grid-cols-1 lg:grid-cols-3 gap-3">
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2">최근 7일 신청 추이</h2>
|
||||
<div class="h-24 flex items-end gap-1.5">
|
||||
<?php $maxReq = max($weeklyRequests); ?>
|
||||
<?php foreach ($weeklyRequests as $idx => $v): ?>
|
||||
<?php $h = (int) round(($v / $maxReq) * 100); ?>
|
||||
<div class="flex-1 flex flex-col items-center justify-end gap-1">
|
||||
<span class="text-[10px] text-gray-500"><?= esc((string) $v) ?></span>
|
||||
<div class="w-full rounded-t bg-gradient-to-t from-blue-700 to-blue-400" style="height: <?= $h ?>%"></div>
|
||||
<span class="text-[10px] text-gray-400">D<?= 6 - $idx ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2">재고 구성 비율</h2>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-20 h-20 rounded-full" style="background: conic-gradient(#3b82f6 0% 52%, #10b981 52% 80%, #f59e0b 80% 100%);">
|
||||
<div class="w-12 h-12 bg-white rounded-full m-auto mt-4"></div>
|
||||
</div>
|
||||
<ul class="text-[11px] text-gray-600 space-y-1">
|
||||
<?php foreach ($stockMix as $item): ?>
|
||||
<li class="flex items-center gap-2">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full" style="background-color: <?= esc($item['color'], 'attr') ?>"></span>
|
||||
<span><?= esc($item['name']) ?> <?= esc((string) $item['value']) ?>%</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2">부족 재고 품목</h2>
|
||||
<div class="space-y-2">
|
||||
<?php foreach ($lowStock as $item): ?>
|
||||
<div>
|
||||
<div class="flex justify-between text-[11px] text-gray-600 mb-1">
|
||||
<span><?= esc($item['name']) ?></span>
|
||||
<span><?= esc((string) $item['percent']) ?>%</span>
|
||||
</div>
|
||||
<div class="h-2 rounded bg-gray-100 overflow-hidden">
|
||||
<div class="h-full rounded bg-amber-500" style="width: <?= (int) $item['percent'] ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 자주 가는 화면 바로가기 -->
|
||||
<section class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 flex items-center gap-2">
|
||||
<i class="fa-solid fa-location-arrow text-blue-600"></i>
|
||||
자주 가는 화면
|
||||
</h2>
|
||||
<p class="text-[11px] text-gray-500 mb-3">
|
||||
왼쪽 메뉴를 모두 펼치지 않고도, 자주 사용하는 업무 화면으로 바로 이동할 수 있습니다.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<a href="<?= base_url('bag/inventory') ?>" class="group flex items-center gap-3 px-3 py-2 rounded border border-gray-200 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
||||
<div class="h-8 w-8 rounded-full bg-emerald-50 text-emerald-700 flex items-center justify-center">
|
||||
<i class="fa-solid fa-boxes-stacked"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold text-gray-900 group-hover:text-blue-800">창고 재고 조회</p>
|
||||
<p class="text-[11px] text-gray-500 truncate">품목별 현재 재고를 간단히 확인</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<?= base_url('bag/order/create') ?>" class="group flex items-center gap-3 px-3 py-2 rounded border border-gray-200 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
||||
<div class="h-8 w-8 rounded-full bg-sky-50 text-sky-700 flex items-center justify-center">
|
||||
<i class="fa-solid fa-cart-shopping"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold text-gray-900 group-hover:text-blue-800">발주(구매신청) 등록</p>
|
||||
<p class="text-[11px] text-gray-500 truncate">지정판매소 발주·구매신청 입력</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<?= base_url('bag/flow') ?>" class="group flex items-center gap-3 px-3 py-2 rounded border border-gray-200 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
||||
<div class="h-8 w-8 rounded-full bg-orange-50 text-orange-600 flex items-center justify-center">
|
||||
<i class="fa-solid fa-arrow-right-arrow-left"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold text-gray-900 group-hover:text-blue-800">수불 흐름 보기</p>
|
||||
<p class="text-[11px] text-gray-500 truncate">입고·출고 내역을 한눈에</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<?= base_url('bag/sales') ?>" class="group flex items-center gap-3 px-3 py-2 rounded border border-gray-200 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
||||
<div class="h-8 w-8 rounded-full bg-indigo-50 text-indigo-600 flex items-center justify-center">
|
||||
<i class="fa-solid fa-receipt"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold text-gray-900 group-hover:text-blue-800">판매 내역 조회</p>
|
||||
<p class="text-[11px] text-gray-500 truncate">기간별 봉투 판매 현황</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<?= base_url('bag/help') ?>" class="group flex items-center gap-3 px-3 py-2 rounded border border-gray-200 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
||||
<div class="h-8 w-8 rounded-full bg-gray-50 text-gray-600 flex items-center justify-center">
|
||||
<i class="fa-solid fa-circle-question"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold text-gray-900 group-hover:text-blue-800">도움말 / 매뉴얼</p>
|
||||
<p class="text-[11px] text-gray-500 truncate">업무별 사용 방법 안내</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="<?= base_url('dashboard') ?>" class="group flex items-center gap-3 px-3 py-2 rounded border border-dashed border-gray-300 hover:border-blue-500 hover:bg-blue-50/40 transition">
|
||||
<div class="h-8 w-8 rounded-full bg-white text-gray-500 flex items-center justify-center">
|
||||
<i class="fa-solid fa-table-columns"></i>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold text-gray-900 group-hover:text-blue-800">자세히 보기 (/dashboard)</p>
|
||||
<p class="text-[11px] text-gray-500 truncate">그래프·표가 많은 기존 화면으로 이동</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 알림/메모 영역 (간단 텍스트만) -->
|
||||
<section class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
|
||||
<h2 class="text-sm font-semibold text-gray-900 mb-2 flex items-center gap-2">
|
||||
<i class="fa-solid fa-clipboard-list text-gray-600"></i>
|
||||
오늘 확인하면 좋은 것들
|
||||
</h2>
|
||||
<ul class="list-disc list-inside text-[11px] text-gray-600 space-y-1.5">
|
||||
<li>재고 부족 품목이 있는지 간단히 확인하고, 필요 시 발주를 등록합니다.</li>
|
||||
<li>미처리 구매신청과 승인 대기 건을 하루 한 번 이상 처리합니다.</li>
|
||||
<li>더 자세한 분석·그래프는 <span class="text-blue-700 font-semibold">/dashboard</span> 화면에서 확인할 수 있습니다.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
130
app/Views/bag/order_lot_seed.php
Normal file
130
app/Views/bag/order_lot_seed.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
$orders = is_array($orders ?? null) ? $orders : [];
|
||||
$companyMap = is_array($companyMap ?? null) ? $companyMap : [];
|
||||
$itemSummary = is_array($itemSummary ?? null) ? $itemSummary : [];
|
||||
$companies = is_array($companies ?? null) ? $companies : [];
|
||||
$startMonthValue = (string) ($startMonth ?? date('Y-m'));
|
||||
$endMonthValue = (string) ($endMonth ?? date('Y-m'));
|
||||
$baseYear = (int) date('Y');
|
||||
if (preg_match('/^(\d{4})-\d{2}$/', $startMonthValue, $sm)) {
|
||||
$baseYear = (int) $sm[1];
|
||||
} elseif (preg_match('/^(\d{4})-\d{2}$/', $endMonthValue, $em)) {
|
||||
$baseYear = (int) $em[1];
|
||||
}
|
||||
$monthOptionValues = [];
|
||||
for ($year = $baseYear - 2; $year <= $baseYear + 2; $year++) {
|
||||
for ($m = 1; $m <= 12; $m++) {
|
||||
$monthValue = sprintf('%04d-%02d', $year, $m);
|
||||
$monthOptionValues[] = ['value' => $monthValue, 'label' => $year . '년 ' . $m . '월'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">LOT-No 디스켓 불출</span>
|
||||
</section>
|
||||
|
||||
<section class="border border-gray-300 p-2 mt-2 bg-white">
|
||||
<form method="get" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-6 gap-2 text-sm">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">시작월</label>
|
||||
<select id="start_month" name="start_month" class="border border-gray-300 rounded px-2 py-1 w-full min-w-0">
|
||||
<?php foreach ($monthOptionValues as $opt): ?>
|
||||
<option value="<?= esc($opt['value']) ?>" <?= $opt['value'] === $startMonthValue ? 'selected' : '' ?>><?= esc($opt['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">종료월</label>
|
||||
<select id="end_month" name="end_month" class="border border-gray-300 rounded px-2 py-1 w-full min-w-0">
|
||||
<?php foreach ($monthOptionValues as $opt): ?>
|
||||
<option value="<?= esc($opt['value']) ?>" <?= $opt['value'] === $endMonthValue ? 'selected' : '' ?>><?= esc($opt['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-w-0 xl:col-span-2">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">LOT-No</label>
|
||||
<input type="text" name="lot_no" value="<?= esc((string) ($lotNo ?? '')) ?>" placeholder="예: ZLCH2M" class="border border-gray-300 rounded px-2 py-1 w-full min-w-0" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<label class="font-bold text-gray-700 whitespace-nowrap shrink-0">제작업체</label>
|
||||
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1 w-full min-w-0">
|
||||
<option value="0">전체</option>
|
||||
<?php foreach ($companies as $company): ?>
|
||||
<?php $cpIdx = (int) ($company->cp_idx ?? 0); ?>
|
||||
<option value="<?= $cpIdx ?>" <?= (int) ($companyIdx ?? 0) === $cpIdx ? 'selected' : '' ?>>
|
||||
<?= esc((string) ($company->cp_name ?? '')) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">조회</button>
|
||||
<a href="<?= base_url('bag/order/lot-seed') ?>" class="text-sm text-gray-500 hover:text-gray-700">초기화</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="border border-gray-300 mt-2 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">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-20">작업</th>
|
||||
<th class="w-24">발주일</th>
|
||||
<th class="w-28">LOT-No</th>
|
||||
<th>제작업체</th>
|
||||
<th class="w-24">품목수</th>
|
||||
<th class="w-24">박스합계</th>
|
||||
<th class="w-28">낱장합계</th>
|
||||
<th class="w-20">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($orders !== []): ?>
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<?php
|
||||
$boIdx = (int) ($order->bo_idx ?? 0);
|
||||
$sum = $itemSummary[$boIdx] ?? ['line_count' => 0, 'qty_box' => 0, 'qty_sheet' => 0];
|
||||
$companyName = (string) ($companyMap[(int) ($order->bo_company_idx ?? 0)] ?? '-');
|
||||
$status = (string) ($order->bo_status ?? 'normal');
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<form method="post" action="<?= base_url('bag/order/lot-seed/generate') ?>" class="inline">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="bo_idx" value="<?= $boIdx ?>" />
|
||||
<button type="submit" class="text-blue-600 hover:underline text-xs">seed 생성</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-center"><?= esc((string) ($order->bo_order_date ?? '')) ?></td>
|
||||
<td class="text-center font-mono"><?= esc((string) ($order->bo_lot_no ?? '')) ?></td>
|
||||
<td class="pl-2"><?= esc($companyName) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($sum['line_count'] ?? 0)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($sum['qty_box'] ?? 0)) ?></td>
|
||||
<td class="text-right pr-2"><?= number_format((int) ($sum['qty_sheet'] ?? 0)) ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($status === 'cancelled'): ?>
|
||||
<span class="text-orange-600">취소</span>
|
||||
<?php elseif ($status === 'deleted'): ?>
|
||||
<span class="text-red-600">삭제</span>
|
||||
<?php else: ?>
|
||||
정상
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-gray-400 py-4">조회된 발주가 없습니다.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (isset($pager)): ?>
|
||||
<div class="mt-2 mb-2 no-print"><?= $pager->links() ?></div>
|
||||
<?php endif; ?>
|
||||
454
app/Views/bag/order_phone.php
Normal file
454
app/Views/bag/order_phone.php
Normal file
@@ -0,0 +1,454 @@
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<span class="text-sm font-bold text-gray-700">전화 주문 접수</span>
|
||||
</section>
|
||||
|
||||
<div class="border border-gray-300 p-4 mt-2 bg-white">
|
||||
<?php if (session()->getFlashdata('success')): ?>
|
||||
<div class="mb-3 border border-emerald-300 bg-emerald-50 text-emerald-800 px-3 py-2 rounded-sm text-sm">
|
||||
<?= esc(session()->getFlashdata('success')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (session()->getFlashdata('error')): ?>
|
||||
<div class="mb-3 border border-red-300 bg-red-50 text-red-700 px-3 py-2 rounded-sm text-sm">
|
||||
<?= esc(session()->getFlashdata('error')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php $flashErrors = session()->getFlashdata('errors'); ?>
|
||||
<?php if (is_array($flashErrors) && $flashErrors !== []): ?>
|
||||
<div class="mb-3 border border-red-300 bg-red-50 text-red-700 px-3 py-2 rounded-sm text-sm">
|
||||
<ul class="list-disc list-inside">
|
||||
<?php foreach ($flashErrors as $err): ?>
|
||||
<li><?= esc((string) $err) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form action="<?= base_url('bag/shop-order/store') ?>" method="POST" class="space-y-4" id="phone-order-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="return_to" value="bag/order/phone"/>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
||||
<div class="xl:col-span-2 space-y-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">판매소 검색</label>
|
||||
<div class="relative flex-1 min-w-[20rem]">
|
||||
<input id="shop-search" class="border border-gray-300 rounded px-3 py-1.5 text-sm w-[34rem] max-w-full" type="text" autocomplete="off" placeholder="코드/사업자번호/대표자명/상호/전화/주소 중 하나 입력"/>
|
||||
<div id="shop-search-suggest" class="hidden absolute left-0 top-full mt-1 w-[48rem] max-w-[90vw] max-h-72 overflow-auto border border-gray-300 bg-white shadow-lg z-30"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-28">판매소 선택 <span class="text-red-500">*</span></label>
|
||||
<select id="shop-select" class="border border-gray-300 rounded px-3 py-1.5 text-sm w-[34rem] max-w-full" name="so_ds_idx" required>
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($shops as $shop): ?>
|
||||
<option
|
||||
value="<?= esc($shop->ds_idx) ?>"
|
||||
data-shop-no="<?= esc((string) ($shop->ds_shop_no ?? '')) ?>"
|
||||
data-biz-no="<?= esc((string) ($shop->ds_biz_no ?? '')) ?>"
|
||||
data-name="<?= esc((string) ($shop->ds_name ?? '')) ?>"
|
||||
data-rep-name="<?= esc((string) ($shop->ds_rep_name ?? '')) ?>"
|
||||
data-tel="<?= esc((string) ($shop->ds_tel ?? '')) ?>"
|
||||
data-rep-phone="<?= esc((string) ($shop->ds_rep_phone ?? '')) ?>"
|
||||
data-address="<?= esc(trim((string) ($shop->ds_addr ?? '') . ' ' . (string) ($shop->ds_addr_detail ?? ''))) ?>"
|
||||
data-va-bank="<?= esc((string) ($shop->ds_va_bank ?? '')) ?>"
|
||||
data-va-account="<?= esc((string) ($shop->ds_va_account ?? '')) ?>"
|
||||
>
|
||||
<?= esc(($shop->ds_shop_no ? '[' . $shop->ds_shop_no . '] ' : '') . $shop->ds_name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-300 p-2 bg-gray-50">
|
||||
<div class="text-sm font-bold text-gray-700 mb-2">접수 정보</div>
|
||||
<table class="w-full text-sm">
|
||||
<tr><th class="text-left w-28 py-1">접수번호</th><td class="py-1 text-gray-800 font-semibold"><?= esc((string) ($receiptNo ?? 1)) ?></td></tr>
|
||||
<tr><th class="text-left py-1">접수일</th><td class="py-1 text-gray-700"><?= esc(date('Y-m-d')) ?></td></tr>
|
||||
<tr><th class="text-left py-1">배달일</th><td class="py-1 text-gray-700"><?= esc(date('Y-m-d', strtotime('+1 day'))) ?> (자동)</td></tr>
|
||||
<tr><th class="text-left py-1">담당자</th><td class="py-1 text-gray-700"><?= esc((string) (session()->get('mb_name') ?? '담당자')) ?></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<div class="border border-gray-300 p-2 bg-gray-50">
|
||||
<div class="text-sm font-bold text-gray-700 mb-2">지정판매소 정보</div>
|
||||
<table class="w-full text-sm">
|
||||
<tr><th class="text-left w-28 py-1">코드</th><td id="shop-info-code" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">사업자번호</th><td id="shop-info-biz" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">대표자명</th><td id="shop-info-rep" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">상호명</th><td id="shop-info-name" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">전화번호</th><td id="shop-info-tel" class="py-1 text-gray-700">-</td></tr>
|
||||
<tr><th class="text-left py-1">주소</th><td id="shop-info-addr" class="py-1 text-gray-700">-</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-300 p-2 bg-gray-50">
|
||||
<div class="text-sm font-bold text-gray-700 mb-2">결제/가상계좌</div>
|
||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||||
<label class="block text-sm font-bold text-gray-700 w-24">결제구분 <span class="text-red-500">*</span></label>
|
||||
<select id="payment-type" class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="so_payment_type" required>
|
||||
<option value="">선택</option>
|
||||
<option value="이체">이체</option>
|
||||
<option value="가상계좌">가상계좌</option>
|
||||
</select>
|
||||
<span id="payment-guide" class="text-xs text-gray-500"></span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="font-semibold text-gray-700">가상계좌:</span>
|
||||
<span id="shop-info-va" class="text-gray-700">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="so_delivery_date" value="<?= esc(date('Y-m-d', strtotime('+1 day'))) ?>"/>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-sm font-bold text-gray-700">전화 주문접수표</label>
|
||||
<button type="button" id="add-order-row" class="border border-gray-300 bg-white px-3 py-1 rounded-sm text-xs text-gray-700 hover:bg-gray-50">행 추가</button>
|
||||
</div>
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">구분</th>
|
||||
<th class="w-56">품목</th>
|
||||
<th class="w-40">1박스(낱장/판매가)</th>
|
||||
<th class="w-40">1팩(낱장/판매가)</th>
|
||||
<th class="w-24">단가</th>
|
||||
<th class="w-28">주문수량</th>
|
||||
<th class="w-28">금액</th>
|
||||
<th class="w-44">포장(박스/팩/낱장)</th>
|
||||
<th class="w-20">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="order-rows">
|
||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||
<tr class="order-row">
|
||||
<td class="text-center item-kind-cell">-</td>
|
||||
<td>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full bag-code-select" name="item_bag_code[]">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $cd): ?>
|
||||
<?php
|
||||
$code = (string) $cd->cd_code;
|
||||
$name = (string) ($cd->cd_name ?? '');
|
||||
$price = $priceMap[$code] ?? null;
|
||||
$unit = $unitMap[$code] ?? null;
|
||||
$unitPrice = (int) ($price->bp_consumer ?? 0);
|
||||
$boxSheets = (int) ($unit->pu_total_per_box ?? 0);
|
||||
$packSheets = (int) ($unit->pu_pack_per_sheet ?? 0);
|
||||
$kindLabel = (mb_strpos($name, '스티커') !== false) ? '스티커' : '봉투';
|
||||
?>
|
||||
<option value="<?= esc($code) ?>" data-name="<?= esc($name, 'attr') ?>" data-kind-label="<?= esc($kindLabel, 'attr') ?>" data-unit-price="<?= esc((string) $unitPrice, 'attr') ?>" data-box-sheets="<?= esc((string) $boxSheets, 'attr') ?>" data-pack-sheets="<?= esc((string) $packSheets, 'attr') ?>">
|
||||
<?= esc($code) ?> — <?= esc($name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-right px-2 box-info-cell">0 / 0</td>
|
||||
<td class="text-right px-2 pack-info-cell">0 / 0</td>
|
||||
<td class="text-right px-2 unit-price-cell">0</td>
|
||||
<td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-qty-input" name="item_qty[]" type="number" min="0" value="0"/></td>
|
||||
<td class="text-right px-2 item-amount-cell">0</td>
|
||||
<td class="text-right px-2 pack-result-cell">박스=0, 팩=0, 낱장=0</td>
|
||||
<td class="text-center px-2">
|
||||
<button type="button" class="remove-order-row border border-red-300 text-red-600 px-2 py-0.5 rounded text-xs hover:bg-red-50">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-semibold bg-gray-50">
|
||||
<td colspan="5" class="text-right px-2 py-1">합계</td>
|
||||
<td class="text-right px-2 py-1" id="sum-qty">0</td>
|
||||
<td class="text-right px-2 py-1" id="sum-amount">0</td>
|
||||
<td class="text-right px-2 py-1" id="sum-pack">박스=0, 팩=0, 낱장=0</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">저장</button>
|
||||
<a href="<?= base_url('bag/sales') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<template id="order-row-template">
|
||||
<tr class="order-row">
|
||||
<td class="text-center item-kind-cell">-</td>
|
||||
<td>
|
||||
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full bag-code-select" name="item_bag_code[]">
|
||||
<option value="">선택</option>
|
||||
<?php foreach ($bagCodes as $cd): ?>
|
||||
<?php
|
||||
$code = (string) $cd->cd_code;
|
||||
$name = (string) ($cd->cd_name ?? '');
|
||||
$price = $priceMap[$code] ?? null;
|
||||
$unit = $unitMap[$code] ?? null;
|
||||
$unitPrice = (int) ($price->bp_consumer ?? 0);
|
||||
$boxSheets = (int) ($unit->pu_total_per_box ?? 0);
|
||||
$packSheets = (int) ($unit->pu_pack_per_sheet ?? 0);
|
||||
$kindLabel = (mb_strpos($name, '스티커') !== false) ? '스티커' : '봉투';
|
||||
?>
|
||||
<option value="<?= esc($code) ?>" data-name="<?= esc($name, 'attr') ?>" data-kind-label="<?= esc($kindLabel, 'attr') ?>" data-unit-price="<?= esc((string) $unitPrice, 'attr') ?>" data-box-sheets="<?= esc((string) $boxSheets, 'attr') ?>" data-pack-sheets="<?= esc((string) $packSheets, 'attr') ?>">
|
||||
<?= esc($code) ?> — <?= esc($name) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-right px-2 box-info-cell">0 / 0</td>
|
||||
<td class="text-right px-2 pack-info-cell">0 / 0</td>
|
||||
<td class="text-right px-2 unit-price-cell">0</td>
|
||||
<td><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-qty-input" name="item_qty[]" type="number" min="0" value="0"/></td>
|
||||
<td class="text-right px-2 item-amount-cell">0</td>
|
||||
<td class="text-right px-2 pack-result-cell">박스=0, 팩=0, 낱장=0</td>
|
||||
<td class="text-center px-2">
|
||||
<button type="button" class="remove-order-row border border-red-300 text-red-600 px-2 py-0.5 rounded text-xs hover:bg-red-50">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const shopSearch = document.getElementById('shop-search');
|
||||
const shopSelect = document.getElementById('shop-select');
|
||||
const shopSuggest = document.getElementById('shop-search-suggest');
|
||||
const paymentType = document.getElementById('payment-type');
|
||||
const paymentGuide = document.getElementById('payment-guide');
|
||||
const addRowButton = document.getElementById('add-order-row');
|
||||
const orderRows = document.getElementById('order-rows');
|
||||
const rowTemplate = document.getElementById('order-row-template');
|
||||
const form = document.getElementById('phone-order-form');
|
||||
|
||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||
|
||||
function updateShopInfo() {
|
||||
const opt = shopSelect.options[shopSelect.selectedIndex];
|
||||
const bank = opt?.dataset?.vaBank || '';
|
||||
const account = opt?.dataset?.vaAccount || '';
|
||||
const va = bank || account ? [bank, account].filter(Boolean).join(' ') : '-';
|
||||
|
||||
document.getElementById('shop-info-code').textContent = opt?.dataset?.shopNo || '-';
|
||||
document.getElementById('shop-info-biz').textContent = opt?.dataset?.bizNo || '-';
|
||||
document.getElementById('shop-info-rep').textContent = opt?.dataset?.repName || '-';
|
||||
document.getElementById('shop-info-name').textContent = opt?.dataset?.name || '-';
|
||||
document.getElementById('shop-info-tel').textContent = opt?.dataset?.tel || opt?.dataset?.repPhone || '-';
|
||||
document.getElementById('shop-info-addr').textContent = opt?.dataset?.address || '-';
|
||||
document.getElementById('shop-info-va').textContent = va;
|
||||
paymentGuide.textContent = paymentType.value === '가상계좌' ? ('안내 계좌: ' + va) : '';
|
||||
}
|
||||
|
||||
function shopMergedText(opt) {
|
||||
return [
|
||||
opt.dataset.shopNo || '',
|
||||
opt.dataset.bizNo || '',
|
||||
opt.dataset.repName || '',
|
||||
opt.dataset.name || '',
|
||||
opt.dataset.tel || '',
|
||||
opt.dataset.address || '',
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function matchShopByKeyword(keyword) {
|
||||
const q = (keyword || '').trim().toLowerCase();
|
||||
if (!q) return;
|
||||
for (let i = 0; i < shopSelect.options.length; i++) {
|
||||
const opt = shopSelect.options[i];
|
||||
if (!opt.value) continue;
|
||||
const merged = (shopMergedText(opt) + ' ' + (opt.text || '')).toLowerCase();
|
||||
if (merged.includes(q)) {
|
||||
shopSelect.selectedIndex = i;
|
||||
updateShopInfo();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideSuggest() {
|
||||
if (!shopSuggest) return;
|
||||
shopSuggest.classList.add('hidden');
|
||||
shopSuggest.innerHTML = '';
|
||||
}
|
||||
|
||||
function renderSuggest(query) {
|
||||
if (!shopSuggest || !shopSelect) return;
|
||||
const q = (query || '').trim().toLowerCase();
|
||||
const matched = [];
|
||||
for (let i = 0; i < shopSelect.options.length; i++) {
|
||||
const opt = shopSelect.options[i];
|
||||
if (!opt.value) continue;
|
||||
const merged = shopMergedText(opt);
|
||||
if (!q || merged.toLowerCase().includes(q)) {
|
||||
matched.push({ index: i, label: merged });
|
||||
}
|
||||
if (matched.length >= 50) break;
|
||||
}
|
||||
if (matched.length === 0) {
|
||||
hideSuggest();
|
||||
return;
|
||||
}
|
||||
shopSuggest.innerHTML = matched.map((m) => `
|
||||
<button type="button" class="shop-suggest-item w-full text-left px-2 py-1.5 hover:bg-blue-50 text-xs border-b border-gray-100 whitespace-normal break-all" data-index="${m.index}">${m.label}</button>
|
||||
`).join('');
|
||||
shopSuggest.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function calcRow(row) {
|
||||
const select = row.querySelector('.bag-code-select');
|
||||
const qtyInput = row.querySelector('.item-qty-input');
|
||||
const selected = select.options[select.selectedIndex];
|
||||
|
||||
const qty = parseInt(qtyInput.value || '0', 10) || 0;
|
||||
const unitPrice = parseInt(selected?.dataset?.unitPrice || '0', 10) || 0;
|
||||
const boxSheets = parseInt(selected?.dataset?.boxSheets || '0', 10) || 0;
|
||||
const packSheets = parseInt(selected?.dataset?.packSheets || '0', 10) || 0;
|
||||
const kindLabel = selected?.dataset?.kindLabel || '-';
|
||||
|
||||
let box = 0;
|
||||
let pack = 0;
|
||||
let sheet = qty;
|
||||
if (boxSheets > 0) {
|
||||
box = Math.floor(qty / boxSheets);
|
||||
const remain = qty % boxSheets;
|
||||
if (packSheets > 0) {
|
||||
pack = Math.floor(remain / packSheets);
|
||||
sheet = remain % packSheets;
|
||||
} else {
|
||||
sheet = remain;
|
||||
}
|
||||
} else if (packSheets > 0) {
|
||||
pack = Math.floor(qty / packSheets);
|
||||
sheet = qty % packSheets;
|
||||
}
|
||||
|
||||
const amount = unitPrice * qty;
|
||||
const boxPrice = boxSheets * unitPrice;
|
||||
const packPrice = packSheets * unitPrice;
|
||||
|
||||
row.querySelector('.item-kind-cell').textContent = kindLabel;
|
||||
row.querySelector('.box-info-cell').textContent = nf(boxSheets) + ' / ' + nf(boxPrice);
|
||||
row.querySelector('.pack-info-cell').textContent = nf(packSheets) + ' / ' + nf(packPrice);
|
||||
row.querySelector('.unit-price-cell').textContent = nf(unitPrice);
|
||||
row.querySelector('.item-amount-cell').textContent = nf(amount);
|
||||
row.querySelector('.pack-result-cell').textContent = '박스=' + nf(box) + ', 팩=' + nf(pack) + ', 낱장=' + nf(sheet);
|
||||
|
||||
return { qty, amount, box, pack, sheet };
|
||||
}
|
||||
|
||||
function recalcAllRows() {
|
||||
let sumQty = 0, sumAmount = 0, sumBox = 0, sumPack = 0, sumSheet = 0;
|
||||
document.querySelectorAll('.order-row').forEach((row) => {
|
||||
const r = calcRow(row);
|
||||
sumQty += r.qty;
|
||||
sumAmount += r.amount;
|
||||
sumBox += r.box;
|
||||
sumPack += r.pack;
|
||||
sumSheet += r.sheet;
|
||||
});
|
||||
document.getElementById('sum-qty').textContent = nf(sumQty);
|
||||
document.getElementById('sum-amount').textContent = nf(sumAmount);
|
||||
document.getElementById('sum-pack').textContent = '박스=' + nf(sumBox) + ', 팩=' + nf(sumPack) + ', 낱장=' + nf(sumSheet);
|
||||
}
|
||||
|
||||
function clearSearchInputOnly() {
|
||||
if (shopSearch) shopSearch.value = '';
|
||||
}
|
||||
|
||||
// 판매소 검색 input을 다시 누르면(또는 포커스를 다시 받으면) 검색 input 텍스트만 비운다.
|
||||
// 기존에 선택된 판매소 정보(셀렉트, 지정판매소 정보, 가상계좌 등)는 그대로 유지한다.
|
||||
shopSearch?.addEventListener('focus', () => {
|
||||
if (shopSelect && shopSelect.value) clearSearchInputOnly();
|
||||
renderSuggest('');
|
||||
});
|
||||
shopSearch?.addEventListener('mousedown', () => {
|
||||
if (shopSelect && shopSelect.value) clearSearchInputOnly();
|
||||
});
|
||||
shopSearch?.addEventListener('click', () => renderSuggest(shopSearch.value || ''));
|
||||
shopSearch?.addEventListener('input', (e) => renderSuggest(e.target.value || ''));
|
||||
|
||||
shopSuggest?.addEventListener('mousedown', (e) => {
|
||||
const btn = e.target.closest('.shop-suggest-item');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
const idx = Number(btn.dataset.index || -1);
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= shopSelect.options.length) return;
|
||||
shopSelect.selectedIndex = idx;
|
||||
const opt = shopSelect.options[idx];
|
||||
shopSearch.value = shopMergedText(opt);
|
||||
updateShopInfo();
|
||||
hideSuggest();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!shopSuggest || !shopSearch) return;
|
||||
if (e.target === shopSearch || shopSuggest.contains(e.target)) return;
|
||||
hideSuggest();
|
||||
});
|
||||
|
||||
shopSearch?.addEventListener('change', (e) => matchShopByKeyword(e.target.value));
|
||||
shopSearch?.addEventListener('blur', () => {
|
||||
// 자동완성 클릭이 우선되도록 약간 지연.
|
||||
setTimeout(() => {
|
||||
matchShopByKeyword(shopSearch.value || '');
|
||||
}, 150);
|
||||
});
|
||||
shopSelect?.addEventListener('change', updateShopInfo);
|
||||
paymentType?.addEventListener('change', updateShopInfo);
|
||||
|
||||
orderRows?.addEventListener('change', function (e) {
|
||||
if (e.target.closest('.bag-code-select') || e.target.closest('.item-qty-input')) {
|
||||
recalcAllRows();
|
||||
}
|
||||
});
|
||||
orderRows?.addEventListener('input', function (e) {
|
||||
if (e.target.closest('.item-qty-input')) {
|
||||
recalcAllRows();
|
||||
}
|
||||
});
|
||||
orderRows?.addEventListener('click', function (e) {
|
||||
const removeButton = e.target.closest('.remove-order-row');
|
||||
if (!removeButton) return;
|
||||
const row = removeButton.closest('.order-row');
|
||||
if (!row) return;
|
||||
if (orderRows.querySelectorAll('.order-row').length <= 1) {
|
||||
alert('최소 1개 행은 유지해야 합니다.');
|
||||
return;
|
||||
}
|
||||
row.remove();
|
||||
recalcAllRows();
|
||||
});
|
||||
|
||||
addRowButton?.addEventListener('click', function () {
|
||||
if (!rowTemplate || !orderRows) return;
|
||||
const fragment = rowTemplate.content.cloneNode(true);
|
||||
orderRows.appendChild(fragment);
|
||||
recalcAllRows();
|
||||
});
|
||||
|
||||
form?.addEventListener('submit', function (e) {
|
||||
let hasItem = false;
|
||||
document.querySelectorAll('.order-row').forEach((row) => {
|
||||
const code = row.querySelector('.bag-code-select').value;
|
||||
const qty = parseInt(row.querySelector('.item-qty-input').value || '0', 10) || 0;
|
||||
if (code && qty > 0) hasItem = true;
|
||||
});
|
||||
if (!hasItem) {
|
||||
e.preventDefault();
|
||||
alert('주문 품목과 수량을 1개 이상 입력해 주세요.');
|
||||
}
|
||||
});
|
||||
|
||||
updateShopInfo();
|
||||
recalcAllRows();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?= view('bag/_dev_all_sales_panel') ?>
|
||||
261
app/Views/bag/order_phone_manage.php
Normal file
261
app/Views/bag/order_phone_manage.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="text-sm font-bold text-gray-700">전화 주문 접수 관리</span>
|
||||
<a href="<?= base_url('bag/order/phone') ?>" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm shadow hover:opacity-90">전화 주문 접수</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-5 gap-3 mt-2">
|
||||
<section class="xl:col-span-2 border border-gray-300 bg-white">
|
||||
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700">접수 리스트(전화)</div>
|
||||
<div class="max-h-[72vh] overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">번호</th>
|
||||
<th>판매소</th>
|
||||
<th class="w-28">접수일</th>
|
||||
<th class="w-24">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="order-list-body">
|
||||
<?php foreach (($orders ?? []) as $row): ?>
|
||||
<?php $isCancelled = (($row['so_status'] ?? 'normal') === 'cancelled'); ?>
|
||||
<tr class="order-list-row cursor-pointer hover:bg-blue-50 <?= $isCancelled ? 'bg-gray-50 text-gray-400' : '' ?>" data-order-id="<?= esc((string) ($row['so_idx'] ?? 0), 'attr') ?>">
|
||||
<td class="text-center"><?= esc((string) ($row['so_idx'] ?? 0)) ?></td>
|
||||
<td class="text-left pl-2"><?= esc((string) ($row['so_ds_name'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= esc((string) ($row['so_order_date'] ?? '')) ?></td>
|
||||
<td class="text-center"><?= $isCancelled ? '취소' : '정상' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($orders)): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-8 text-gray-400">전화 주문 데이터가 없습니다.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="xl:col-span-3 border border-gray-300 bg-white">
|
||||
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700">상세 정보</div>
|
||||
|
||||
<form id="order-detail-form" action="<?= base_url('bag/order/phone/manage/update') ?>" method="POST" class="p-3 space-y-3">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="so_idx" id="detail-so-idx" value=""/>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm">
|
||||
<div class="border border-gray-200 p-2 bg-gray-50">
|
||||
<div><span class="font-semibold text-gray-700">접수번호:</span> <span id="detail-so-no">-</span></div>
|
||||
<div><span class="font-semibold text-gray-700">판매소:</span> <span id="detail-shop-name">-</span></div>
|
||||
<div><span class="font-semibold text-gray-700">결제구분:</span> <span id="detail-payment">-</span></div>
|
||||
</div>
|
||||
<div class="border border-gray-200 p-2 bg-gray-50">
|
||||
<div><span class="font-semibold text-gray-700">접수일:</span> <span id="detail-order-date">-</span></div>
|
||||
<div><span class="font-semibold text-gray-700">배달일:</span> <span id="detail-delivery-date">-</span></div>
|
||||
<div><span class="font-semibold text-gray-700">상태:</span> <span id="detail-status">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-300 overflow-auto">
|
||||
<table class="w-full data-table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-14">번호</th>
|
||||
<th>품목</th>
|
||||
<th class="w-24">단가</th>
|
||||
<th class="w-24">접수량</th>
|
||||
<th class="w-28">접수금액</th>
|
||||
<th class="w-40">포장단위(박스/팩/낱장)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="detail-items-body">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-6 text-gray-400">왼쪽 리스트에서 주문을 선택해 주세요.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-semibold bg-gray-50">
|
||||
<td colspan="3" class="text-right px-2 py-1">합계</td>
|
||||
<td class="text-right px-2 py-1" id="detail-sum-qty">0</td>
|
||||
<td class="text-right px-2 py-1" id="detail-sum-amount">0</td>
|
||||
<td class="text-right px-2 py-1" id="detail-sum-pack">박스=0, 팩=0, 낱장=0</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" id="btn-save" class="bg-btn-search text-white px-5 py-1.5 rounded-sm text-sm shadow hover:opacity-90" disabled>주문 수정 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form id="order-cancel-form" method="POST" class="px-3 pb-3">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" id="btn-cancel-order" class="border border-red-300 text-red-600 px-5 py-1.5 rounded-sm text-sm hover:bg-red-50" disabled>주문 취소</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const orders = <?= json_encode($orders ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
const orderMap = new Map(orders.map((o) => [String(o.so_idx), o]));
|
||||
|
||||
const listBody = document.getElementById('order-list-body');
|
||||
const form = document.getElementById('order-detail-form');
|
||||
const cancelForm = document.getElementById('order-cancel-form');
|
||||
const detailBody = document.getElementById('detail-items-body');
|
||||
const inputSoIdx = document.getElementById('detail-so-idx');
|
||||
const btnSave = document.getElementById('btn-save');
|
||||
const btnCancel = document.getElementById('btn-cancel-order');
|
||||
|
||||
const nf = (n) => new Intl.NumberFormat('ko-KR').format(n || 0);
|
||||
|
||||
function setHeader(order) {
|
||||
document.getElementById('detail-so-no').textContent = order ? String(order.so_idx || '-') : '-';
|
||||
document.getElementById('detail-shop-name').textContent = order ? (order.so_ds_name || '-') : '-';
|
||||
document.getElementById('detail-payment').textContent = order ? (order.so_payment_type || '-') : '-';
|
||||
document.getElementById('detail-order-date').textContent = order ? (order.so_order_date || '-') : '-';
|
||||
document.getElementById('detail-delivery-date').textContent = order ? (order.so_delivery_date || '-') : '-';
|
||||
document.getElementById('detail-status').textContent = order ? ((order.so_status === 'cancelled') ? '취소' : '정상') : '-';
|
||||
}
|
||||
|
||||
function calcRow(tr) {
|
||||
const qtyInput = tr.querySelector('.item-qty-input');
|
||||
const qty = Math.max(0, parseInt(qtyInput.value || '0', 10) || 0);
|
||||
qtyInput.value = String(qty);
|
||||
|
||||
const unitPrice = parseInt(tr.dataset.unitPrice || '0', 10) || 0;
|
||||
const boxSheets = parseInt(tr.dataset.boxSheets || '0', 10) || 0;
|
||||
const packSheets = parseInt(tr.dataset.packSheets || '0', 10) || 0;
|
||||
|
||||
let box = 0;
|
||||
let pack = 0;
|
||||
let sheet = qty;
|
||||
if (boxSheets > 0) {
|
||||
box = Math.floor(qty / boxSheets);
|
||||
const remain = qty % boxSheets;
|
||||
if (packSheets > 0) {
|
||||
pack = Math.floor(remain / packSheets);
|
||||
sheet = remain % packSheets;
|
||||
} else {
|
||||
sheet = remain;
|
||||
}
|
||||
} else if (packSheets > 0) {
|
||||
pack = Math.floor(qty / packSheets);
|
||||
sheet = qty % packSheets;
|
||||
}
|
||||
|
||||
const amount = qty * unitPrice;
|
||||
tr.querySelector('.item-amount-cell').textContent = nf(amount);
|
||||
tr.querySelector('.item-pack-cell').textContent = `박스=${nf(box)}, 팩=${nf(pack)}, 낱장=${nf(sheet)}`;
|
||||
return { qty, amount, box, pack, sheet };
|
||||
}
|
||||
|
||||
function recalcTotals() {
|
||||
let sumQty = 0;
|
||||
let sumAmount = 0;
|
||||
let sumBox = 0;
|
||||
let sumPack = 0;
|
||||
let sumSheet = 0;
|
||||
|
||||
detailBody.querySelectorAll('tr.order-item-row').forEach((tr) => {
|
||||
const r = calcRow(tr);
|
||||
sumQty += r.qty;
|
||||
sumAmount += r.amount;
|
||||
sumBox += r.box;
|
||||
sumPack += r.pack;
|
||||
sumSheet += r.sheet;
|
||||
});
|
||||
|
||||
document.getElementById('detail-sum-qty').textContent = nf(sumQty);
|
||||
document.getElementById('detail-sum-amount').textContent = nf(sumAmount);
|
||||
document.getElementById('detail-sum-pack').textContent = `박스=${nf(sumBox)}, 팩=${nf(sumPack)}, 낱장=${nf(sumSheet)}`;
|
||||
}
|
||||
|
||||
function renderDetail(orderId) {
|
||||
const order = orderMap.get(String(orderId));
|
||||
if (!order) return;
|
||||
|
||||
inputSoIdx.value = String(order.so_idx || '');
|
||||
setHeader(order);
|
||||
cancelForm.action = `<?= base_url('bag/order/phone/manage/cancel') ?>/${order.so_idx}`;
|
||||
|
||||
const isCancelled = order.so_status === 'cancelled';
|
||||
btnSave.disabled = isCancelled;
|
||||
btnCancel.disabled = isCancelled;
|
||||
|
||||
const items = Array.isArray(order.items) ? order.items : [];
|
||||
if (items.length === 0) {
|
||||
detailBody.innerHTML = '<tr><td colspan="6" class="text-center py-6 text-gray-400">품목 정보가 없습니다.</td></tr>';
|
||||
recalcTotals();
|
||||
return;
|
||||
}
|
||||
|
||||
detailBody.innerHTML = items.map((item, idx) => {
|
||||
const itemId = String(item.soi_idx || '');
|
||||
const bagName = `${item.soi_bag_code || ''} ${item.soi_bag_name || ''}`.trim();
|
||||
const qty = parseInt(item.soi_qty || 0, 10) || 0;
|
||||
const unitPrice = parseInt(item.soi_unit_price || 0, 10) || 0;
|
||||
const amount = parseInt(item.soi_amount || 0, 10) || 0;
|
||||
const box = parseInt(item.soi_box_count || 0, 10) || 0;
|
||||
const pack = parseInt(item.soi_pack_count || 0, 10) || 0;
|
||||
const sheet = parseInt(item.soi_sheet_count || 0, 10) || 0;
|
||||
const boxSheets = parseInt(item.box_sheets || 0, 10) || 0;
|
||||
const packSheets = parseInt(item.pack_sheets || 0, 10) || 0;
|
||||
|
||||
return `
|
||||
<tr class="order-item-row" data-unit-price="${unitPrice}" data-box-sheets="${boxSheets}" data-pack-sheets="${packSheets}">
|
||||
<td class="text-center">${idx + 1}</td>
|
||||
<td class="text-left pl-2">${bagName}</td>
|
||||
<td class="text-right pr-2">${nf(unitPrice)}</td>
|
||||
<td class="text-right pr-2">
|
||||
<input type="number" min="0" class="item-qty-input border border-gray-300 rounded px-2 py-1 w-24 text-right" name="item_qty[${itemId}]" value="${qty}" ${isCancelled ? 'disabled' : ''}/>
|
||||
</td>
|
||||
<td class="text-right pr-2 item-amount-cell">${nf(amount)}</td>
|
||||
<td class="text-right pr-2 item-pack-cell">박스=${nf(box)}, 팩=${nf(pack)}, 낱장=${nf(sheet)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
recalcTotals();
|
||||
}
|
||||
|
||||
listBody?.addEventListener('click', (e) => {
|
||||
const tr = e.target.closest('.order-list-row');
|
||||
if (!tr) return;
|
||||
listBody.querySelectorAll('.order-list-row').forEach((row) => row.classList.remove('bg-blue-100'));
|
||||
tr.classList.add('bg-blue-100');
|
||||
renderDetail(tr.dataset.orderId);
|
||||
});
|
||||
|
||||
detailBody?.addEventListener('input', (e) => {
|
||||
if (e.target.closest('.item-qty-input')) {
|
||||
recalcTotals();
|
||||
}
|
||||
});
|
||||
|
||||
cancelForm?.addEventListener('submit', (e) => {
|
||||
if (!confirm('해당 주문을 취소 처리하시겠습니까? (삭제되지 않고 상태만 취소로 변경됩니다)')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
form?.addEventListener('submit', (e) => {
|
||||
if (!inputSoIdx.value) {
|
||||
e.preventDefault();
|
||||
alert('수정할 주문을 먼저 선택해 주세요.');
|
||||
}
|
||||
});
|
||||
|
||||
const firstRow = listBody?.querySelector('.order-list-row');
|
||||
if (firstRow) {
|
||||
firstRow.classList.add('bg-blue-100');
|
||||
renderDetail(firstRow.dataset.orderId);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?= view('bag/_dev_all_sales_panel') ?>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?= view('components/print_header', ['printTitle' => '봉투 입고 현황', 'printShowApproval' => false]) ?>
|
||||
<?= view('components/print_header', ['printTitle' => '봉투 입고 현황']) ?>
|
||||
<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>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>종량제 시스템 — 봉투 수불 현황</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<style data-purpose="global-font-scale">html{font-size:18px}.app-brand,.app-brand *{font-size:16px}</style>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<style data-purpose="base-typography">
|
||||
body {
|
||||
|
||||
Reference in New Issue
Block a user