feat: 발주입고(스캐너)·일괄입고 좌우 워크스페이스 재설계 + 실시간 스캔 입고
- 발주입고/일괄입고를 공용 워크스페이스로 통합: 좌측 발주현황(탭: 제작업체·발주기간 / 인수자·인계자, 정렬) · 우측 선택발주 상세 + 스캔/수량 입고 · 하단 입고 세부내역(박스코드) - 실시간 스캔 입고 엔드포인트 receivingScanBox 추가(스캔 1회=1박스, 수량 직접입력 병행) - buildReceivingCandidateRows에 카테고리(봉투/음식물·폐기물)·발주기간·대행소 필터 추가 - 일괄입고를 '일괄입고(음식물,폐기물)'로 개편, 음식물·폐기물만 표시(메뉴명 변경 SQL 포함) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,7 @@ $routes->get('bag/receiving/create', 'Bag::receivingCreate');
|
|||||||
$routes->post('bag/receiving/store', 'Bag::receivingStore');
|
$routes->post('bag/receiving/store', 'Bag::receivingStore');
|
||||||
$routes->get('bag/receiving/scanner', 'Bag::receivingScanner');
|
$routes->get('bag/receiving/scanner', 'Bag::receivingScanner');
|
||||||
$routes->post('bag/receiving/scanner/store', 'Bag::receivingScannerStore');
|
$routes->post('bag/receiving/scanner/store', 'Bag::receivingScannerStore');
|
||||||
|
$routes->post('bag/receiving/scan-box', 'Bag::receivingScanBox');
|
||||||
$routes->get('bag/receiving/batch', 'Bag::receivingBatch');
|
$routes->get('bag/receiving/batch', 'Bag::receivingBatch');
|
||||||
$routes->post('bag/receiving/batch/store', 'Bag::receivingBatchStore');
|
$routes->post('bag/receiving/batch/store', 'Bag::receivingBatchStore');
|
||||||
$routes->get('bag/receiving/status', 'Bag::receivingStatus');
|
$routes->get('bag/receiving/status', 'Bag::receivingStatus');
|
||||||
|
|||||||
@@ -4704,12 +4704,20 @@ SQL);
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 발주 입고(스캐너 대체 수동입력)
|
* 발주 입고(스캐너) — 좌우 분할 워크스페이스로 렌더.
|
||||||
* - 미입고가 남은 발주의 LOT·봉투(이름)로 조회 범위를 좁힌 뒤 입고 처리
|
|
||||||
* - 인수자: 대행소(agency) 담당자, 기본값 동명이면 로그인 사용자명과 일치하는 담당자
|
|
||||||
* - 인계자: 제작업체(company) 담당자
|
|
||||||
*/
|
*/
|
||||||
public function receivingScanner(): string|RedirectResponse
|
public function receivingScanner(): string|RedirectResponse
|
||||||
|
{
|
||||||
|
return $this->renderReceivingWorkspace('scanner');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발주입고(스캐너) / 일괄입고 공용 워크스페이스.
|
||||||
|
* 좌측: 발주 현황(탭: 제작업체·발주기간 / 인수자·인계자, 정렬) · 우측: 선택 발주 상세 + 스캔/수량 입고 + 입고 세부내역.
|
||||||
|
*
|
||||||
|
* @param string $mode 'scanner'(종량제 봉투) | 'batch'(음식물·폐기물)
|
||||||
|
*/
|
||||||
|
private function renderReceivingWorkspace(string $mode): string|RedirectResponse
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
$lgIdx = $this->lgIdx();
|
$lgIdx = $this->lgIdx();
|
||||||
@@ -4717,75 +4725,206 @@ SQL);
|
|||||||
return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$companyIdx = (int) old('company_idx', (int) ($this->request->getGet('company_idx') ?? 0));
|
$isBatch = ($mode === 'batch');
|
||||||
$lotNo = '';
|
$category = $isBatch ? 'bulk' : 'bag';
|
||||||
$bagCode = '';
|
$title = $isBatch ? '일괄입고(음식물,폐기물)' : '발주 입고(스캐너)';
|
||||||
|
|
||||||
|
$tab = (string) ($this->request->getGet('tab') ?? 'company');
|
||||||
|
if (! in_array($tab, ['company', 'agency'], true)) {
|
||||||
|
$tab = 'company';
|
||||||
|
}
|
||||||
|
|
||||||
|
$companyIdx = (int) ($this->request->getGet('company_idx') ?? 0);
|
||||||
|
$agencyIdx = (int) ($this->request->getGet('agency_idx') ?? 0);
|
||||||
|
$startMonth = (string) ($this->request->getGet('start_month') ?? '');
|
||||||
|
$endMonth = (string) ($this->request->getGet('end_month') ?? '');
|
||||||
|
$startDate = preg_match('/^\d{4}-\d{2}$/', $startMonth) ? $startMonth . '-01' : '';
|
||||||
|
$endDate = preg_match('/^\d{4}-\d{2}$/', $endMonth) ? date('Y-m-t', strtotime($endMonth . '-01 00:00:00')) : '';
|
||||||
|
|
||||||
$companies = model(CompanyModel::class)
|
$companies = model(CompanyModel::class)
|
||||||
->where('cp_lg_idx', $lgIdx)
|
->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)
|
||||||
->where('cp_type', '제작업체')
|
->orderBy('cp_name', 'ASC')->findAll();
|
||||||
->where('cp_state', 1)
|
$agencies = model(SalesAgencyModel::class)
|
||||||
->orderBy('cp_name', 'ASC')
|
->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll();
|
||||||
->findAll();
|
|
||||||
|
|
||||||
$defaultCompanyIdx = ! empty($companies)
|
// 조회 기준: company 탭 = 제작업체+발주기간 / agency 탭 = 인수자(대행소)+인계자(제작업체)
|
||||||
? (int) ($companies[0]->cp_idx ?? 0)
|
$rows = $this->buildReceivingCandidateRows(
|
||||||
: 0;
|
$lgIdx,
|
||||||
|
$companyIdx,
|
||||||
if ($companyIdx > 0) {
|
'',
|
||||||
$validCompany = false;
|
true,
|
||||||
foreach ($companies as $company) {
|
'',
|
||||||
if ((int) ($company->cp_idx ?? 0) === $companyIdx) {
|
$tab === 'agency' ? $agencyIdx : 0,
|
||||||
$validCompany = true;
|
$category,
|
||||||
break;
|
$tab === 'company' ? $startDate : '',
|
||||||
}
|
$tab === 'company' ? $endDate : ''
|
||||||
}
|
);
|
||||||
if (! $validCompany) {
|
|
||||||
$companyIdx = $defaultCompanyIdx;
|
|
||||||
}
|
|
||||||
} elseif ($defaultCompanyIdx > 0) {
|
|
||||||
// 초기 진입 시 드롭다운 최상단 제작업체를 기본 선택한다.
|
|
||||||
$companyIdx = $defaultCompanyIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lotChoices = [];
|
|
||||||
$bagFilterOptions = $this->receivingBagFilterOptions($lgIdx, $companyIdx, '');
|
|
||||||
|
|
||||||
$pick = $this->receivingManagerPickers($lgIdx);
|
|
||||||
$recvSel = $this->receivingReceiverSelect($lgIdx);
|
|
||||||
$receiverRef = (string) old('br_receiver_ref', $recvSel['defaultReceiverRef']);
|
|
||||||
$receiverRef = $this->sanitizeReceiverRef($recvSel['receiverOptions'], $receiverRef);
|
|
||||||
if ($receiverRef === '') {
|
|
||||||
$receiverRef = $recvSel['defaultReceiverRef'];
|
|
||||||
}
|
|
||||||
$senderIdx = (int) old('br_sender_idx', $pick['defaultSenderIdx']);
|
|
||||||
|
|
||||||
$rows = $companyIdx > 0
|
|
||||||
? $this->buildReceivingCandidateRows($lgIdx, $companyIdx, '', true, '')
|
|
||||||
: [];
|
|
||||||
$rowsByKey = [];
|
$rowsByKey = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$rowsByKey[(string) $row['row_key']] = $row;
|
$rowsByKey[(string) $row['row_key']] = $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render(
|
$pick = $this->receivingManagerPickers($lgIdx);
|
||||||
'발주 입고(스캐너)',
|
$recvSel = $this->receivingReceiverSelect($lgIdx);
|
||||||
'bag/receiving_scanner',
|
|
||||||
[
|
// 발주기간 드롭다운 옵션(최근 5년치 월)
|
||||||
'companyIdx' => $companyIdx,
|
$monthOptions = [];
|
||||||
|
$baseYear = (int) date('Y');
|
||||||
|
for ($y = $baseYear; $y >= $baseYear - 4; $y--) {
|
||||||
|
for ($m = 12; $m >= 1; $m--) {
|
||||||
|
$v = sprintf('%04d-%02d', $y, $m);
|
||||||
|
$monthOptions[] = ['value' => $v, 'label' => $y . '년 ' . $m . '월'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render($title, 'bag/receiving_scanner', [
|
||||||
|
'mode' => $mode,
|
||||||
|
'isBatch' => $isBatch,
|
||||||
|
'workspaceTitle' => $title,
|
||||||
|
'tab' => $tab,
|
||||||
'companies' => $companies,
|
'companies' => $companies,
|
||||||
'lotNo' => '',
|
'agencies' => $agencies,
|
||||||
'bagCode' => '',
|
'companyIdx' => $companyIdx,
|
||||||
'bagFilterOptions' => $bagFilterOptions,
|
'agencyIdx' => $agencyIdx,
|
||||||
'lotChoices' => $lotChoices,
|
'startMonth' => $startMonth,
|
||||||
|
'endMonth' => $endMonth,
|
||||||
|
'monthOptions' => $monthOptions,
|
||||||
'receiverOptions' => $recvSel['receiverOptions'],
|
'receiverOptions' => $recvSel['receiverOptions'],
|
||||||
'receiverRef' => $receiverRef,
|
'receiverRef' => $recvSel['defaultReceiverRef'],
|
||||||
'senders' => $pick['senders'],
|
'senders' => $pick['senders'],
|
||||||
'senderIdx' => $senderIdx,
|
'senderIdx' => $pick['defaultSenderIdx'],
|
||||||
'rows' => $rows,
|
'rows' => $rows,
|
||||||
'rowsByKey' => $rowsByKey,
|
'rowsByKey' => $rowsByKey,
|
||||||
]
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실시간 입고 처리(스캔 1회 = 1박스, 또는 수량 직접입력). AJAX 전용.
|
||||||
|
* 선택 발주행에 대해 입고 레코드 생성 → 재고 반영 → 박스코드(brpc) 생성 후 반환.
|
||||||
|
*/
|
||||||
|
public function receivingScanBox(): ResponseInterface
|
||||||
|
{
|
||||||
|
helper('admin');
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return $this->response->setJSON(['ok' => false, 'message' => '지자체를 선택해 주세요.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowKey = (string) ($this->request->getPost('row_key') ?? '');
|
||||||
|
$receiverRef = (string) ($this->request->getPost('br_receiver_ref') ?? '');
|
||||||
|
$senderIdx = (int) ($this->request->getPost('br_sender_idx') ?? 0);
|
||||||
|
$receiveDate = (string) ($this->request->getPost('br_receive_date') ?? date('Y-m-d'));
|
||||||
|
$barcode = trim((string) ($this->request->getPost('barcode') ?? ''));
|
||||||
|
$mode = (string) ($this->request->getPost('mode') ?? 'scanner');
|
||||||
|
$qtySheetIn = (int) ($this->request->getPost('qty_sheet') ?? 0);
|
||||||
|
$category = ($mode === 'batch') ? 'bulk' : 'bag';
|
||||||
|
|
||||||
|
$refresh = ['csrf' => csrf_hash()];
|
||||||
|
|
||||||
|
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $receiveDate)) {
|
||||||
|
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '입고일 형식을 확인해 주세요.']));
|
||||||
|
}
|
||||||
|
$recvSel = $this->receivingReceiverSelect($lgIdx);
|
||||||
|
$receiverRef = $this->sanitizeReceiverRef($recvSel['receiverOptions'], $receiverRef);
|
||||||
|
if ($receiverRef === '') {
|
||||||
|
$receiverRef = $recvSel['defaultReceiverRef'];
|
||||||
|
}
|
||||||
|
$receiverIdx = $this->parseReceiverRefToStoredIdx($lgIdx, $receiverRef);
|
||||||
|
if ($receiverIdx <= 0) {
|
||||||
|
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '인수자를 선택해 주세요.']));
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $this->buildReceivingCandidateRows($lgIdx, 0, '', true, '', 0, $category, '', '');
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$map[(string) $r['row_key']] = $r;
|
||||||
|
}
|
||||||
|
if (! isset($map[$rowKey])) {
|
||||||
|
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '선택한 발주를 찾을 수 없거나 이미 전량 입고되었습니다.']));
|
||||||
|
}
|
||||||
|
$base = $map[$rowKey];
|
||||||
|
$pending = (int) $base['pending_qty_sheet'];
|
||||||
|
if ($pending <= 0) {
|
||||||
|
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '미입고 잔량이 없습니다.']));
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalPerBox = max(1, (int) $base['total_per_box']);
|
||||||
|
$qty = $qtySheetIn > 0 ? $qtySheetIn : $totalPerBox; // 스캔 1회 = 1박스
|
||||||
|
if ($qty > $pending) {
|
||||||
|
$qty = $pending;
|
||||||
|
}
|
||||||
|
$qtyBox = intdiv($qty, $totalPerBox);
|
||||||
|
$senderName = $this->resolveCompanySenderName($lgIdx, $senderIdx);
|
||||||
|
$sender = $senderName !== '' ? $senderName : (string) ($base['company_rep_name'] ?? '');
|
||||||
|
|
||||||
|
$recvModel = model(BagReceivingModel::class);
|
||||||
|
$invModel = model(BagInventoryModel::class);
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$db->transStart();
|
||||||
|
$recvModel->insert([
|
||||||
|
'br_bo_idx' => (int) $base['bo_idx'],
|
||||||
|
'br_lg_idx' => $lgIdx,
|
||||||
|
'br_bag_code' => (string) $base['bag_code'],
|
||||||
|
'br_bag_name' => (string) $base['bag_name'],
|
||||||
|
'br_qty_box' => $qtyBox,
|
||||||
|
'br_qty_sheet' => $qty,
|
||||||
|
'br_receive_date' => $receiveDate,
|
||||||
|
'br_receiver_idx' => $receiverIdx,
|
||||||
|
'br_sender_name' => $sender,
|
||||||
|
'br_type' => 'scanner',
|
||||||
|
'br_regdate' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
$brIdx = (int) $recvModel->getInsertID();
|
||||||
|
$invModel->adjustQty($lgIdx, (string) $base['bag_code'], (string) $base['bag_name'], $qty);
|
||||||
|
$this->createReceivingPackCodes(
|
||||||
|
$lgIdx,
|
||||||
|
$brIdx,
|
||||||
|
(int) $base['bo_idx'],
|
||||||
|
(string) $base['bag_code'],
|
||||||
|
(string) $base['bag_name'],
|
||||||
|
$qty,
|
||||||
|
max(1, (int) $base['pack_per_sheet']),
|
||||||
|
$totalPerBox,
|
||||||
|
(string) ($base['lot_no'] ?? '')
|
||||||
);
|
);
|
||||||
|
$db->transComplete();
|
||||||
|
if (! $db->transStatus()) {
|
||||||
|
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '입고 처리 중 오류가 발생했습니다.']));
|
||||||
|
}
|
||||||
|
|
||||||
|
$boxRows = $db->table('bag_receiving_pack_code')
|
||||||
|
->select('brpc_box_code, COUNT(*) AS pack_cnt, SUM(brpc_sheet_qty) AS sheet_sum')
|
||||||
|
->where('brpc_br_idx', $brIdx)
|
||||||
|
->groupBy('brpc_box_code')
|
||||||
|
->orderBy('brpc_box_code', 'ASC')
|
||||||
|
->get()->getResultArray();
|
||||||
|
$boxes = array_map(static fn ($b): array => [
|
||||||
|
'box_code' => (string) ($b['brpc_box_code'] ?? ''),
|
||||||
|
'packs' => (int) ($b['pack_cnt'] ?? 0),
|
||||||
|
'sheet' => (int) ($b['sheet_sum'] ?? 0),
|
||||||
|
], $boxRows);
|
||||||
|
|
||||||
|
// 갱신된 미입고/입고량 재계산
|
||||||
|
$after = $this->buildReceivingCandidateRows($lgIdx, 0, '', false, '', 0, $category, '', '');
|
||||||
|
$updated = null;
|
||||||
|
foreach ($after as $r) {
|
||||||
|
if ((string) $r['row_key'] === $rowKey) {
|
||||||
|
$updated = $r;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->response->setJSON(array_merge($refresh, [
|
||||||
|
'ok' => true,
|
||||||
|
'row_key' => $rowKey,
|
||||||
|
'bag_name' => (string) $base['bag_name'],
|
||||||
|
'qty_sheet' => $qty,
|
||||||
|
'qty_box' => $qtyBox,
|
||||||
|
'received_qty_sheet' => (int) ($updated['received_qty_sheet'] ?? ((int) $base['received_qty_sheet'] + $qty)),
|
||||||
|
'pending_qty_sheet' => (int) ($updated['pending_qty_sheet'] ?? max(0, $pending - $qty)),
|
||||||
|
'boxes' => $boxes,
|
||||||
|
'scanned' => $barcode,
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function receivingScannerStore(): RedirectResponse
|
public function receivingScannerStore(): RedirectResponse
|
||||||
@@ -4906,51 +5045,12 @@ SQL);
|
|||||||
/**
|
/**
|
||||||
* 일괄 입고: LOT-봉투 행 기준 미입고량 전체 입고.
|
* 일괄 입고: LOT-봉투 행 기준 미입고량 전체 입고.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 일괄입고(음식물,폐기물) — 발주입고(스캐너)와 동일 워크스페이스, 음식물·폐기물만 표시.
|
||||||
|
*/
|
||||||
public function receivingBatch(): string|RedirectResponse
|
public function receivingBatch(): string|RedirectResponse
|
||||||
{
|
{
|
||||||
helper('admin');
|
return $this->renderReceivingWorkspace('batch');
|
||||||
$lgIdx = $this->lgIdx();
|
|
||||||
if (! $lgIdx) {
|
|
||||||
return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$companyIdx = (int) ($this->request->getGet('company_idx') ?? 0);
|
|
||||||
$bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
|
|
||||||
|
|
||||||
$companies = model(CompanyModel::class)
|
|
||||||
->where('cp_lg_idx', $lgIdx)
|
|
||||||
->where('cp_type', '제작업체')
|
|
||||||
->where('cp_state', 1)
|
|
||||||
->orderBy('cp_name', 'ASC')
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
||||||
$bagCodeOptions = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
|
||||||
$pick = $this->receivingManagerPickers($lgIdx);
|
|
||||||
$recvSel = $this->receivingReceiverSelect($lgIdx);
|
|
||||||
$receiverRef = (string) old('br_receiver_ref', $recvSel['defaultReceiverRef']);
|
|
||||||
$receiverRef = $this->sanitizeReceiverRef($recvSel['receiverOptions'], $receiverRef);
|
|
||||||
if ($receiverRef === '') {
|
|
||||||
$receiverRef = $recvSel['defaultReceiverRef'];
|
|
||||||
}
|
|
||||||
// 조회 화면에서는 입고완료 행도 함께 보여 미입고량 0을 확인할 수 있게 한다.
|
|
||||||
$rows = $this->buildReceivingCandidateRows($lgIdx, $companyIdx, $bagCode, false, '');
|
|
||||||
|
|
||||||
return $this->render(
|
|
||||||
'일괄 입고',
|
|
||||||
'bag/receiving_batch',
|
|
||||||
[
|
|
||||||
'companyIdx' => $companyIdx,
|
|
||||||
'bagCode' => $bagCode,
|
|
||||||
'companies' => $companies,
|
|
||||||
'bagCodeOptions' => $bagCodeOptions,
|
|
||||||
'receiverOptions' => $recvSel['receiverOptions'],
|
|
||||||
'receiverRef' => $receiverRef,
|
|
||||||
'senders' => $pick['senders'],
|
|
||||||
'senderIdx' => (int) old('br_sender_idx', $pick['defaultSenderIdx']),
|
|
||||||
'rows' => $rows,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function receivingBatchStore(): RedirectResponse
|
public function receivingBatchStore(): RedirectResponse
|
||||||
@@ -5266,7 +5366,28 @@ SQL);
|
|||||||
*
|
*
|
||||||
* @param string $lotNo 빈 문자열이면 LOT 제한 없음. 지정 시 해당 LOT(최신 헤드) 발주만.
|
* @param string $lotNo 빈 문자열이면 LOT 제한 없음. 지정 시 해당 LOT(최신 헤드) 발주만.
|
||||||
*/
|
*/
|
||||||
private function buildReceivingCandidateRows(int $lgIdx, int $companyIdx = 0, string $bagCode = '', bool $onlyPending = true, string $lotNo = ''): array
|
/**
|
||||||
|
* 봉투 이름 기준 품목 분류 (판매대장 통계와 동일 규칙).
|
||||||
|
* 반환: '음식물' | '폐기물' | '봉투'
|
||||||
|
*/
|
||||||
|
private function receivingBagCategory(string $bagName): string
|
||||||
|
{
|
||||||
|
if (mb_strpos($bagName, '음식물') !== false) {
|
||||||
|
return '음식물';
|
||||||
|
}
|
||||||
|
if (mb_strpos($bagName, '폐기물') !== false || mb_strpos($bagName, '대형') !== false) {
|
||||||
|
return '폐기물';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '봉투';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $category '' = 전체, 'bulk' = 음식물+폐기물(일괄입고용), 'bag' = 봉투(종량제)만
|
||||||
|
* @param string $startDate 'YYYY-MM-DD' (발주일 하한, '' = 무시)
|
||||||
|
* @param string $endDate 'YYYY-MM-DD' (발주일 상한, '' = 무시)
|
||||||
|
*/
|
||||||
|
private function buildReceivingCandidateRows(int $lgIdx, int $companyIdx = 0, string $bagCode = '', bool $onlyPending = true, string $lotNo = '', int $agencyIdx = 0, string $category = '', string $startDate = '', string $endDate = ''): array
|
||||||
{
|
{
|
||||||
$orderBuilder = model(BagOrderModel::class)
|
$orderBuilder = model(BagOrderModel::class)
|
||||||
->where('bo_lg_idx', $lgIdx)
|
->where('bo_lg_idx', $lgIdx)
|
||||||
@@ -5280,6 +5401,15 @@ SQL);
|
|||||||
if ($companyIdx > 0) {
|
if ($companyIdx > 0) {
|
||||||
$orderBuilder->where('bo_company_idx', $companyIdx);
|
$orderBuilder->where('bo_company_idx', $companyIdx);
|
||||||
}
|
}
|
||||||
|
if ($agencyIdx > 0) {
|
||||||
|
$orderBuilder->where('bo_agency_idx', $agencyIdx);
|
||||||
|
}
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
|
||||||
|
$orderBuilder->where('bo_order_date >=', $startDate);
|
||||||
|
}
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
|
||||||
|
$orderBuilder->where('bo_order_date <=', $endDate);
|
||||||
|
}
|
||||||
$orders = $orderBuilder->findAll();
|
$orders = $orderBuilder->findAll();
|
||||||
if (empty($orders)) {
|
if (empty($orders)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -5348,6 +5478,16 @@ SQL);
|
|||||||
|
|
||||||
$unit = $unitMap[$itemBagCode] ?? ['pack_per_sheet' => 1, 'total_per_box' => 1];
|
$unit = $unitMap[$itemBagCode] ?? ['pack_per_sheet' => 1, 'total_per_box' => 1];
|
||||||
$companyInfo = $companyMap[(int) ($order->bo_company_idx ?? 0)] ?? ['name' => '', 'rep' => ''];
|
$companyInfo = $companyMap[(int) ($order->bo_company_idx ?? 0)] ?? ['name' => '', 'rep' => ''];
|
||||||
|
$bagName = (string) ($item->boi_bag_name ?? '');
|
||||||
|
$bagCategory = $this->receivingBagCategory($bagName);
|
||||||
|
|
||||||
|
// 카테고리 필터 (일괄입고: 음식물+폐기물만 / bag: 봉투만)
|
||||||
|
if ($category === 'bulk' && $bagCategory === '봉투') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($category === 'bag' && $bagCategory !== '봉투') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
'row_key' => $boIdx . '|' . $itemBagCode,
|
'row_key' => $boIdx . '|' . $itemBagCode,
|
||||||
@@ -5359,7 +5499,8 @@ SQL);
|
|||||||
'company_rep_name' => (string) ($companyInfo['rep'] ?? ''),
|
'company_rep_name' => (string) ($companyInfo['rep'] ?? ''),
|
||||||
'agency_name' => (string) ($agencyMap[(int) ($order->bo_agency_idx ?? 0)] ?? ''),
|
'agency_name' => (string) ($agencyMap[(int) ($order->bo_agency_idx ?? 0)] ?? ''),
|
||||||
'bag_code' => $itemBagCode,
|
'bag_code' => $itemBagCode,
|
||||||
'bag_name' => (string) ($item->boi_bag_name ?? ''),
|
'bag_name' => $bagName,
|
||||||
|
'bag_category' => $bagCategory,
|
||||||
'order_qty_sheet' => $orderQtySheet,
|
'order_qty_sheet' => $orderQtySheet,
|
||||||
'received_qty_sheet' => $receivedQtySheet,
|
'received_qty_sheet' => $receivedQtySheet,
|
||||||
'pending_qty_sheet' => $pendingQtySheet,
|
'pending_qty_sheet' => $pendingQtySheet,
|
||||||
|
|||||||
@@ -1,148 +1,360 @@
|
|||||||
|
<?php
|
||||||
|
$mode = (string) ($mode ?? 'scanner');
|
||||||
|
$isBatch = (bool) ($isBatch ?? false);
|
||||||
|
$tab = (string) ($tab ?? 'company');
|
||||||
|
$companies = is_array($companies ?? null) ? $companies : [];
|
||||||
|
$agencies = is_array($agencies ?? null) ? $agencies : [];
|
||||||
|
$rows = is_array($rows ?? null) ? $rows : [];
|
||||||
|
$monthOptions = is_array($monthOptions ?? null) ? $monthOptions : [];
|
||||||
|
$receiverOptions = is_array($receiverOptions ?? null) ? $receiverOptions : [];
|
||||||
|
$senders = is_array($senders ?? null) ? $senders : [];
|
||||||
|
$companyIdx = (int) ($companyIdx ?? 0);
|
||||||
|
$agencyIdx = (int) ($agencyIdx ?? 0);
|
||||||
|
$baseUrl = $isBatch ? base_url('bag/receiving/batch') : base_url('bag/receiving/scanner');
|
||||||
|
$otherUrl = $isBatch ? base_url('bag/receiving/scanner') : base_url('bag/receiving/batch');
|
||||||
|
$otherLabel = $isBatch ? '발주 입고(스캐너)' : '일괄입고(음식물,폐기물)';
|
||||||
|
$tabUrl = static function (string $t) use ($baseUrl): string {
|
||||||
|
return $baseUrl . '?' . http_build_query(['tab' => $t]);
|
||||||
|
};
|
||||||
|
?>
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel flex flex-wrap items-center justify-between gap-2">
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel flex flex-wrap items-center justify-between gap-2">
|
||||||
<span class="text-sm font-bold text-gray-700">발주 입고(스캐너 대체 수동입력)</span>
|
<span class="text-sm font-bold text-gray-700"><?= esc((string) ($workspaceTitle ?? '발주 입고')) ?></span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a href="<?= base_url('bag/receiving/batch') ?>" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">일괄 입고</a>
|
<a href="<?= esc($otherUrl) ?>" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50"><?= esc($otherLabel) ?></a>
|
||||||
<a href="<?= base_url('bag/receiving/status') ?>" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">입고 현황</a>
|
<a href="<?= base_url('bag/receiving/status') ?>" class="border border-gray-300 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">입고 현황</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<?php /* 플래시 메시지는 레이아웃(embed/portal)에서 한 번만 표시 — 중복 출력 방지 */ ?>
|
<div class="grid grid-cols-1 xl:grid-cols-12 gap-3 mt-2">
|
||||||
|
<!-- 좌: 발주 현황 -->
|
||||||
|
<section class="xl:col-span-7 border border-gray-300 rounded-lg bg-white overflow-hidden flex flex-col">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700">발주 현황</div>
|
||||||
|
|
||||||
<section class="p-2 bg-white border border-gray-300 rounded-lg mt-2">
|
<!-- 조회 기준 탭 -->
|
||||||
<form method="get" action="<?= base_url('bag/receiving/scanner') ?>" class="flex flex-wrap items-end gap-2">
|
<div class="flex border-b border-gray-200 text-sm">
|
||||||
<div class="flex flex-col min-w-[14rem] max-w-[22rem]">
|
<a href="<?= esc($tabUrl('company')) ?>" class="px-4 py-2 border-b-2 <?= $tab === 'company' ? 'border-blue-600 text-blue-700 font-semibold' : 'border-transparent text-gray-500 hover:text-gray-700' ?>">제작업체 · 발주기간</a>
|
||||||
<label class="text-xs text-gray-500 mb-1">제작업체</label>
|
<a href="<?= esc($tabUrl('agency')) ?>" class="px-4 py-2 border-b-2 <?= $tab === 'agency' ? 'border-blue-600 text-blue-700 font-semibold' : 'border-transparent text-gray-500 hover:text-gray-700' ?>">인수자 · 인계자</a>
|
||||||
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1.5 text-sm bg-white">
|
</div>
|
||||||
<option value="0">제작업체 선택</option>
|
|
||||||
<?php foreach (($companies ?? []) as $company): ?>
|
<!-- 조회 필터 -->
|
||||||
<option value="<?= (int) ($company->cp_idx ?? 0) ?>" <?= (int) ($companyIdx ?? 0) === (int) ($company->cp_idx ?? 0) ? 'selected' : '' ?>>
|
<form method="get" action="<?= esc($baseUrl) ?>" class="p-2 flex flex-wrap items-end gap-2 border-b border-gray-100 text-sm">
|
||||||
<?= esc((string) ($company->cp_name ?? '')) ?>
|
<input type="hidden" name="tab" value="<?= esc($tab) ?>"/>
|
||||||
</option>
|
<?php if ($tab === 'company'): ?>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="text-xs text-gray-500 mb-0.5">제작업체</label>
|
||||||
|
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem]">
|
||||||
|
<option value="0">전체</option>
|
||||||
|
<?php foreach ($companies as $c): ?>
|
||||||
|
<option value="<?= (int) ($c->cp_idx ?? 0) ?>" <?= $companyIdx === (int) ($c->cp_idx ?? 0) ? 'selected' : '' ?>><?= esc((string) ($c->cp_name ?? '')) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="text-xs text-gray-500 mb-0.5">발주기간(시작월)</label>
|
||||||
|
<select name="start_month" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[8rem]">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<?php foreach ($monthOptions as $opt): ?>
|
||||||
|
<option value="<?= esc((string) $opt['value']) ?>" <?= (string) ($startMonth ?? '') === (string) $opt['value'] ? 'selected' : '' ?>><?= esc((string) $opt['label']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="text-xs text-gray-500 mb-0.5">종료월</label>
|
||||||
|
<select name="end_month" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[8rem]">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<?php foreach ($monthOptions as $opt): ?>
|
||||||
|
<option value="<?= esc((string) $opt['value']) ?>" <?= (string) ($endMonth ?? '') === (string) $opt['value'] ? 'selected' : '' ?>><?= esc((string) $opt['label']) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="text-xs text-gray-500 mb-0.5">인수자(대행소)</label>
|
||||||
|
<select name="agency_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem]">
|
||||||
|
<option value="0">전체</option>
|
||||||
|
<?php foreach ($agencies as $a): ?>
|
||||||
|
<option value="<?= (int) ($a->sa_idx ?? 0) ?>" <?= $agencyIdx === (int) ($a->sa_idx ?? 0) ? 'selected' : '' ?>><?= esc((string) ($a->sa_name ?? '')) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="text-xs text-gray-500 mb-0.5">인계자(제작업체)</label>
|
||||||
|
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem]">
|
||||||
|
<option value="0">전체</option>
|
||||||
|
<?php foreach ($companies as $c): ?>
|
||||||
|
<option value="<?= (int) ($c->cp_idx ?? 0) ?>" <?= $companyIdx === (int) ($c->cp_idx ?? 0) ? 'selected' : '' ?>><?= esc((string) ($c->cp_name ?? '')) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm shrink-0">조회</button>
|
|
||||||
</form>
|
|
||||||
<?php if ((int) ($companyIdx ?? 0) <= 0): ?>
|
|
||||||
<p class="text-xs text-gray-600 mt-2">제작업체를 선택하면 해당 업체 발주 중 미입고 내역을 조회합니다.</p>
|
|
||||||
<?php elseif (empty($rows ?? [])): ?>
|
|
||||||
<p class="text-xs text-amber-700 mt-2">미입고 잔량이 있는 발주가 없습니다. 발주 등록 후 다시 확인해 주세요.</p>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<form action="<?= base_url('bag/receiving/scanner/store') ?>" method="post" class="mt-2 space-y-2">
|
<div class="overflow-auto max-h-[60vh]">
|
||||||
<?= csrf_field() ?>
|
<table class="w-full data-table text-sm" id="order-status-table">
|
||||||
<input type="hidden" name="company_idx" value="<?= (int) ($companyIdx ?? 0) ?>" />
|
|
||||||
|
|
||||||
<section class="p-2 bg-white border border-gray-300 rounded-lg">
|
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2 items-end">
|
|
||||||
<div class="xl:col-span-3 flex items-center gap-2">
|
|
||||||
<label class="text-sm text-gray-600 shrink-0 w-28">인수자 (대행소)</label>
|
|
||||||
<select name="br_receiver_ref" class="border border-gray-300 rounded px-2 py-1 text-sm w-full" required>
|
|
||||||
<?php foreach (($receiverOptions ?? []) as $opt): ?>
|
|
||||||
<option value="<?= esc((string) ($opt['ref'] ?? '')) ?>" <?= (string) ($receiverRef ?? '') === (string) ($opt['ref'] ?? '') ? 'selected' : '' ?>>
|
|
||||||
<?= esc((string) ($opt['label'] ?? '')) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="xl:col-span-3 flex items-center gap-2">
|
|
||||||
<label class="text-sm text-gray-600 shrink-0 w-28">인계자 (제작업체)</label>
|
|
||||||
<select name="br_sender_idx" class="border border-gray-300 rounded px-2 py-1 text-sm w-full">
|
|
||||||
<?php foreach (($senders ?? []) as $sender): ?>
|
|
||||||
<option value="<?= (int) ($sender->mg_idx ?? 0) ?>" <?= (int) ($senderIdx ?? 0) === (int) ($sender->mg_idx ?? 0) ? 'selected' : '' ?>>
|
|
||||||
<?= esc((string) ($sender->mg_name ?? '')) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="xl:col-span-2 flex items-center gap-2">
|
|
||||||
<label class="text-sm text-gray-600 w-16">입고일</label>
|
|
||||||
<input type="date" name="br_receive_date" value="<?= esc((string) old('br_receive_date', date('Y-m-d'))) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm w-full" required />
|
|
||||||
</div>
|
|
||||||
<div class="xl:col-span-2">
|
|
||||||
<button type="submit" class="w-full border border-blue-600 text-blue-700 px-2 py-1 rounded-sm text-sm hover:bg-blue-50">입고 처리</button>
|
|
||||||
</div>
|
|
||||||
<div class="xl:col-span-2 text-xs text-gray-500">상단에서 제작업체를 조회한 뒤, 아래에서 입고량(매)을 입력해 저장합니다.</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="border border-gray-300 rounded-lg p-4 overflow-auto bg-white">
|
|
||||||
<table class="w-full data-table text-sm">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-center">발주일자</th>
|
<th class="text-center sort-th cursor-pointer select-none" data-sort="order_date" data-type="str" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">발주일자<span class="sort-ind"></span></th>
|
||||||
<th>봉투종류</th>
|
<th class="text-left sort-th cursor-pointer select-none" data-sort="bag_name" data-type="str" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투종류<span class="sort-ind"></span></th>
|
||||||
<th class="text-right">발주량(매)</th>
|
<th class="text-right sort-th cursor-pointer select-none" data-sort="order_qty" data-type="num" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">발주량<span class="sort-ind"></span></th>
|
||||||
<th class="text-right">미입고량(매)</th>
|
<th class="text-right sort-th cursor-pointer select-none" data-sort="pending" data-type="num" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">미입고량<span class="sort-ind"></span></th>
|
||||||
<th class="text-right">입고량(매)</th>
|
<th class="text-right sort-th cursor-pointer select-none" data-sort="received" data-type="num" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">입고량<span class="sort-ind"></span></th>
|
||||||
<th>제작업체</th>
|
<th class="text-left" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">제작업체</th>
|
||||||
<th class="text-center">LOT NO</th>
|
<th class="text-center" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">LOT</th>
|
||||||
<th class="text-center">발주NO</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="order-status-body">
|
||||||
<?php foreach (($rows ?? []) as $row): ?>
|
<?php foreach ($rows as $row): $k = (string) ($row['row_key'] ?? ''); ?>
|
||||||
<?php $k = (string) ($row['row_key'] ?? ''); ?>
|
<tr class="os-row cursor-pointer hover:bg-blue-50"
|
||||||
<tr data-row-key="<?= esc($k) ?>" data-lot-no="<?= esc((string) ($row['lot_no'] ?? '')) ?>" data-bag-code="<?= esc((string) ($row['bag_code'] ?? '')) ?>" data-total-per-box="<?= (int) ($row['total_per_box'] ?? 1) ?>" data-pack-per-sheet="<?= (int) ($row['pack_per_sheet'] ?? 1) ?>" data-pending-original="<?= (int) ($row['pending_qty_sheet'] ?? 0) ?>">
|
data-row-key="<?= esc($k, 'attr') ?>"
|
||||||
|
data-bag-name="<?= esc((string) ($row['bag_name'] ?? ''), 'attr') ?>"
|
||||||
|
data-company="<?= esc((string) ($row['company_name'] ?? ''), 'attr') ?>"
|
||||||
|
data-lot="<?= esc((string) ($row['lot_no'] ?? ''), 'attr') ?>"
|
||||||
|
data-order-qty="<?= (int) ($row['order_qty_sheet'] ?? 0) ?>"
|
||||||
|
data-pending="<?= (int) ($row['pending_qty_sheet'] ?? 0) ?>"
|
||||||
|
data-received="<?= (int) ($row['received_qty_sheet'] ?? 0) ?>"
|
||||||
|
data-total-per-box="<?= (int) ($row['total_per_box'] ?? 1) ?>">
|
||||||
<td class="text-center"><?= esc((string) ($row['order_date'] ?? '')) ?></td>
|
<td class="text-center"><?= esc((string) ($row['order_date'] ?? '')) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
|
<td class="text-left pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
|
||||||
<td class="text-right"><?= number_format((int) ($row['order_qty_sheet'] ?? 0)) ?></td>
|
<td class="text-right pr-2"><?= number_format((int) ($row['order_qty_sheet'] ?? 0)) ?></td>
|
||||||
<td class="text-right pending-cell"><?= number_format((int) ($row['pending_qty_sheet'] ?? 0)) ?></td>
|
<td class="text-right pr-2 cell-pending"><?= number_format((int) ($row['pending_qty_sheet'] ?? 0)) ?></td>
|
||||||
<td class="text-right">
|
<td class="text-right pr-2 cell-received"><?= number_format((int) ($row['received_qty_sheet'] ?? 0)) ?></td>
|
||||||
<input type="number" min="0" max="<?= (int) ($row['pending_qty_sheet'] ?? 0) ?>" name="receive_qty_sheet[<?= esc($k) ?>]" value="<?= esc((string) old('receive_qty_sheet.' . $k, '0')) ?>" class="w-24 border border-gray-300 rounded px-1 py-0.5 text-sm text-right receive-input" />
|
|
||||||
</td>
|
|
||||||
<td class="text-left pl-2"><?= esc((string) ($row['company_name'] ?? '')) ?></td>
|
<td class="text-left pl-2"><?= esc((string) ($row['company_name'] ?? '')) ?></td>
|
||||||
<td class="text-center font-mono"><?= esc((string) ($row['lot_no'] ?? '')) ?></td>
|
<td class="text-center font-mono"><?= esc((string) ($row['lot_no'] ?? '')) ?></td>
|
||||||
<td class="text-center"><?= esc((string) ($row['order_no'] ?? '')) ?></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($rows ?? [])): ?>
|
<?php if (empty($rows)): ?>
|
||||||
<tr><td colspan="8" class="text-center text-gray-400 py-4"><?php
|
<tr><td colspan="7" class="text-center text-gray-400 py-6">조회 조건에 맞는 미입고 발주가 없습니다.</td></tr>
|
||||||
if ((int) ($companyIdx ?? 0) <= 0) {
|
|
||||||
echo '제작업체를 선택하고 조회해 주세요.';
|
|
||||||
} else {
|
|
||||||
echo '해당 제작업체의 미입고 발주 내역이 없습니다.';
|
|
||||||
}
|
|
||||||
?></td></tr>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<!-- 우: 선택 발주 상세 + 스캔/수량 입고 + 입고 세부내역 -->
|
||||||
<a href="<?= base_url('bag/purchase-inbound') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm">취소</a>
|
<section class="xl:col-span-5 flex flex-col gap-3">
|
||||||
|
<div class="border border-gray-300 rounded-lg bg-white overflow-hidden">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700">선택 발주 · 입고 처리</div>
|
||||||
|
<div class="p-3 space-y-3 text-sm">
|
||||||
|
<div id="recv-empty" class="text-gray-400 py-6 text-center">좌측 발주 현황에서 발주를 선택하세요.</div>
|
||||||
|
<div id="recv-detail" class="hidden space-y-3">
|
||||||
|
<dl class="grid grid-cols-[5rem_1fr] gap-y-1 text-[13px] border border-gray-200 rounded p-2 bg-gray-50">
|
||||||
|
<dt class="font-semibold text-gray-600">봉투종류</dt><dd id="sel-bag"></dd>
|
||||||
|
<dt class="font-semibold text-gray-600">제작업체</dt><dd id="sel-company"></dd>
|
||||||
|
<dt class="font-semibold text-gray-600">발주량</dt><dd><span id="sel-orderqty">0</span> 매</dd>
|
||||||
|
<dt class="font-semibold text-gray-600">입고량</dt><dd><span id="sel-received" class="font-bold text-blue-700">0</span> 매</dd>
|
||||||
|
<dt class="font-semibold text-gray-600">미입고량</dt><dd><span id="sel-pending" class="font-bold text-red-600">0</span> 매</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<label class="flex flex-col text-xs text-gray-500">인수자(대행소)
|
||||||
|
<select id="br_receiver_ref" class="mt-0.5 border border-gray-300 rounded px-2 py-1 text-sm">
|
||||||
|
<?php foreach ($receiverOptions as $opt): ?>
|
||||||
|
<option value="<?= esc((string) ($opt['ref'] ?? ''), 'attr') ?>" <?= (string) ($receiverRef ?? '') === (string) ($opt['ref'] ?? '') ? 'selected' : '' ?>><?= esc((string) ($opt['label'] ?? '')) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-col text-xs text-gray-500">인계자(제작업체)
|
||||||
|
<select id="br_sender_idx" class="mt-0.5 border border-gray-300 rounded px-2 py-1 text-sm">
|
||||||
|
<?php foreach ($senders as $s): ?>
|
||||||
|
<option value="<?= (int) ($s->mg_idx ?? 0) ?>" <?= (int) ($senderIdx ?? 0) === (int) ($s->mg_idx ?? 0) ? 'selected' : '' ?>><?= esc((string) ($s->mg_name ?? '')) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<label class="flex flex-col text-xs text-gray-500">입고일
|
||||||
|
<input type="date" id="br_receive_date" value="<?= esc(date('Y-m-d'), 'attr') ?>" class="mt-0.5 border border-gray-300 rounded px-2 py-1 text-sm w-40"/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="border border-emerald-300 rounded-lg p-2 bg-emerald-50/40">
|
||||||
|
<label class="block text-xs font-semibold text-emerald-800 mb-1"><i class="fa-solid fa-barcode mr-1"></i>박스 바코드 스캔 (스캔 1회 = 1박스 입고)</label>
|
||||||
|
<input type="text" id="scan-input" autocomplete="off" placeholder="박스 바코드를 스캔 후 Enter" class="border border-gray-300 rounded px-2 py-1 text-sm w-full" disabled/>
|
||||||
|
<div class="flex items-center gap-2 mt-2">
|
||||||
|
<span class="text-xs text-gray-500 shrink-0">또는 수량 직접입력(매)</span>
|
||||||
|
<input type="number" id="qty-input" min="1" placeholder="매수" class="border border-gray-300 rounded px-2 py-1 text-sm w-28 text-right" disabled/>
|
||||||
|
<button type="button" id="btn-receive" class="border border-blue-600 text-blue-700 px-3 py-1 rounded-sm text-sm hover:bg-blue-50 disabled:opacity-50" disabled>입고 처리</button>
|
||||||
|
</div>
|
||||||
|
<p id="scan-msg" class="text-xs text-gray-500 mt-1"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 rounded-lg bg-white overflow-hidden">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700 flex items-center justify-between">
|
||||||
|
<span>입고 세부내역 (박스코드)</span>
|
||||||
|
<span class="text-gray-500 text-xs">박스 <span id="detail-count" class="font-bold text-blue-700">0</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-auto max-h-[36vh]">
|
||||||
|
<table class="w-full data-table text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-10 text-center" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">No</th>
|
||||||
|
<th class="text-left" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">봉투종류</th>
|
||||||
|
<th class="text-left" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">박스코드</th>
|
||||||
|
<th class="w-16 text-right" style="background:#fff;box-shadow:inset 0 -1px 0 #e5e7eb;">수량</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="detail-body">
|
||||||
|
<tr><td colspan="4" class="text-center text-gray-400 py-6">스캔/입고 시 박스코드가 추가됩니다.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const parseIntSafe = (v) => {
|
const mode = <?= json_encode($mode) ?>;
|
||||||
const n = Number(String(v ?? '').replace(/,/g, ''));
|
const scanApi = new URL('<?= base_url('bag/receiving/scan-box') ?>', window.location.href).pathname;
|
||||||
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
const csrfName = '<?= csrf_token() ?>';
|
||||||
};
|
let csrfHash = '<?= csrf_hash() ?>';
|
||||||
|
const nf = (n) => new Intl.NumberFormat('ko-KR').format(Number(n) || 0);
|
||||||
|
const esc = (s) => String(s == null ? '' : s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
||||||
|
|
||||||
const refreshPending = (row) => {
|
const tbody = document.getElementById('order-status-body');
|
||||||
const pendingCell = row.querySelector('.pending-cell');
|
const detailBody = document.getElementById('detail-body');
|
||||||
const input = row.querySelector('.receive-input');
|
const detailCountEl = document.getElementById('detail-count');
|
||||||
if (!pendingCell || !input) return;
|
const scanInput = document.getElementById('scan-input');
|
||||||
const original = parseIntSafe(row.getAttribute('data-pending-original'));
|
const qtyInput = document.getElementById('qty-input');
|
||||||
const current = parseIntSafe(input.value);
|
const btnReceive = document.getElementById('btn-receive');
|
||||||
const remain = Math.max(0, original - current);
|
const scanMsg = document.getElementById('scan-msg');
|
||||||
pendingCell.textContent = Number(remain || 0).toLocaleString('ko-KR');
|
let selectedRow = null;
|
||||||
};
|
let detailNo = 0;
|
||||||
|
|
||||||
document.querySelectorAll('.receive-input').forEach((input) => {
|
// ── 정렬 ──
|
||||||
input.addEventListener('input', (e) => {
|
let sortKey = '', sortDir = 'asc';
|
||||||
const row = e.target.closest('tr');
|
const numOf = (tr, key) => {
|
||||||
if (!row) return;
|
const map = { order_qty: 'data-order-qty', pending: 'data-pending', received: 'data-received' };
|
||||||
const original = parseIntSafe(row.getAttribute('data-pending-original'));
|
if (map[key]) return Number(tr.getAttribute(map[key]) || 0);
|
||||||
const current = Math.min(parseIntSafe(input.value), original);
|
return 0;
|
||||||
input.value = String(current);
|
};
|
||||||
refreshPending(row);
|
const strOf = (tr, key) => {
|
||||||
|
if (key === 'order_date') return tr.children[0].textContent.trim();
|
||||||
|
if (key === 'bag_name') return tr.getAttribute('data-bag-name') || '';
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
document.querySelectorAll('#order-status-table .sort-th').forEach((th) => {
|
||||||
|
th.addEventListener('click', () => {
|
||||||
|
const key = th.dataset.sort, type = th.dataset.type;
|
||||||
|
if (sortKey === key) { sortDir = sortDir === 'asc' ? 'desc' : 'asc'; } else { sortKey = key; sortDir = 'asc'; }
|
||||||
|
const rows = Array.prototype.slice.call(tbody.querySelectorAll('tr.os-row'));
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
let va, vb;
|
||||||
|
if (type === 'num') { va = numOf(a, key); vb = numOf(b, key); return sortDir === 'asc' ? va - vb : vb - va; }
|
||||||
|
va = strOf(a, key); vb = strOf(b, key);
|
||||||
|
return sortDir === 'asc' ? va.localeCompare(vb, 'ko') : vb.localeCompare(va, 'ko');
|
||||||
});
|
});
|
||||||
const row = input.closest('tr');
|
rows.forEach((r) => tbody.appendChild(r));
|
||||||
if (row) refreshPending(row);
|
document.querySelectorAll('#order-status-table .sort-ind').forEach((s) => { s.textContent = ''; });
|
||||||
|
const ind = th.querySelector('.sort-ind');
|
||||||
|
if (ind) ind.textContent = sortDir === 'asc' ? ' ▲' : ' ▼';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 행 선택 ──
|
||||||
|
function selectRow(tr) {
|
||||||
|
if (selectedRow) selectedRow.classList.remove('bg-blue-100');
|
||||||
|
selectedRow = tr;
|
||||||
|
tr.classList.add('bg-blue-100');
|
||||||
|
document.getElementById('recv-empty').classList.add('hidden');
|
||||||
|
document.getElementById('recv-detail').classList.remove('hidden');
|
||||||
|
document.getElementById('sel-bag').textContent = tr.getAttribute('data-bag-name') || '';
|
||||||
|
document.getElementById('sel-company').textContent = tr.getAttribute('data-company') || '-';
|
||||||
|
document.getElementById('sel-orderqty').textContent = nf(tr.getAttribute('data-order-qty'));
|
||||||
|
document.getElementById('sel-received').textContent = nf(tr.getAttribute('data-received'));
|
||||||
|
document.getElementById('sel-pending').textContent = nf(tr.getAttribute('data-pending'));
|
||||||
|
const pending = Number(tr.getAttribute('data-pending') || 0);
|
||||||
|
const enabled = pending > 0;
|
||||||
|
scanInput.disabled = !enabled;
|
||||||
|
qtyInput.disabled = !enabled;
|
||||||
|
btnReceive.disabled = !enabled;
|
||||||
|
scanMsg.textContent = enabled ? '' : '이미 전량 입고된 발주입니다.';
|
||||||
|
scanMsg.className = 'text-xs mt-1 ' + (enabled ? 'text-gray-500' : 'text-amber-700');
|
||||||
|
if (enabled) scanInput.focus();
|
||||||
|
}
|
||||||
|
tbody.addEventListener('click', (e) => {
|
||||||
|
const tr = e.target.closest('tr.os-row');
|
||||||
|
if (tr) selectRow(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 입고 처리(스캔/수량) AJAX ──
|
||||||
|
async function doReceive(barcode, qtySheet) {
|
||||||
|
if (!selectedRow) return;
|
||||||
|
const body = new URLSearchParams();
|
||||||
|
body.set(csrfName, csrfHash);
|
||||||
|
body.set('mode', mode);
|
||||||
|
body.set('row_key', selectedRow.getAttribute('data-row-key'));
|
||||||
|
body.set('br_receiver_ref', document.getElementById('br_receiver_ref').value || '');
|
||||||
|
body.set('br_sender_idx', document.getElementById('br_sender_idx').value || '0');
|
||||||
|
body.set('br_receive_date', document.getElementById('br_receive_date').value || '');
|
||||||
|
body.set('barcode', barcode || '');
|
||||||
|
body.set('qty_sheet', String(qtySheet || 0));
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const res = await fetch(scanApi, {
|
||||||
|
method: 'POST', credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
|
body: body.toString(),
|
||||||
|
});
|
||||||
|
data = await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
scanMsg.textContent = '통신 오류가 발생했습니다.'; scanMsg.className = 'text-xs mt-1 text-red-600';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data && data.csrf) csrfHash = data.csrf;
|
||||||
|
if (!data || !data.ok) {
|
||||||
|
scanMsg.textContent = (data && data.message) || '입고 처리 실패';
|
||||||
|
scanMsg.className = 'text-xs mt-1 text-red-600 font-semibold';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택 행 · 상세 수치 갱신
|
||||||
|
selectedRow.setAttribute('data-received', String(data.received_qty_sheet));
|
||||||
|
selectedRow.setAttribute('data-pending', String(data.pending_qty_sheet));
|
||||||
|
selectedRow.querySelector('.cell-received').textContent = nf(data.received_qty_sheet);
|
||||||
|
selectedRow.querySelector('.cell-pending').textContent = nf(data.pending_qty_sheet);
|
||||||
|
document.getElementById('sel-received').textContent = nf(data.received_qty_sheet);
|
||||||
|
document.getElementById('sel-pending').textContent = nf(data.pending_qty_sheet);
|
||||||
|
|
||||||
|
// 입고 세부내역에 박스코드 추가(최신이 위로)
|
||||||
|
(data.boxes || []).forEach((b) => {
|
||||||
|
detailNo++;
|
||||||
|
if (detailNo === 1) detailBody.innerHTML = '';
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = '<td class="text-center">' + detailNo + '</td>'
|
||||||
|
+ '<td class="text-left pl-2">' + esc(data.bag_name) + '</td>'
|
||||||
|
+ '<td class="text-left pl-2 font-mono text-gray-600">' + esc(b.box_code) + '</td>'
|
||||||
|
+ '<td class="text-right pr-2">' + nf(b.sheet) + '</td>';
|
||||||
|
detailBody.insertBefore(tr, detailBody.firstChild);
|
||||||
|
});
|
||||||
|
detailCountEl.textContent = nf(detailNo);
|
||||||
|
|
||||||
|
scanMsg.textContent = '입고: ' + data.bag_name + ' / ' + nf(data.qty_sheet) + '매' + (data.qty_box ? ' (' + data.qty_box + '박스)' : '') + ' · 잔량 ' + nf(data.pending_qty_sheet);
|
||||||
|
scanMsg.className = 'text-xs mt-1 text-emerald-700';
|
||||||
|
|
||||||
|
// 전량 입고되면 입력 잠금
|
||||||
|
if (Number(data.pending_qty_sheet) <= 0) {
|
||||||
|
scanInput.disabled = true; qtyInput.disabled = true; btnReceive.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scanInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Enter') return;
|
||||||
|
e.preventDefault();
|
||||||
|
const code = (scanInput.value || '').trim();
|
||||||
|
if (!code) return;
|
||||||
|
doReceive(code, 0);
|
||||||
|
scanInput.value = '';
|
||||||
|
scanInput.focus();
|
||||||
|
});
|
||||||
|
btnReceive.addEventListener('click', () => {
|
||||||
|
const q = Math.floor(Number(qtyInput.value || 0));
|
||||||
|
if (!(q > 0)) { scanMsg.textContent = '입고 수량(매)을 입력하세요.'; scanMsg.className = 'text-xs mt-1 text-amber-700'; return; }
|
||||||
|
doReceive('', q);
|
||||||
|
qtyInput.value = '';
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
11
writable/database/rename_batch_receiving_menu.sql
Normal file
11
writable/database/rename_batch_receiving_menu.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- 메뉴명 변경: '일괄입고' → '일괄입고(음식물,폐기물)'
|
||||||
|
-- 링크(mm_link='bag/receiving/batch') 기준, 기본 이름을 쓰는 항목만 변경.
|
||||||
|
-- 실행: mysql --default-character-set=utf8mb4 -u USER -p DBNAME < writable/database/rename_batch_receiving_menu.sql
|
||||||
|
|
||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
SET CHARACTER_SET_CLIENT = utf8mb4;
|
||||||
|
|
||||||
|
UPDATE `menu`
|
||||||
|
SET `mm_name` = '일괄입고(음식물,폐기물)'
|
||||||
|
WHERE `mm_link` = 'bag/receiving/batch'
|
||||||
|
AND `mm_name` = '일괄입고';
|
||||||
Reference in New Issue
Block a user