Compare commits
2 Commits
feature/go
...
clean/push
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8753b1aa68 | ||
|
|
aa50eb72ee |
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
description: 패키지 설치 전 승인·안정성 확인 및 공급망 보안 습관
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# 의존성·패키지 보안
|
|
||||||
|
|
||||||
- **새 패키지(npm, Composer 등)를 설치·추가하기 전에 반드시 사용자에게 먼저 물어본다.** 자동으로 `npm install`, `composer require` 등을 실행하지 않는다(사용자가 명시적으로 요청한 경우만).
|
|
||||||
- 새 버전을 제안할 때는 **공식 레지스트리(npmjs.org, packagist.org) 출처**인지 확인하고, **출시된 지 최소 며칠(가이드: 7일) 이상 지난 안정(stable) 버전**을 우선 제안한다. 방금 출시된 버전은 typosquat·피싱 패키지 위험이 있어 사용자에게 그 점을 짚어 준다.
|
|
||||||
- 락 파일(`package-lock.json`, `composer.lock`)을 대량 수정하거나 생소한 패키지를 넣지 않는다. 이상하면 사용자에게 중단하고 확인을 요청한다.
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -174,5 +174,4 @@ blob-report/
|
|||||||
|
|
||||||
/results/
|
/results/
|
||||||
/phpunit*.xml
|
/phpunit*.xml
|
||||||
# 최상위 개발 문서/스크린샷 폴더만 제외 (app/Docs 등 하위 docs 경로는 추적).
|
docs/
|
||||||
/docs/
|
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ assets/ # 기획 문서 (엑셀)
|
|||||||
php spark serve --port=8045
|
php spark serve --port=8045
|
||||||
```
|
```
|
||||||
|
|
||||||
- 로컬에서 **Apache + 여러 VirtualHost + PHP-FPM**을 쓰면 `localhost`·포트·FPM(예: 9001) 설정에 따라 500/503이 나기 쉽습니다. **일상 개발은 위 내장 서버를 기본**으로 두고, `.env`의 `app.baseURL`을 `http://localhost:8045/` 등과 맞추는 것을 권장합니다.
|
|
||||||
|
|
||||||
## 테스트 (Playwright E2E)
|
## 테스트 (Playwright E2E)
|
||||||
|
|
||||||
모든 작업 완료 후 반드시 Playwright 브라우저 테스트를 수행합니다.
|
모든 작업 완료 후 반드시 Playwright 브라우저 테스트를 수행합니다.
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -190,23 +190,12 @@ assets/ # 기획 문서 (엑셀)
|
|||||||
| `/bag/code-kinds` | 기본코드 종류 | 종류 목록·세부코드 링크 (조회; CRUD는 관리자만) |
|
| `/bag/code-kinds` | 기본코드 종류 | 종류 목록·세부코드 링크 (조회; CRUD는 관리자만) |
|
||||||
| `/bag/code-details/{ck_idx}` | 기본코드 세부 | 해당 종류의 세부코드 목록 (조회; CRUD는 관리자만) |
|
| `/bag/code-details/{ck_idx}` | 기본코드 세부 | 해당 종류의 세부코드 목록 (조회; CRUD는 관리자만) |
|
||||||
| `/bag/purchase-inbound` | 발주 입고 관리 | 발주/입고 목록 + 등록 버튼 |
|
| `/bag/purchase-inbound` | 발주 입고 관리 | 발주/입고 목록 + 등록 버튼 |
|
||||||
| `/bag/order/create` | 발주 등록 | 발주서 신규 작성 |
|
|
||||||
| `/bag/order/change` | 발주 변경 | 발주 변경 목록/수정 진입 |
|
|
||||||
| `/bag/order/revise/{bo_idx}` | 발주 수정 | 선택 발주 수정 화면 |
|
|
||||||
| `/bag/order/lot-seed` | LOT-No 디스켓 불출 | 발주 LOT 기준 seed 생성/다운로드 |
|
|
||||||
| `/bag/issue` | 불출 관리 | 불출 목록 + 처리/취소 |
|
| `/bag/issue` | 불출 관리 | 불출 목록 + 처리/취소 |
|
||||||
| `/bag/inventory` | 재고 관리 | 봉투별 현재 재고 조회 |
|
| `/bag/inventory` | 재고 관리 | 봉투별 현재 재고 조회 |
|
||||||
| `/bag/sales` | 판매 관리 | 주문/판매/반품 + 등록 |
|
| `/bag/sales` | 판매 관리 | 주문/판매/반품 + 등록 |
|
||||||
| `/bag/order/phone` | 전화 주문 접수 | 전화 주문 접수표 작성/저장 |
|
|
||||||
| `/bag/order/phone/manage` | 전화 주문 접수 관리 | 접수 리스트 선택 후 품목 수량 수정/취소 |
|
|
||||||
| `/bag/sale/designated` | 지정판매소 판매 | 주문 선택 + 바코드 스캔 + 판매 저장 |
|
|
||||||
| `/bag/receiving/batch` | 일괄 입고 | 미입고 건 선택 일괄 입고 |
|
|
||||||
| `/bag/sales-stats` | 판매 현황 | 기간별 판매 데이터 |
|
| `/bag/sales-stats` | 판매 현황 | 기간별 판매 데이터 |
|
||||||
| `/bag/flow` | 봉투 수불 관리 | 봉투코드별 입출고 수불 요약 |
|
| `/bag/flow` | 봉투 수불 관리 | 봉투코드별 입출고 수불 요약 |
|
||||||
| `/bag/analytics` | 통계 분석 (→ 전년 대비로 리다이렉트) | |
|
| `/bag/analytics` | 통계 분석 관리 | Phase 6 예정 |
|
||||||
| `/bag/analytics/year-over-year` | 전년 대비 판매 분석 (w_gm604r) | |
|
|
||||||
| `/bag/analytics/monthly-trend` | 월별 판매 추이 분석 (w_gm606r) | |
|
|
||||||
| `/bag/analytics/seasonal-trend` | 계절별 판매 추이 분석 (w_gm607r) | |
|
|
||||||
| `/bag/window` | 창 | Phase 6 예정 |
|
| `/bag/window` | 창 | Phase 6 예정 |
|
||||||
| `/bag/help` | 도움말 | 시스템 안내 |
|
| `/bag/help` | 도움말 | 시스템 안내 |
|
||||||
|
|
||||||
@@ -260,29 +249,6 @@ assets/ # 기획 문서 (엑셀)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 주문/판매 실무 흐름 (현재 구현)
|
|
||||||
|
|
||||||
1. 전화 주문 접수: `/bag/order/phone`
|
|
||||||
2. 전화 주문 관리(수정/취소): `/bag/order/phone/manage`
|
|
||||||
3. 지정판매소 판매 처리(바코드 스캔): `/bag/sale/designated`
|
|
||||||
4. 판매/재고 반영: `bag_sale` 기록 + `bag_inventory` 차감 + 주문 수령상태(`so_received`) 갱신
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 바코드 생성/사용 시점 (현재 코드 기준)
|
|
||||||
|
|
||||||
상세 코드 체계(LOT·팩·낱장·품목코드·판매소번호): [`doc/봉투-LOT-바코드-코드체계.md`](doc/봉투-LOT-바코드-코드체계.md)
|
|
||||||
|
|
||||||
- **현재 코드 구현**
|
|
||||||
- 박스/팩/낱장 바코드는 `bag_receiving_pack_code`에 저장됩니다.
|
|
||||||
- 생성 트리거는 입고 처리(`receiving/scanner/store`, `receiving/batch/store`) 시점의 `createReceivingPackCodes()` 호출입니다.
|
|
||||||
- 판매 단계(`/bag/sale/designated`)에서는 생성된 코드를 스캔하여 `in_stock -> sold` 상태로 전환합니다.
|
|
||||||
- **요구사항 문서 관점**
|
|
||||||
- 노션 요구사항에는 발주 단계에서 바코드 원시데이터 생성 후 제작업체 인쇄 흐름이 명시되어 있습니다.
|
|
||||||
- 현재 구현과 요구사항 간 시점 차이가 존재하므로, 운영 정책 확정 후 발주 단계 생성으로 이관 검토가 필요합니다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 모델 (25개)
|
## 모델 (25개)
|
||||||
|
|
||||||
| 모델 | 테이블 | 용도 |
|
| 모델 | 테이블 | 용도 |
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Config;
|
|
||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 카카오 Developers — 내 애플리케이션 — 앱 키 — JavaScript 키
|
|
||||||
* .env 예: kakao.javascriptKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
*/
|
|
||||||
class Kakao extends BaseConfig
|
|
||||||
{
|
|
||||||
public string $javascriptKey = '';
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
|
|
||||||
$v = env('kakao.javascriptKey');
|
|
||||||
if (is_string($v) && $v !== '') {
|
|
||||||
$this->javascriptKey = $v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Config;
|
|
||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 사용자 매뉴얼(설명서) 목차 정의.
|
|
||||||
*
|
|
||||||
* - 배열 순서가 곧 목차(사이드바) 노출 순서입니다.
|
|
||||||
* - slug 는 URL 세그먼트이자 화이트리스트입니다. 여기에 없는 slug 는 404 입니다.
|
|
||||||
* - file 은 $dir 하위의 실제 마크다운 파일명입니다(사용자 입력으로 조합하지 않음).
|
|
||||||
*/
|
|
||||||
class Manual extends BaseConfig
|
|
||||||
{
|
|
||||||
/** 마크다운 콘텐츠 디렉터리 (웹 루트 밖). */
|
|
||||||
public string $dir = APPPATH . 'Docs/manual/';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<string, array{title: string, file: string}>
|
|
||||||
*/
|
|
||||||
public array $pages = [
|
|
||||||
'overview' => ['title' => '시작하기·시스템 개요', 'file' => '00_overview.md'],
|
|
||||||
'flow' => ['title' => '핵심 업무 흐름', 'file' => '10_workflow.md'],
|
|
||||||
'order' => ['title' => '발주·입고', 'file' => '20_order_receiving.md'],
|
|
||||||
'inventory' => ['title' => '재고·실사', 'file' => '30_inventory.md'],
|
|
||||||
'sales' => ['title' => '판매·불출', 'file' => '40_sales_issue.md'],
|
|
||||||
'reports' => ['title' => '판매현황·수불·통계', 'file' => '50_reports.md'],
|
|
||||||
'codes' => ['title' => '봉투·LOT·바코드 코드체계', 'file' => '90_code_system.md'],
|
|
||||||
'faq' => ['title' => '자주 묻는 질문·문의', 'file' => '99_faq.md'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -7,18 +7,11 @@ use CodeIgniter\Router\RouteCollection;
|
|||||||
*/
|
*/
|
||||||
$routes->get('/', 'Home::index');
|
$routes->get('/', 'Home::index');
|
||||||
$routes->get('dashboard', 'Home::dashboard');
|
$routes->get('dashboard', 'Home::dashboard');
|
||||||
$routes->get('dashboard/simple', 'Home::dashboardSimple');
|
|
||||||
$routes->get('dashboard/compact', 'Home::dashboardCompact');
|
|
||||||
$routes->get('dashboard/classic-mock', 'Home::dashboardClassicMock');
|
$routes->get('dashboard/classic-mock', 'Home::dashboardClassicMock');
|
||||||
$routes->get('dashboard/modern', 'Home::dashboardModern');
|
$routes->get('dashboard/modern', 'Home::dashboardModern');
|
||||||
$routes->get('dashboard/dense', 'Home::dashboardDense');
|
$routes->get('dashboard/dense', 'Home::dashboardDense');
|
||||||
$routes->get('dashboard/charts', 'Home::dashboardCharts');
|
$routes->get('dashboard/charts', 'Home::dashboardCharts');
|
||||||
$routes->get('dashboard/blend', 'Home::dashboardBlend');
|
$routes->get('dashboard/blend', 'Home::dashboardBlend');
|
||||||
$routes->get('dashboard/lite', 'Home::dashboardLite');
|
|
||||||
$routes->get('dashboard/gov-portal', 'Home::dashboardGovPortal');
|
|
||||||
$routes->get('dashboard/gov-portal/code-kinds', 'Home::dashboardGovPortalCodeKinds');
|
|
||||||
$routes->get('dashboard/gov-portal-strip', 'Home::dashboardGovPortalStrip');
|
|
||||||
$routes->get('dashboard/gov-portal-strip/code-kinds', 'Home::dashboardGovPortalStripCodeKinds');
|
|
||||||
$routes->get('bag/inventory-inquiry', 'Home::inventoryInquiry');
|
$routes->get('bag/inventory-inquiry', 'Home::inventoryInquiry');
|
||||||
$routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
|
$routes->get('bag/waste-suibal-enterprise', 'Home::wasteSuibalEnterprise');
|
||||||
|
|
||||||
@@ -33,78 +26,28 @@ $routes->get('bag/code-details/(:num)', 'Bag::codeDetails/$1');
|
|||||||
// 옛 주소 호환: 세부 목록만 사이트로 이동
|
// 옛 주소 호환: 세부 목록만 사이트로 이동
|
||||||
$routes->get('admin/code-details/(:num)', 'Admin\CodeDetail::index/$1');
|
$routes->get('admin/code-details/(:num)', 'Admin\CodeDetail::index/$1');
|
||||||
$routes->get('bag/purchase-inbound', 'Bag::purchaseInbound');
|
$routes->get('bag/purchase-inbound', 'Bag::purchaseInbound');
|
||||||
$routes->get('bag/issue', 'Bag::issueLegacy');
|
$routes->get('bag/issue', 'Bag::issue');
|
||||||
$routes->get('bag/issue/cancel', 'Bag::issue');
|
|
||||||
$routes->get('bag/inventory', 'Bag::inventory');
|
$routes->get('bag/inventory', 'Bag::inventory');
|
||||||
$routes->get('bag/inventory/export', 'Bag::inventoryExport');
|
|
||||||
$routes->get('bag/inventory/inspection-select', 'Bag::inspectionSelect');
|
|
||||||
$routes->get('bag/inventory/inspection-work', 'Bag::inspectionWork');
|
|
||||||
$routes->post('bag/inventory/inspection-run', 'Bag::inspectionRun');
|
|
||||||
$routes->post('bag/inventory/inspection-select/save', 'Bag::inspectionSelectSave');
|
|
||||||
$routes->post('bag/inventory/inspection-select/confirm', 'Bag::inspectionSelectConfirm');
|
|
||||||
$routes->get('bag/inventory/inspection/(:num)', 'Bag::inspectionDetail/$1');
|
|
||||||
$routes->post('bag/inventory/inspection/(:num)/save', 'Bag::inspectionSave/$1');
|
|
||||||
$routes->post('bag/inventory/inspection/(:num)/apply', 'Bag::inspectionApply/$1');
|
|
||||||
$routes->get('bag/sales', 'Bag::sales');
|
$routes->get('bag/sales', 'Bag::sales');
|
||||||
$routes->get('bag/sales-stats', 'Bag::salesStats');
|
$routes->get('bag/sales-stats', 'Bag::salesStats');
|
||||||
$routes->get('bag/flow', 'Bag::flow');
|
$routes->get('bag/flow', 'Bag::flow');
|
||||||
$routes->get('bag/flow/export', 'Bag::flowExport');
|
|
||||||
$routes->get('bag/analytics', 'Bag::analytics');
|
$routes->get('bag/analytics', 'Bag::analytics');
|
||||||
$routes->get('bag/analytics/year-over-year', 'Bag::analyticsYearOverYear');
|
|
||||||
$routes->get('bag/analytics/monthly-trend', 'Bag::analyticsMonthlyTrend');
|
|
||||||
$routes->get('bag/analytics/seasonal-trend', 'Bag::analyticsSeasonalTrend');
|
|
||||||
$routes->get('bag/window', 'Bag::window');
|
$routes->get('bag/window', 'Bag::window');
|
||||||
$routes->get('bag/help', 'Bag::help');
|
$routes->get('bag/help', 'Bag::help');
|
||||||
|
|
||||||
// 사용자 매뉴얼(설명서) — 로그인 사용자 전용
|
|
||||||
$routes->group('bag', ['filter' => 'loginAuth'], static function ($routes): void {
|
|
||||||
$routes->get('manual', 'Bag::manual');
|
|
||||||
$routes->get('manual/(:segment)', 'Bag::manualPage/$1');
|
|
||||||
});
|
|
||||||
|
|
||||||
$routes->get('bag/number-lookup', 'Bag::numberLookup');
|
|
||||||
$routes->post('bag/number-lookup/resolve', 'Bag::numberLookupResolve');
|
|
||||||
|
|
||||||
// 사이트 메뉴 CRUD (사이트 레이아웃)
|
// 사이트 메뉴 CRUD (사이트 레이아웃)
|
||||||
|
$routes->get('bag/inventory/adjust', 'Bag::inventoryAdjust');
|
||||||
|
$routes->post('bag/inventory/adjust', 'Bag::inventoryAdjustStore');
|
||||||
$routes->get('bag/issue/create', 'Bag::issueCreate');
|
$routes->get('bag/issue/create', 'Bag::issueCreate');
|
||||||
$routes->post('bag/issue/store', 'Bag::issueStore');
|
$routes->post('bag/issue/store', 'Bag::issueStore');
|
||||||
$routes->post('bag/issue/cancel/(:num)', 'Bag::issueCancel/$1');
|
$routes->post('bag/issue/cancel/(:num)', 'Bag::issueCancel/$1');
|
||||||
$routes->post('bag/issue/cancel-save', 'Bag::issueCancelSave');
|
|
||||||
$routes->get('bag/order/create', 'Bag::orderCreate');
|
$routes->get('bag/order/create', 'Bag::orderCreate');
|
||||||
$routes->get('bag/order/phone', 'Bag::phoneOrderCreate');
|
|
||||||
$routes->get('bag/order/phone/manage', 'Bag::phoneOrderManage');
|
|
||||||
$routes->post('bag/order/phone/manage/update', 'Bag::phoneOrderUpdate');
|
|
||||||
$routes->post('bag/order/phone/manage/cancel/(:num)', 'Bag::phoneOrderCancel/$1');
|
|
||||||
$routes->get('bag/order/change', 'Bag::orderChange');
|
|
||||||
$routes->get('bag/order/revise/(:num)', 'Bag::orderRevise/$1');
|
|
||||||
$routes->get('bag/order/lot-seed', 'Bag::orderLotSeed');
|
|
||||||
$routes->post('bag/order/lot-seed/generate', 'Bag::orderLotSeedGenerate');
|
|
||||||
$routes->post('bag/order/store', 'Bag::orderStore');
|
$routes->post('bag/order/store', 'Bag::orderStore');
|
||||||
$routes->post('bag/order/cancel/(:num)', 'Bag::orderCancel/$1');
|
$routes->post('bag/order/cancel/(:num)', 'Bag::orderCancel/$1');
|
||||||
$routes->post('bag/order/delete', 'Bag::orderDeletePost');
|
|
||||||
$routes->post('bag/order/delete/(:num)', 'Bag::orderDelete/$1');
|
|
||||||
$routes->get('bag/receiving/create', 'Bag::receivingCreate');
|
$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->post('bag/receiving/scanner/store', 'Bag::receivingScannerStore');
|
|
||||||
$routes->get('bag/receiving/batch', 'Bag::receivingBatch');
|
|
||||||
$routes->post('bag/receiving/batch/store', 'Bag::receivingBatchStore');
|
|
||||||
$routes->get('bag/receiving/status', 'Bag::receivingStatus');
|
|
||||||
$routes->get('bag/receiving/status/export', 'Bag::receivingStatusExport');
|
|
||||||
$routes->get('bag/sale/create', 'Bag::saleCreate');
|
$routes->get('bag/sale/create', 'Bag::saleCreate');
|
||||||
$routes->post('bag/sale/store', 'Bag::saleStore');
|
$routes->post('bag/sale/store', 'Bag::saleStore');
|
||||||
$routes->get('bag/sale/designated', 'Bag::designatedShopSaleCreate');
|
|
||||||
$routes->get('bag/sale/designated/dev-saleable-barcodes', 'Bag::designatedShopDevSaleableBarcodes');
|
|
||||||
$routes->get('bag/sale/dev-all-sales-history', 'Bag::devAllSalesHistory');
|
|
||||||
$routes->post('bag/sale/designated/scan', 'Bag::designatedShopSaleScan');
|
|
||||||
$routes->post('bag/sale/designated/save', 'Bag::designatedShopSaleSave');
|
|
||||||
$routes->get('bag/sale/designated-return', 'Bag::designatedShopSaleReturnCreate');
|
|
||||||
$routes->post('bag/sale/designated-return/scan', 'Bag::designatedShopSaleReturnScan');
|
|
||||||
$routes->post('bag/sale/designated-return/save', 'Bag::designatedShopSaleReturnSave');
|
|
||||||
$routes->get('bag/sale/designated-return-cancel', 'Bag::designatedShopSaleReturnCancelCreate');
|
|
||||||
$routes->post('bag/sale/designated-return-cancel/save', 'Bag::designatedShopSaleReturnCancelSave');
|
|
||||||
$routes->get('bag/sale/designated-cancel', 'Bag::designatedShopReturnCreate');
|
|
||||||
$routes->post('bag/sale/designated-cancel/submit', 'Bag::designatedShopReturnCancel');
|
|
||||||
$routes->get('bag/shop-order/create', 'Bag::shopOrderCreate');
|
$routes->get('bag/shop-order/create', 'Bag::shopOrderCreate');
|
||||||
$routes->post('bag/shop-order/store', 'Bag::shopOrderStore');
|
$routes->post('bag/shop-order/store', 'Bag::shopOrderStore');
|
||||||
|
|
||||||
@@ -140,13 +83,7 @@ $routes->group('bag', ['filter' => 'adminAuth'], static function ($routes): void
|
|||||||
|
|
||||||
$routes->get('designated-shops/export', 'Admin\DesignatedShop::export');
|
$routes->get('designated-shops/export', 'Admin\DesignatedShop::export');
|
||||||
$routes->get('designated-shops/map', 'Admin\DesignatedShop::map');
|
$routes->get('designated-shops/map', 'Admin\DesignatedShop::map');
|
||||||
$routes->get('designated-shops/status/export', 'Admin\DesignatedShop::statusExport');
|
|
||||||
$routes->get('designated-shops/status', 'Admin\DesignatedShop::status');
|
$routes->get('designated-shops/status', 'Admin\DesignatedShop::status');
|
||||||
$routes->get('designated-shops/barcode', 'Admin\DesignatedShop::barcode');
|
|
||||||
$routes->post('designated-shops/barcode/print', 'Admin\DesignatedShop::barcodePrint');
|
|
||||||
$routes->get('designated-shops/district-new-cancel/export', 'Admin\DesignatedShop::districtNewCancelExport');
|
|
||||||
$routes->get('designated-shops/district-new-cancel', 'Admin\DesignatedShop::districtNewCancel');
|
|
||||||
$routes->get('designated-shops/browse', 'Admin\DesignatedShop::browse');
|
|
||||||
$routes->get('designated-shops', 'Admin\DesignatedShop::index');
|
$routes->get('designated-shops', 'Admin\DesignatedShop::index');
|
||||||
$routes->get('designated-shops/create', 'Admin\DesignatedShop::create');
|
$routes->get('designated-shops/create', 'Admin\DesignatedShop::create');
|
||||||
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
|
$routes->post('designated-shops/store', 'Admin\DesignatedShop::store');
|
||||||
@@ -165,7 +102,6 @@ $routes->group('bag', ['filter' => 'adminAuth'], static function ($routes): void
|
|||||||
$routes->get('bag-orders/export', 'Admin\BagOrder::export');
|
$routes->get('bag-orders/export', 'Admin\BagOrder::export');
|
||||||
$routes->get('bag-orders', 'Admin\BagOrder::index');
|
$routes->get('bag-orders', 'Admin\BagOrder::index');
|
||||||
$routes->get('bag-orders/create', 'Admin\BagOrder::create');
|
$routes->get('bag-orders/create', 'Admin\BagOrder::create');
|
||||||
$routes->get('bag-orders/revise/(:num)', 'Admin\BagOrder::revise/$1');
|
|
||||||
$routes->post('bag-orders/store', 'Admin\BagOrder::store');
|
$routes->post('bag-orders/store', 'Admin\BagOrder::store');
|
||||||
$routes->get('bag-orders/detail/(:num)', 'Admin\BagOrder::detail/$1');
|
$routes->get('bag-orders/detail/(:num)', 'Admin\BagOrder::detail/$1');
|
||||||
$routes->post('bag-orders/cancel/(:num)', 'Admin\BagOrder::cancel/$1');
|
$routes->post('bag-orders/cancel/(:num)', 'Admin\BagOrder::cancel/$1');
|
||||||
@@ -209,11 +145,9 @@ $routes->group('bag', ['filter' => 'adminAuth'], static function ($routes): void
|
|||||||
$routes->get('reports/shop-sales', 'Admin\SalesReport::shopSales');
|
$routes->get('reports/shop-sales', 'Admin\SalesReport::shopSales');
|
||||||
$routes->get('reports/hometax-export', 'Admin\SalesReport::hometaxExport');
|
$routes->get('reports/hometax-export', 'Admin\SalesReport::hometaxExport');
|
||||||
$routes->get('reports/returns', 'Admin\SalesReport::returns');
|
$routes->get('reports/returns', 'Admin\SalesReport::returns');
|
||||||
$routes->get('reports/returns/export', 'Admin\SalesReport::returnsExport');
|
|
||||||
$routes->get('reports/lot-flow', 'Admin\SalesReport::lotFlow');
|
$routes->get('reports/lot-flow', 'Admin\SalesReport::lotFlow');
|
||||||
$routes->get('reports/misc-flow', 'Admin\SalesReport::miscFlow');
|
$routes->get('reports/misc-flow', 'Admin\SalesReport::miscFlow');
|
||||||
$routes->post('reports/misc-flow', 'Admin\SalesReport::miscFlowStore');
|
$routes->post('reports/misc-flow', 'Admin\SalesReport::miscFlowStore');
|
||||||
$routes->post('reports/misc-flow/delete', 'Admin\SalesReport::miscFlowDelete');
|
|
||||||
|
|
||||||
$routes->get('password-change', 'Admin\PasswordChange::index');
|
$routes->get('password-change', 'Admin\PasswordChange::index');
|
||||||
$routes->post('password-change', 'Admin\PasswordChange::update');
|
$routes->post('password-change', 'Admin\PasswordChange::update');
|
||||||
|
|||||||
@@ -43,9 +43,8 @@ class BagInventory extends BaseController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export_xlsx(
|
export_csv(
|
||||||
'재고현황_' . date('Ymd') . '.xlsx',
|
'재고현황_' . date('Ymd') . '.csv',
|
||||||
'재고현황',
|
|
||||||
['번호', '봉투코드', '봉투명', '현재재고(낱장)', '최종갱신'],
|
['번호', '봉투코드', '봉투명', '현재재고(낱장)', '최종갱신'],
|
||||||
$rows
|
$rows
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,56 +4,17 @@ namespace App\Controllers\Admin;
|
|||||||
|
|
||||||
use App\Controllers\BaseController;
|
use App\Controllers\BaseController;
|
||||||
use App\Models\BagIssueModel;
|
use App\Models\BagIssueModel;
|
||||||
use App\Models\BagIssueItemCodeModel;
|
|
||||||
use App\Models\BagInventoryModel;
|
use App\Models\BagInventoryModel;
|
||||||
use App\Models\CodeKindModel;
|
use App\Models\CodeKindModel;
|
||||||
use App\Models\CodeDetailModel;
|
use App\Models\CodeDetailModel;
|
||||||
use App\Models\FreeRecipientModel;
|
|
||||||
use App\Models\PackagingUnitModel;
|
|
||||||
|
|
||||||
class BagIssue extends BaseController
|
class BagIssue extends BaseController
|
||||||
{
|
{
|
||||||
private BagIssueModel $issueModel;
|
private BagIssueModel $issueModel;
|
||||||
private BagIssueItemCodeModel $issueItemCodeModel;
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->issueModel = model(BagIssueModel::class);
|
$this->issueModel = model(BagIssueModel::class);
|
||||||
$this->issueItemCodeModel = model(BagIssueItemCodeModel::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 낱장 수량을 품목코드 단위로 분해한다.
|
|
||||||
*
|
|
||||||
* @return array<int,array{issueCode:string,qty:int}>
|
|
||||||
*/
|
|
||||||
private function buildIssueCodeRows(int $bi2Idx, int $sheetQty, array $packUnit): array
|
|
||||||
{
|
|
||||||
$sheetQty = max(0, $sheetQty);
|
|
||||||
if ($sheetQty <= 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$chunkSize = max(
|
|
||||||
1,
|
|
||||||
(int) ($packUnit['totalPerBox'] ?? 0),
|
|
||||||
(int) ($packUnit['packPerSheet'] ?? 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
$remaining = $sheetQty;
|
|
||||||
$seq = 1;
|
|
||||||
while ($remaining > 0) {
|
|
||||||
$qty = min($chunkSize, $remaining);
|
|
||||||
$rows[] = [
|
|
||||||
'issueCode' => sprintf('%d-%06d-%03d', (int) date('y'), $bi2Idx, $seq),
|
|
||||||
'qty' => $qty,
|
|
||||||
];
|
|
||||||
$remaining -= $qty;
|
|
||||||
$seq++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
@@ -101,219 +62,48 @@ class BagIssue extends BaseController
|
|||||||
'bi2_issue_type' => 'required|max_length[20]',
|
'bi2_issue_type' => 'required|max_length[20]',
|
||||||
'bi2_issue_date' => 'required|valid_date[Y-m-d]',
|
'bi2_issue_date' => 'required|valid_date[Y-m-d]',
|
||||||
'bi2_dest_name' => 'required|max_length[100]',
|
'bi2_dest_name' => 'required|max_length[100]',
|
||||||
// 사이트 다건 입력(item_bag_code/item_qty)과 기존 관리자 단건 입력을 함께 허용
|
'bi2_bag_code' => 'required|max_length[50]',
|
||||||
'bi2_bag_code' => 'permit_empty|max_length[50]',
|
'bi2_qty' => 'required|is_natural_no_zero',
|
||||||
'bi2_qty' => 'permit_empty|is_natural_no_zero',
|
|
||||||
];
|
];
|
||||||
if (! $this->validate($rules)) {
|
if (! $this->validate($rules)) {
|
||||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$bagCode = $this->request->getPost('bi2_bag_code');
|
||||||
|
$qty = (int) $this->request->getPost('bi2_qty');
|
||||||
|
|
||||||
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
$issueType = trim((string) $this->request->getPost('bi2_issue_type'));
|
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
|
||||||
$destType = trim((string) ($this->request->getPost('bi2_dest_type') ?? ''));
|
$bagName = $detail ? $detail->cd_name : '';
|
||||||
$destName = trim((string) ($this->request->getPost('bi2_dest_name') ?? ''));
|
|
||||||
$destDongCode = trim((string) ($this->request->getPost('bi2_dest_dong_code') ?? ''));
|
|
||||||
|
|
||||||
if ($destType === '') {
|
|
||||||
$destType = '동사무소';
|
|
||||||
}
|
|
||||||
if ($issueType === '공공용' && $destType === '동사무소') {
|
|
||||||
$destType = '구청';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($issueType === '무료용' && $destDongCode !== '') {
|
|
||||||
$existsFreeDong = model(FreeRecipientModel::class)
|
|
||||||
->where('fr_lg_idx', $lgIdx)
|
|
||||||
->where('fr_state', 1)
|
|
||||||
->where('fr_dong_code', $destDongCode)
|
|
||||||
->first();
|
|
||||||
if (! $existsFreeDong) {
|
|
||||||
return redirect()->back()->withInput()->with('error', '선택한 불출처는 무료용 대상이 아닙니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$invRows = model(BagInventoryModel::class)
|
|
||||||
->where('bi_lg_idx', $lgIdx)
|
|
||||||
->where('bi_qty >', 0)
|
|
||||||
->findAll();
|
|
||||||
$inventoryMap = [];
|
|
||||||
foreach ($invRows as $inv) {
|
|
||||||
$code = (string) ($inv->bi_bag_code ?? '');
|
|
||||||
if ($code === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$inventoryMap[$code] = [
|
|
||||||
'qty' => (int) ($inv->bi_qty ?? 0),
|
|
||||||
'name' => (string) ($inv->bi_bag_name ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$unitRows = model(PackagingUnitModel::class)
|
|
||||||
->where('pu_lg_idx', $lgIdx)
|
|
||||||
->where('pu_state', 1)
|
|
||||||
->findAll();
|
|
||||||
$packMap = [];
|
|
||||||
foreach ($unitRows as $unit) {
|
|
||||||
$code = (string) ($unit->pu_bag_code ?? '');
|
|
||||||
if ($code === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$packMap[$code] = [
|
|
||||||
'packPerSheet' => max(1, (int) ($unit->pu_pack_per_sheet ?? 1)),
|
|
||||||
'totalPerBox' => max(1, (int) ($unit->pu_total_per_box ?? 1)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
$itemCodes = $this->request->getPost('item_bag_code');
|
|
||||||
$itemQtys = $this->request->getPost('item_qty');
|
|
||||||
$itemPacks = $this->request->getPost('item_pack');
|
|
||||||
$itemCodes = is_array($itemCodes) ? $itemCodes : [];
|
|
||||||
$itemQtys = is_array($itemQtys) ? $itemQtys : [];
|
|
||||||
$itemPacks = is_array($itemPacks) ? $itemPacks : [];
|
|
||||||
|
|
||||||
$count = max(count($itemCodes), count($itemQtys), count($itemPacks));
|
|
||||||
for ($i = 0; $i < $count; $i++) {
|
|
||||||
$bagCode = trim((string) ($itemCodes[$i] ?? ''));
|
|
||||||
$qtyRaw = (int) ($itemQtys[$i] ?? 0);
|
|
||||||
$pack = trim((string) ($itemPacks[$i] ?? 'sheet'));
|
|
||||||
if ($bagCode === '' || $qtyRaw <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (! in_array($pack, ['box', 'pack', 'sheet'], true)) {
|
|
||||||
$pack = 'sheet';
|
|
||||||
}
|
|
||||||
$packUnit = $packMap[$bagCode] ?? ['packPerSheet' => 1, 'totalPerBox' => 1];
|
|
||||||
$sheetQty = $qtyRaw;
|
|
||||||
if ($pack === 'box') {
|
|
||||||
$sheetQty = $qtyRaw * (int) $packUnit['totalPerBox'];
|
|
||||||
} elseif ($pack === 'pack') {
|
|
||||||
$sheetQty = $qtyRaw * (int) $packUnit['packPerSheet'];
|
|
||||||
}
|
|
||||||
$sheetQty = max(1, (int) $sheetQty);
|
|
||||||
|
|
||||||
$detail = $kindO
|
|
||||||
? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, $bagCode, $lgIdx)
|
|
||||||
: null;
|
|
||||||
$bagName = $detail ? (string) ($detail->cd_name ?? '') : (string) ($inventoryMap[$bagCode]['name'] ?? '');
|
|
||||||
if ($bagName === '') {
|
|
||||||
$bagName = (string) $bagCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
$items[] = [
|
|
||||||
'bagCode' => $bagCode,
|
|
||||||
'bagName' => $bagName,
|
|
||||||
'pack' => $pack,
|
|
||||||
'rawQty' => $qtyRaw,
|
|
||||||
'sheetQty' => $sheetQty,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기존 관리자 단건 폼과의 호환
|
|
||||||
if ($items === []) {
|
|
||||||
$singleBagCode = trim((string) $this->request->getPost('bi2_bag_code'));
|
|
||||||
$singleQty = (int) $this->request->getPost('bi2_qty');
|
|
||||||
if ($singleBagCode !== '' && $singleQty > 0) {
|
|
||||||
$detail = $kindO
|
|
||||||
? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, $singleBagCode, $lgIdx)
|
|
||||||
: null;
|
|
||||||
$bagName = $detail ? (string) ($detail->cd_name ?? '') : (string) ($inventoryMap[$singleBagCode]['name'] ?? '');
|
|
||||||
if ($bagName === '') {
|
|
||||||
$bagName = (string) $singleBagCode;
|
|
||||||
}
|
|
||||||
$items[] = [
|
|
||||||
'bagCode' => $singleBagCode,
|
|
||||||
'bagName' => $bagName,
|
|
||||||
'pack' => 'sheet',
|
|
||||||
'rawQty' => $singleQty,
|
|
||||||
'sheetQty' => $singleQty,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($items === []) {
|
|
||||||
return redirect()->back()->withInput()->with('error', '불출 품목을 1건 이상 입력해 주세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$requiredByBag = [];
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$code = (string) $item['bagCode'];
|
|
||||||
if (! isset($requiredByBag[$code])) {
|
|
||||||
$requiredByBag[$code] = 0;
|
|
||||||
}
|
|
||||||
$requiredByBag[$code] += (int) $item['sheetQty'];
|
|
||||||
}
|
|
||||||
foreach ($requiredByBag as $code => $requiredQty) {
|
|
||||||
$available = (int) ($inventoryMap[$code]['qty'] ?? 0);
|
|
||||||
if ($available <= 0) {
|
|
||||||
return redirect()->back()->withInput()->with('error', '입고 재고가 없는 봉투코드는 불출할 수 없습니다: ' . $code);
|
|
||||||
}
|
|
||||||
if ($available < $requiredQty) {
|
|
||||||
return redirect()->back()->withInput()->with('error', '재고가 부족합니다: ' . $code . ' (재고 ' . number_format($available) . ', 요청 ' . number_format($requiredQty) . ')');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$db->transStart();
|
$db->transStart();
|
||||||
$hasIssueCodeTable = $db->tableExists('bag_issue_item_code');
|
|
||||||
|
|
||||||
$issueYear = (int) $this->request->getPost('bi2_year');
|
|
||||||
$issueQuarter = (int) $this->request->getPost('bi2_quarter');
|
|
||||||
$issueDate = (string) $this->request->getPost('bi2_issue_date');
|
|
||||||
$createdCount = 0;
|
|
||||||
helper('audit');
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$issueData = [
|
$issueData = [
|
||||||
'bi2_lg_idx' => $lgIdx,
|
'bi2_lg_idx' => $lgIdx,
|
||||||
'bi2_year' => $issueYear,
|
'bi2_year' => (int) $this->request->getPost('bi2_year'),
|
||||||
'bi2_quarter' => $issueQuarter,
|
'bi2_quarter' => (int) $this->request->getPost('bi2_quarter'),
|
||||||
'bi2_issue_type' => $issueType,
|
'bi2_issue_type' => $this->request->getPost('bi2_issue_type'),
|
||||||
'bi2_issue_date' => $issueDate,
|
'bi2_issue_date' => $this->request->getPost('bi2_issue_date'),
|
||||||
'bi2_dest_type' => $destType,
|
'bi2_dest_type' => $this->request->getPost('bi2_dest_type') ?? '',
|
||||||
'bi2_dest_name' => $destName,
|
'bi2_dest_name' => $this->request->getPost('bi2_dest_name'),
|
||||||
'bi2_bag_code' => (string) $item['bagCode'],
|
'bi2_bag_code' => $bagCode,
|
||||||
'bi2_bag_name' => (string) $item['bagName'],
|
'bi2_bag_name' => $bagName,
|
||||||
'bi2_qty' => (int) $item['sheetQty'],
|
'bi2_qty' => $qty,
|
||||||
'bi2_status' => 'normal',
|
'bi2_status' => 'normal',
|
||||||
'bi2_regdate' => date('Y-m-d H:i:s'),
|
'bi2_regdate' => date('Y-m-d H:i:s'),
|
||||||
];
|
];
|
||||||
$this->issueModel->insert($issueData);
|
$this->issueModel->insert($issueData);
|
||||||
$bi2Idx = (int) $this->issueModel->getInsertID();
|
$bi2Idx = (int) $this->issueModel->getInsertID();
|
||||||
|
|
||||||
|
helper('audit');
|
||||||
audit_log('create', 'bag_issue', $bi2Idx, null, array_merge($issueData, ['bi2_idx' => $bi2Idx]));
|
audit_log('create', 'bag_issue', $bi2Idx, null, array_merge($issueData, ['bi2_idx' => $bi2Idx]));
|
||||||
|
|
||||||
if ($hasIssueCodeTable) {
|
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, -$qty);
|
||||||
$codeRows = $this->buildIssueCodeRows($bi2Idx, (int) $item['sheetQty'], $packMap[(string) $item['bagCode']] ?? []);
|
|
||||||
foreach ($codeRows as $codeRow) {
|
|
||||||
$this->issueItemCodeModel->insert([
|
|
||||||
'bic_lg_idx' => $lgIdx,
|
|
||||||
'bic_bi2_idx' => $bi2Idx,
|
|
||||||
'bic_bag_code' => (string) $item['bagCode'],
|
|
||||||
'bic_issue_code' => (string) $codeRow['issueCode'],
|
|
||||||
'bic_qty' => (int) $codeRow['qty'],
|
|
||||||
'bic_cancel_qty' => 0,
|
|
||||||
'bic_state' => 'normal',
|
|
||||||
'bic_regdate' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
model(BagInventoryModel::class)->adjustQty(
|
|
||||||
$lgIdx,
|
|
||||||
(string) $item['bagCode'],
|
|
||||||
(string) $item['bagName'],
|
|
||||||
-((int) $item['sheetQty'])
|
|
||||||
);
|
|
||||||
$createdCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$db->transComplete();
|
$db->transComplete();
|
||||||
|
|
||||||
if (! $db->transStatus()) {
|
return redirect()->to(mgmt_url('bag-issues'))->with('success', '불출 처리되었습니다.');
|
||||||
return redirect()->back()->withInput()->with('error', '불출 처리 중 오류가 발생했습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->to(mgmt_url('bag-issues'))->with('success', $createdCount . '건 불출 처리되었습니다.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cancel(int $id)
|
public function cancel(int $id)
|
||||||
@@ -326,38 +116,12 @@ class BagIssue extends BaseController
|
|||||||
|
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$db->transStart();
|
$db->transStart();
|
||||||
$hasIssueCodeTable = $db->tableExists('bag_issue_item_code');
|
|
||||||
|
|
||||||
$before = (array) $item;
|
$before = (array) $item;
|
||||||
$this->issueModel->update($id, ['bi2_status' => 'cancelled']);
|
$this->issueModel->update($id, ['bi2_status' => 'cancelled']);
|
||||||
helper('audit');
|
helper('audit');
|
||||||
audit_log('update', 'bag_issue', $id, $before, ['bi2_status' => 'cancelled']);
|
audit_log('update', 'bag_issue', $id, $before, ['bi2_status' => 'cancelled']);
|
||||||
|
model(BagInventoryModel::class)->adjustQty((int) $item->bi2_lg_idx, $item->bi2_bag_code, $item->bi2_bag_name, (int) $item->bi2_qty);
|
||||||
$restoreQty = (int) $item->bi2_qty;
|
|
||||||
if ($hasIssueCodeTable) {
|
|
||||||
$codeRows = $db->table('bag_issue_item_code')
|
|
||||||
->select('bic_idx, bic_qty, bic_cancel_qty')
|
|
||||||
->where('bic_lg_idx', (int) $item->bi2_lg_idx)
|
|
||||||
->where('bic_bi2_idx', $id)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
$restoreQty = 0;
|
|
||||||
foreach ($codeRows as $codeRow) {
|
|
||||||
$bicIdx = (int) ($codeRow['bic_idx'] ?? 0);
|
|
||||||
$qty = (int) ($codeRow['bic_qty'] ?? 0);
|
|
||||||
$oldCancel = (int) ($codeRow['bic_cancel_qty'] ?? 0);
|
|
||||||
$restoreQty += max(0, $qty - $oldCancel);
|
|
||||||
$db->table('bag_issue_item_code')
|
|
||||||
->where('bic_idx', $bicIdx)
|
|
||||||
->update([
|
|
||||||
'bic_cancel_qty' => $qty,
|
|
||||||
'bic_state' => 'cancelled',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
model(BagInventoryModel::class)->adjustQty((int) $item->bi2_lg_idx, $item->bi2_bag_code, $item->bi2_bag_name, $restoreQty);
|
|
||||||
$this->issueModel->update($id, ['bi2_qty' => 0, 'bi2_status' => 'cancelled']);
|
|
||||||
|
|
||||||
$db->transComplete();
|
$db->transComplete();
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,8 @@ use App\Models\BagPriceModel;
|
|||||||
use App\Models\PackagingUnitModel;
|
use App\Models\PackagingUnitModel;
|
||||||
use App\Models\CompanyModel;
|
use App\Models\CompanyModel;
|
||||||
use App\Models\SalesAgencyModel;
|
use App\Models\SalesAgencyModel;
|
||||||
use App\Models\BagReceivingModel;
|
|
||||||
use App\Models\CodeKindModel;
|
use App\Models\CodeKindModel;
|
||||||
use App\Models\CodeDetailModel;
|
use App\Models\CodeDetailModel;
|
||||||
use App\Models\LocalGovernmentModel;
|
|
||||||
use App\Libraries\Blockchain\SqlLedger;
|
|
||||||
class BagOrder extends BaseController
|
class BagOrder extends BaseController
|
||||||
{
|
{
|
||||||
private BagOrderModel $orderModel;
|
private BagOrderModel $orderModel;
|
||||||
@@ -33,76 +30,36 @@ class BagOrder extends BaseController
|
|||||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$startMonth = (string) ($this->request->getGet('start_month') ?? date('Y-m'));
|
$builder = $this->orderModel->where('bo_lg_idx', $lgIdx);
|
||||||
$endMonth = (string) ($this->request->getGet('end_month') ?? date('Y-m'));
|
|
||||||
if (! preg_match('/^\d{4}-\d{2}$/', $startMonth)) {
|
// 기간 필터
|
||||||
$startMonth = date('Y-m');
|
$startDate = $this->request->getGet('start_date');
|
||||||
}
|
$endDate = $this->request->getGet('end_date');
|
||||||
if (! preg_match('/^\d{4}-\d{2}$/', $endMonth)) {
|
$status = $this->request->getGet('status');
|
||||||
$endMonth = $startMonth;
|
if ($startDate) $builder->where('bo_order_date >=', $startDate);
|
||||||
}
|
if ($endDate) $builder->where('bo_order_date <=', $endDate);
|
||||||
if (strtotime($startMonth . '-01') > strtotime($endMonth . '-01')) {
|
if ($status) $builder->where('bo_status', $status);
|
||||||
[$startMonth, $endMonth] = [$endMonth, $startMonth];
|
|
||||||
|
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->paginate(20);
|
||||||
|
$pager = $this->orderModel->pager;
|
||||||
|
|
||||||
|
// 발주별 품목 합계
|
||||||
|
$itemSummary = [];
|
||||||
|
foreach ($list as $order) {
|
||||||
|
$items = $this->itemModel->where('boi_bo_idx', $order->bo_idx)->findAll();
|
||||||
|
$totalQty = 0; $totalAmt = 0;
|
||||||
|
foreach ($items as $it) { $totalQty += (int) $it->boi_qty_sheet; $totalAmt += (float) $it->boi_amount; }
|
||||||
|
$itemSummary[$order->bo_idx] = ['qty' => $totalQty, 'amount' => $totalAmt, 'count' => count($items)];
|
||||||
}
|
}
|
||||||
|
|
||||||
$companyIdx = (int) ($this->request->getGet('company_idx') ?? 0);
|
// 제작업체/대행소 이름 매핑
|
||||||
$bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
|
$companyMap = []; $agencyMap = [];
|
||||||
$receiveType = (string) ($this->request->getGet('receive_type') ?? 'all');
|
foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->findAll() as $c) $companyMap[$c->cp_idx] = $c->cp_name;
|
||||||
if (! in_array($receiveType, ['all', 'received', 'pending'], true)) {
|
foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $a) {
|
||||||
$receiveType = 'all';
|
$agencyMap[$a->sa_idx] = '[' . ($a->sa_kind ?? '') . '] ' . ($a->sa_code ?? '') . ' — ' . ($a->sa_name ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
$companies = model(CompanyModel::class)
|
return $this->renderWorkPage('발주 현황', 'admin/bag_order/index', compact('list', 'itemSummary', 'companyMap', 'agencyMap', 'startDate', 'endDate', 'status', 'pager'));
|
||||||
->where('cp_lg_idx', $lgIdx)
|
|
||||||
->where('cp_type', '제작업체')
|
|
||||||
->where('cp_state', 1)
|
|
||||||
->orderBy('cp_name', 'ASC')
|
|
||||||
->findAll();
|
|
||||||
$companyMap = [];
|
|
||||||
foreach ($companies as $company) {
|
|
||||||
$companyMap[(int) $company->cp_idx] = (string) $company->cp_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
$agencyMap = [];
|
|
||||||
foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $agency) {
|
|
||||||
$agencyMap[(int) $agency->sa_idx] = (string) ($agency->sa_name ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
||||||
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
|
||||||
$bagNameMap = [];
|
|
||||||
foreach ($bagCodes as $code) {
|
|
||||||
$bagNameMap[(string) $code->cd_code] = (string) $code->cd_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
$reportData = $this->buildOrderStatusRows(
|
|
||||||
$lgIdx,
|
|
||||||
$startMonth,
|
|
||||||
$endMonth,
|
|
||||||
$companyIdx,
|
|
||||||
$bagCode,
|
|
||||||
$receiveType,
|
|
||||||
$companyMap,
|
|
||||||
$agencyMap,
|
|
||||||
$bagNameMap
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->renderWorkPage(
|
|
||||||
'발주 현황',
|
|
||||||
'admin/bag_order/index',
|
|
||||||
[
|
|
||||||
'startMonth' => $startMonth,
|
|
||||||
'endMonth' => $endMonth,
|
|
||||||
'companyIdx' => $companyIdx,
|
|
||||||
'bagCode' => $bagCode,
|
|
||||||
'receiveType' => $receiveType,
|
|
||||||
'companyOptions' => $companies,
|
|
||||||
'bagCodeOptions' => $bagCodes,
|
|
||||||
'rows' => $reportData['rows'],
|
|
||||||
'groupRows' => $reportData['groupRows'],
|
|
||||||
'grandTotals' => $reportData['grandTotals'],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function export()
|
public function export()
|
||||||
@@ -113,240 +70,44 @@ class BagOrder extends BaseController
|
|||||||
return redirect()->to(mgmt_url('bag-orders'))->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(mgmt_url('bag-orders'))->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$startMonth = (string) ($this->request->getGet('start_month') ?? date('Y-m'));
|
$builder = $this->orderModel->where('bo_lg_idx', $lgIdx);
|
||||||
$endMonth = (string) ($this->request->getGet('end_month') ?? date('Y-m'));
|
$startDate = $this->request->getGet('start_date');
|
||||||
if (! preg_match('/^\d{4}-\d{2}$/', $startMonth)) {
|
$endDate = $this->request->getGet('end_date');
|
||||||
$startMonth = date('Y-m');
|
$status = $this->request->getGet('status');
|
||||||
}
|
if ($startDate) $builder->where('bo_order_date >=', $startDate);
|
||||||
if (! preg_match('/^\d{4}-\d{2}$/', $endMonth)) {
|
if ($endDate) $builder->where('bo_order_date <=', $endDate);
|
||||||
$endMonth = $startMonth;
|
if ($status) $builder->where('bo_status', $status);
|
||||||
}
|
|
||||||
if (strtotime($startMonth . '-01') > strtotime($endMonth . '-01')) {
|
|
||||||
[$startMonth, $endMonth] = [$endMonth, $startMonth];
|
|
||||||
}
|
|
||||||
|
|
||||||
$companyIdx = (int) ($this->request->getGet('company_idx') ?? 0);
|
$list = $builder->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->findAll();
|
||||||
$bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
|
|
||||||
$receiveType = (string) ($this->request->getGet('receive_type') ?? 'all');
|
|
||||||
if (! in_array($receiveType, ['all', 'received', 'pending'], true)) {
|
|
||||||
$receiveType = 'all';
|
|
||||||
}
|
|
||||||
|
|
||||||
$companyMap = [];
|
|
||||||
foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->findAll() as $company) {
|
|
||||||
$companyMap[(int) $company->cp_idx] = (string) $company->cp_name;
|
|
||||||
}
|
|
||||||
$agencyMap = [];
|
|
||||||
foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $agency) {
|
|
||||||
$agencyMap[(int) $agency->sa_idx] = (string) ($agency->sa_name ?? '');
|
|
||||||
}
|
|
||||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
||||||
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
|
||||||
$bagNameMap = [];
|
|
||||||
foreach ($bagCodes as $code) {
|
|
||||||
$bagNameMap[(string) $code->cd_code] = (string) $code->cd_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
$reportData = $this->buildOrderStatusRows(
|
|
||||||
$lgIdx,
|
|
||||||
$startMonth,
|
|
||||||
$endMonth,
|
|
||||||
$companyIdx,
|
|
||||||
$bagCode,
|
|
||||||
$receiveType,
|
|
||||||
$companyMap,
|
|
||||||
$agencyMap,
|
|
||||||
$bagNameMap
|
|
||||||
);
|
|
||||||
|
|
||||||
$rows = [];
|
$rows = [];
|
||||||
foreach ($reportData['rows'] as $row) {
|
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
||||||
if (! empty($row['is_subtotal'])) {
|
foreach ($list as $row) {
|
||||||
$rows[] = [
|
$items = $this->itemModel->where('boi_bo_idx', $row->bo_idx)->findAll();
|
||||||
'',
|
$totalQty = 0;
|
||||||
'',
|
$totalAmt = 0;
|
||||||
(string) ($row['label'] ?? '소계'),
|
foreach ($items as $it) {
|
||||||
(int) ($row['order_qty'] ?? 0),
|
$totalQty += (int) $it->boi_qty_sheet;
|
||||||
(int) ($row['received_qty'] ?? 0),
|
$totalAmt += (float) $it->boi_amount;
|
||||||
(int) ($row['pending_qty'] ?? 0),
|
|
||||||
(float) ($row['amount'] ?? 0),
|
|
||||||
'',
|
|
||||||
];
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
(string) ($row['order_date'] ?? ''),
|
$row->bo_idx,
|
||||||
(string) ($row['company_name'] ?? ''),
|
$row->bo_lot_no,
|
||||||
(string) ($row['bag_name'] ?? ''),
|
$row->bo_order_date,
|
||||||
(int) ($row['order_qty'] ?? 0),
|
count($items),
|
||||||
(int) ($row['received_qty'] ?? 0),
|
$totalQty,
|
||||||
(int) ($row['pending_qty'] ?? 0),
|
$totalAmt,
|
||||||
(float) ($row['amount'] ?? 0),
|
$statusMap[$row->bo_status] ?? $row->bo_status,
|
||||||
(string) ($row['agency_name'] ?? ''),
|
|
||||||
'',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$gt = $reportData['grandTotals'] ?? [];
|
export_csv(
|
||||||
$rows[] = [
|
'발주현황_' . date('Ymd') . '.csv',
|
||||||
'',
|
['번호', 'LOT번호', '발주일', '품목수', '총수량', '총금액', '상태'],
|
||||||
'',
|
|
||||||
'총계',
|
|
||||||
(int) ($gt['order_qty'] ?? 0),
|
|
||||||
(int) ($gt['received_qty'] ?? 0),
|
|
||||||
(int) ($gt['pending_qty'] ?? 0),
|
|
||||||
(float) ($gt['amount'] ?? 0),
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
];
|
|
||||||
|
|
||||||
export_xlsx(
|
|
||||||
'발주현황_' . date('Ymd'),
|
|
||||||
'발주현황',
|
|
||||||
['발주일자', '제작업체', '품명', '발주수량', '입고수량', '미입고수량', '발주금액', '입고처', '비고'],
|
|
||||||
$rows
|
$rows
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 발주 현황(품목 기준) 행 및 소계를 만든다.
|
|
||||||
*/
|
|
||||||
private function buildOrderStatusRows(
|
|
||||||
int $lgIdx,
|
|
||||||
string $startMonth,
|
|
||||||
string $endMonth,
|
|
||||||
int $companyIdx,
|
|
||||||
string $bagCode,
|
|
||||||
string $receiveType,
|
|
||||||
array $companyMap,
|
|
||||||
array $agencyMap,
|
|
||||||
array $bagNameMap
|
|
||||||
): array {
|
|
||||||
$startDate = $startMonth . '-01';
|
|
||||||
$endDate = date('Y-m-t', strtotime($endMonth . '-01 00:00:00'));
|
|
||||||
|
|
||||||
$builder = $this->orderModel
|
|
||||||
->where('bo_lg_idx', $lgIdx)
|
|
||||||
->whereLatestHead($lgIdx)
|
|
||||||
->where('bo_order_date >=', $startDate)
|
|
||||||
->where('bo_order_date <=', $endDate)
|
|
||||||
->whereIn('bo_status', ['normal', 'cancelled'])
|
|
||||||
->orderBy('bo_order_date', 'DESC')
|
|
||||||
->orderBy('bo_idx', 'DESC');
|
|
||||||
if ($companyIdx > 0) {
|
|
||||||
$builder->where('bo_company_idx', $companyIdx);
|
|
||||||
}
|
|
||||||
$orders = $builder->findAll();
|
|
||||||
|
|
||||||
if (empty($orders)) {
|
|
||||||
return ['rows' => [], 'groupRows' => [], 'grandTotals' => ['order_qty' => 0, 'received_qty' => 0, 'pending_qty' => 0, 'amount' => 0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderIds = array_map(static fn($order) => (int) $order->bo_idx, $orders);
|
|
||||||
|
|
||||||
$itemsByOrder = [];
|
|
||||||
if (! empty($orderIds)) {
|
|
||||||
$allItems = $this->itemModel
|
|
||||||
->whereIn('boi_bo_idx', $orderIds)
|
|
||||||
->orderBy('boi_bo_idx', 'DESC')
|
|
||||||
->orderBy('boi_idx', 'ASC')
|
|
||||||
->findAll();
|
|
||||||
foreach ($allItems as $item) {
|
|
||||||
$boIdx = (int) ($item->boi_bo_idx ?? 0);
|
|
||||||
if (! isset($itemsByOrder[$boIdx])) {
|
|
||||||
$itemsByOrder[$boIdx] = [];
|
|
||||||
}
|
|
||||||
$itemsByOrder[$boIdx][] = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$receivedMap = [];
|
|
||||||
$receivingRows = model(BagReceivingModel::class)
|
|
||||||
->select('br_bo_idx, br_bag_code, SUM(br_qty_sheet) as recv_qty')
|
|
||||||
->where('br_lg_idx', $lgIdx)
|
|
||||||
->whereIn('br_bo_idx', $orderIds)
|
|
||||||
->groupBy('br_bo_idx, br_bag_code')
|
|
||||||
->findAll();
|
|
||||||
foreach ($receivingRows as $received) {
|
|
||||||
$key = (int) ($received->br_bo_idx ?? 0) . '|' . (string) ($received->br_bag_code ?? '');
|
|
||||||
$receivedMap[$key] = (int) ($received->recv_qty ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
$groupRows = [];
|
|
||||||
$grandTotals = ['order_qty' => 0, 'received_qty' => 0, 'pending_qty' => 0, 'amount' => 0.0];
|
|
||||||
|
|
||||||
foreach ($orders as $order) {
|
|
||||||
$boIdx = (int) ($order->bo_idx ?? 0);
|
|
||||||
$items = $itemsByOrder[$boIdx] ?? [];
|
|
||||||
$groupCount = 0;
|
|
||||||
$groupTotalOrder = 0;
|
|
||||||
$groupTotalReceived = 0;
|
|
||||||
$groupTotalPending = 0;
|
|
||||||
$groupTotalAmount = 0.0;
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$itemBagCode = (string) ($item->boi_bag_code ?? '');
|
|
||||||
if ($bagCode !== '' && $itemBagCode !== $bagCode) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderQty = (int) ($item->boi_qty_sheet ?? 0);
|
|
||||||
$recvQty = (int) ($receivedMap[$boIdx . '|' . $itemBagCode] ?? 0);
|
|
||||||
if ($recvQty > $orderQty) {
|
|
||||||
$recvQty = $orderQty;
|
|
||||||
}
|
|
||||||
$pendingQty = max(0, $orderQty - $recvQty);
|
|
||||||
|
|
||||||
if ($receiveType === 'received' && $recvQty <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($receiveType === 'pending' && $pendingQty <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$amount = (float) ($item->boi_amount ?? 0);
|
|
||||||
$rows[] = [
|
|
||||||
'bo_idx' => $boIdx,
|
|
||||||
'order_date' => (string) ($order->bo_order_date ?? ''),
|
|
||||||
'company_name' => (string) ($companyMap[(int) ($order->bo_company_idx ?? 0)] ?? ''),
|
|
||||||
'bag_name' => (string) ($item->boi_bag_name ?? ($bagNameMap[$itemBagCode] ?? $itemBagCode)),
|
|
||||||
'order_qty' => $orderQty,
|
|
||||||
'received_qty' => $recvQty,
|
|
||||||
'pending_qty' => $pendingQty,
|
|
||||||
'amount' => $amount,
|
|
||||||
'agency_name' => (string) ($agencyMap[(int) ($order->bo_agency_idx ?? 0)] ?? ''),
|
|
||||||
];
|
|
||||||
|
|
||||||
$groupCount++;
|
|
||||||
$groupTotalOrder += $orderQty;
|
|
||||||
$groupTotalReceived += $recvQty;
|
|
||||||
$groupTotalPending += $pendingQty;
|
|
||||||
$groupTotalAmount += $amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($groupCount > 0) {
|
|
||||||
$groupRows[$boIdx] = $groupCount;
|
|
||||||
$rows[] = [
|
|
||||||
'bo_idx' => $boIdx,
|
|
||||||
'is_subtotal' => true,
|
|
||||||
'label' => '소계',
|
|
||||||
'order_qty' => $groupTotalOrder,
|
|
||||||
'received_qty' => $groupTotalReceived,
|
|
||||||
'pending_qty' => $groupTotalPending,
|
|
||||||
'amount' => $groupTotalAmount,
|
|
||||||
];
|
|
||||||
$grandTotals['order_qty'] += $groupTotalOrder;
|
|
||||||
$grandTotals['received_qty'] += $groupTotalReceived;
|
|
||||||
$grandTotals['pending_qty'] += $groupTotalPending;
|
|
||||||
$grandTotals['amount'] += $groupTotalAmount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['rows' => $rows, 'groupRows' => $groupRows, 'grandTotals' => $grandTotals];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
@@ -358,105 +119,18 @@ class BagOrder extends BaseController
|
|||||||
// 봉투 종류 + 단가 + 포장단위
|
// 봉투 종류 + 단가 + 포장단위
|
||||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
$priceMapRows = model(BagPriceModel::class)->latestActiveMapByBagCode($lgIdx);
|
$prices = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_state', 1)->findAll();
|
||||||
$units = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_state', 1)->findAll();
|
$units = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_state', 1)->findAll();
|
||||||
|
|
||||||
$companies = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)->findAll();
|
$companies = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)->findAll();
|
||||||
$associations = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '협회')->where('cp_state', 1)->findAll();
|
|
||||||
$agencies = model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll();
|
$agencies = model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll();
|
||||||
|
|
||||||
$companyMap = [];
|
return $this->renderWorkPage('발주 등록', 'admin/bag_order/create', compact('bagCodes', 'prices', 'units', 'companies', 'agencies'));
|
||||||
foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->findAll() as $company) {
|
|
||||||
$companyMap[(int) $company->cp_idx] = (string) $company->cp_name;
|
|
||||||
}
|
|
||||||
$agencyMap = [];
|
|
||||||
foreach ($agencies as $agency) {
|
|
||||||
$agencyMap[(int) $agency->sa_idx] = '[' . ($agency->sa_kind ?? '') . '] ' . ($agency->sa_code ?? '') . ' — ' . ($agency->sa_name ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
$recentOrders = $this->orderModel
|
|
||||||
->where('bo_lg_idx', $lgIdx)
|
|
||||||
->whereLatestHead($lgIdx)
|
|
||||||
->orderBy('bo_order_date', 'DESC')
|
|
||||||
->orderBy('bo_idx', 'DESC')
|
|
||||||
->findAll(12);
|
|
||||||
|
|
||||||
$bagNameMap = [];
|
|
||||||
foreach ($bagCodes as $codeDetail) {
|
|
||||||
$bagNameMap[(string) $codeDetail->cd_code] = (string) $codeDetail->cd_name;
|
|
||||||
}
|
|
||||||
$priceMap = [];
|
|
||||||
foreach ($priceMapRows as $bagCode => $price) {
|
|
||||||
$priceMap[(string) $bagCode] = (float) ($price->bp_order_price ?? 0);
|
|
||||||
}
|
|
||||||
$unitMap = [];
|
|
||||||
foreach ($units as $unit) {
|
|
||||||
$unitMap[(string) $unit->pu_bag_code] = [
|
|
||||||
'boxPerPack' => (int) $unit->pu_box_per_pack,
|
|
||||||
'packPerSheet' => (int) $unit->pu_pack_per_sheet,
|
|
||||||
'totalPerBox' => (int) $unit->pu_total_per_box,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$bagReferenceRows = [];
|
|
||||||
foreach ($bagCodes as $codeDetail) {
|
|
||||||
$bagCode = (string) $codeDetail->cd_code;
|
|
||||||
$unit = $unitMap[$bagCode] ?? ['boxPerPack' => 0, 'packPerSheet' => 0, 'totalPerBox' => 0];
|
|
||||||
$bagReferenceRows[] = [
|
|
||||||
'code' => $bagCode,
|
|
||||||
'name' => (string) ($bagNameMap[$bagCode] ?? ''),
|
|
||||||
'orderPrice' => (float) ($priceMap[$bagCode] ?? 0),
|
|
||||||
'boxPerPack' => (int) $unit['boxPerPack'],
|
|
||||||
'packPerSheet' => (int) $unit['packPerSheet'],
|
|
||||||
'totalPerBox' => (int) $unit['totalPerBox'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->renderWorkPage(
|
|
||||||
'발주 등록',
|
|
||||||
'admin/bag_order/create',
|
|
||||||
compact(
|
|
||||||
'bagCodes',
|
|
||||||
'units',
|
|
||||||
'companies',
|
|
||||||
'associations',
|
|
||||||
'agencies',
|
|
||||||
'recentOrders',
|
|
||||||
'companyMap',
|
|
||||||
'agencyMap',
|
|
||||||
'bagReferenceRows'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function revise(int $id)
|
|
||||||
{
|
|
||||||
helper('admin');
|
|
||||||
$lgIdx = admin_effective_lg_idx();
|
|
||||||
$order = $this->orderModel->find($id);
|
|
||||||
if (! $order || (int) $order->bo_lg_idx !== $lgIdx) {
|
|
||||||
return redirect()->to(mgmt_url('bag-orders'))->with('error', '발주를 찾을 수 없습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->to(site_url('bag/order/revise/' . $id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store()
|
public function store()
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null || $lgIdx <= 0) {
|
|
||||||
return redirect()->back()->withInput()->with('error', '지자체를 선택해 주세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceIdx = (int) ($this->request->getPost('bo_source_idx') ?? 0);
|
|
||||||
$sourceOrder = null;
|
|
||||||
if ($sourceIdx > 0) {
|
|
||||||
$sourceOrder = $this->orderModel->find($sourceIdx);
|
|
||||||
if (! $sourceOrder || (int) $sourceOrder->bo_lg_idx !== $lgIdx) {
|
|
||||||
return redirect()->back()->withInput()->with('error', '수정 대상 발주를 찾을 수 없습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$rules = [
|
$rules = [
|
||||||
'bo_order_date' => 'required|valid_date[Y-m-d]',
|
'bo_order_date' => 'required|valid_date[Y-m-d]',
|
||||||
@@ -467,114 +141,65 @@ class BagOrder extends BaseController
|
|||||||
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
||||||
}
|
}
|
||||||
|
|
||||||
$bagCodes = $this->request->getPost('item_bag_code') ?? [];
|
|
||||||
$qtySheets = $this->request->getPost('item_qty_sheet') ?? [];
|
|
||||||
$qtyBoxes = $this->request->getPost('item_qty_box') ?? []; // 구 화면 호환
|
|
||||||
$postedUnitPrices = $this->request->getPost('item_unit_price');
|
|
||||||
$changeKind = (string) ($this->request->getPost('bo_change_mode') ?? 'meta');
|
|
||||||
if (! in_array($changeKind, ['price', 'meta', 'delete'], true)) {
|
|
||||||
$changeKind = 'meta';
|
|
||||||
}
|
|
||||||
$itemCount = count($bagCodes);
|
|
||||||
$normalizedItems = [];
|
|
||||||
for ($i = 0; $i < $itemCount; $i++) {
|
|
||||||
$code = trim((string) ($bagCodes[$i] ?? ''));
|
|
||||||
$qtySheet = (int) ($qtySheets[$i] ?? 0);
|
|
||||||
$qtyBox = (int) ($qtyBoxes[$i] ?? 0);
|
|
||||||
if ($code === '' || ($qtySheet <= 0 && $qtyBox <= 0)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$normalizedItems[] = ['code' => $code, 'qtySheet' => $qtySheet, 'qtyBox' => $qtyBox];
|
|
||||||
}
|
|
||||||
if (empty($normalizedItems)) {
|
|
||||||
return redirect()->back()->withInput()->with('error', '최소 1개 이상의 봉투 수량을 입력해 주세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$priceByCode = [];
|
|
||||||
if ($sourceOrder !== null && $changeKind === 'price' && is_array($postedUnitPrices)) {
|
|
||||||
for ($pi = 0; $pi < count($bagCodes); $pi++) {
|
|
||||||
$c = trim((string) ($bagCodes[$pi] ?? ''));
|
|
||||||
if ($c === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$raw = $postedUnitPrices[$pi] ?? null;
|
|
||||||
if ($raw !== null && $raw !== '' && is_numeric($raw)) {
|
|
||||||
$priceByCode[$c] = round((float) $raw, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$db->transStart();
|
$db->transStart();
|
||||||
|
|
||||||
try {
|
// UUID 생성
|
||||||
if ($sourceOrder) {
|
$uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||||
$uuid = (string) $sourceOrder->bo_uuid;
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||||
$maxVerRow = $this->orderModel->selectMax('bo_version')->where('bo_uuid', $uuid)->first();
|
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
||||||
$latestVersion = ($maxVerRow !== null && isset($maxVerRow->bo_version)) ? (int) $maxVerRow->bo_version : 0;
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
|
||||||
$version = $latestVersion + 1;
|
|
||||||
$lotNo = (string) $sourceOrder->bo_lot_no;
|
// LOT 번호 생성
|
||||||
} else {
|
$lotNo = 'LOT-' . date('Ymd') . '-' . strtoupper(substr(md5($uuid), 0, 6));
|
||||||
$uuid = $this->generateUuidV4();
|
|
||||||
$version = 1;
|
|
||||||
$lotNo = $this->generateLotNo6();
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderData = [
|
$orderData = [
|
||||||
'bo_uuid' => $uuid,
|
'bo_uuid' => $uuid,
|
||||||
'bo_version' => $version,
|
'bo_version' => 1,
|
||||||
'bo_lg_idx' => $lgIdx,
|
'bo_lg_idx' => $lgIdx,
|
||||||
'bo_gugun_code' => $this->resolveGugunCodeFromLg($lgIdx),
|
'bo_gugun_code' => $this->request->getPost('bo_gugun_code') ?? '',
|
||||||
'bo_dong_code' => '',
|
'bo_dong_code' => $this->request->getPost('bo_dong_code') ?? '',
|
||||||
'bo_company_idx' => $this->request->getPost('bo_company_idx') ?: null,
|
'bo_company_idx' => $this->request->getPost('bo_company_idx') ?: null,
|
||||||
'bo_agency_idx' => $this->request->getPost('bo_agency_idx') ?: null,
|
'bo_agency_idx' => $this->request->getPost('bo_agency_idx') ?: null,
|
||||||
'bo_fee_rate' => (float) ($this->request->getPost('bo_fee_rate') ?: 0),
|
'bo_fee_rate' => (float) ($this->request->getPost('bo_fee_rate') ?: 0),
|
||||||
'bo_order_date' => $this->request->getPost('bo_order_date'),
|
'bo_order_date' => $this->request->getPost('bo_order_date'),
|
||||||
'bo_bag_types' => '',
|
|
||||||
'bo_unit_prices' => '',
|
|
||||||
'bo_qty_boxes' => '',
|
|
||||||
'bo_lot_no' => $lotNo,
|
'bo_lot_no' => $lotNo,
|
||||||
'bo_status' => 'normal',
|
'bo_status' => 'normal',
|
||||||
'bo_orderer_idx' => session()->get('mb_idx'),
|
'bo_orderer_idx' => session()->get('mb_idx'),
|
||||||
'bo_regdate' => date('Y-m-d H:i:s'),
|
'bo_regdate' => date('Y-m-d H:i:s'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 품목 입력 후 최종 payload 기준으로 해시를 계산하므로 우선 빈값으로 생성
|
// SHA-256 해시
|
||||||
$orderData['bo_hash'] = '';
|
$orderData['bo_hash'] = hash('sha256', json_encode($orderData));
|
||||||
|
|
||||||
$this->orderModel->insert($orderData);
|
$this->orderModel->insert($orderData);
|
||||||
$boIdx = (int) $this->orderModel->getInsertID();
|
$boIdx = (int) $this->orderModel->getInsertID();
|
||||||
|
|
||||||
|
// CT-05: 감사 로그
|
||||||
|
helper('audit');
|
||||||
|
audit_log('create', 'bag_order', $boIdx, null, array_merge($orderData, ['bo_idx' => $boIdx]));
|
||||||
|
|
||||||
// 품목 저장
|
// 품목 저장
|
||||||
$hashItems = [];
|
$bagCodes = $this->request->getPost('item_bag_code') ?? [];
|
||||||
$bagTypesForHeader = [];
|
$qtyBoxes = $this->request->getPost('item_qty_box') ?? [];
|
||||||
$unitPricesForHeader = [];
|
foreach ($bagCodes as $i => $code) {
|
||||||
$qtyBoxesForHeader = [];
|
if (empty($code) || empty($qtyBoxes[$i])) continue;
|
||||||
foreach ($normalizedItems as $item) {
|
$qtyBox = (int) $qtyBoxes[$i];
|
||||||
$code = $item['code'];
|
|
||||||
$qtySheetInput = (int) ($item['qtySheet'] ?? 0);
|
|
||||||
$qtyBoxInput = (int) ($item['qtyBox'] ?? 0);
|
|
||||||
// 포장단위에서 낱장 환산
|
// 포장단위에서 낱장 환산
|
||||||
$unit = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $code)->where('pu_state', 1)->first();
|
$unit = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_bag_code', $code)->where('pu_state', 1)->first();
|
||||||
$totalPerBox = $unit ? max(1, (int) $unit->pu_total_per_box) : 1;
|
$totalPerBox = $unit ? (int) $unit->pu_total_per_box : 1;
|
||||||
$qtySheet = $qtySheetInput > 0 ? $qtySheetInput : ($qtyBoxInput * $totalPerBox);
|
$qtySheet = $qtyBox * $totalPerBox;
|
||||||
if ($qtySheet <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$qtyBox = intdiv($qtySheet, $totalPerBox);
|
|
||||||
|
|
||||||
// 단가 (발주 변경·단가 구분 시 POST 단가 우선)
|
// 단가
|
||||||
$price = model(BagPriceModel::class)->latestActiveByBagCode($lgIdx, $code);
|
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $code)->where('bp_state', 1)->first();
|
||||||
$unitPrice = $price ? (float) $price->bp_order_price : 0;
|
$unitPrice = $price ? (float) $price->bp_order_price : 0;
|
||||||
if ($sourceOrder !== null && isset($priceByCode[$code])) {
|
|
||||||
$unitPrice = $priceByCode[$code];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 봉투명
|
// 봉투명
|
||||||
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $code, $lgIdx) : null;
|
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $code, $lgIdx) : null;
|
||||||
|
|
||||||
$itemData = [
|
$this->itemModel->insert([
|
||||||
'boi_bo_idx' => $boIdx,
|
'boi_bo_idx' => $boIdx,
|
||||||
'boi_bag_code' => $code,
|
'boi_bag_code' => $code,
|
||||||
'boi_bag_name' => $detail ? $detail->cd_name : '',
|
'boi_bag_name' => $detail ? $detail->cd_name : '',
|
||||||
@@ -582,204 +207,14 @@ class BagOrder extends BaseController
|
|||||||
'boi_qty_box' => $qtyBox,
|
'boi_qty_box' => $qtyBox,
|
||||||
'boi_qty_sheet' => $qtySheet,
|
'boi_qty_sheet' => $qtySheet,
|
||||||
'boi_amount' => $unitPrice * $qtySheet,
|
'boi_amount' => $unitPrice * $qtySheet,
|
||||||
];
|
|
||||||
$this->itemModel->insert($itemData);
|
|
||||||
$hashItems[] = $itemData;
|
|
||||||
|
|
||||||
$bagTypesForHeader[] = [
|
|
||||||
'code' => $itemData['boi_bag_code'],
|
|
||||||
'name' => $itemData['boi_bag_name'],
|
|
||||||
];
|
|
||||||
$unitPricesForHeader[] = [
|
|
||||||
'code' => $itemData['boi_bag_code'],
|
|
||||||
'unit_price' => $itemData['boi_unit_price'],
|
|
||||||
];
|
|
||||||
$qtyBoxesForHeader[] = [
|
|
||||||
'code' => $itemData['boi_bag_code'],
|
|
||||||
'qty_box' => $itemData['boi_qty_box'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderData['bo_bag_types'] = json_encode($bagTypesForHeader, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
|
|
||||||
$orderData['bo_unit_prices'] = json_encode($unitPricesForHeader, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
|
|
||||||
$orderData['bo_qty_boxes'] = json_encode($qtyBoxesForHeader, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
|
|
||||||
|
|
||||||
// 최종 발주 데이터(헤더+품목) 해시
|
|
||||||
$hashPayload = $orderData;
|
|
||||||
$hashPayload['bo_idx'] = $boIdx;
|
|
||||||
$hashPayload['items'] = $hashItems;
|
|
||||||
$hashJson = json_encode($hashPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
||||||
$orderHash = hash('sha256', $hashJson !== false ? $hashJson : (string) $boIdx);
|
|
||||||
$this->orderModel->update($boIdx, [
|
|
||||||
'bo_bag_types' => $orderData['bo_bag_types'],
|
|
||||||
'bo_unit_prices' => $orderData['bo_unit_prices'],
|
|
||||||
'bo_qty_boxes' => $orderData['bo_qty_boxes'],
|
|
||||||
'bo_hash' => $orderHash,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$beforeHash = $sourceOrder ? (string) ($sourceOrder->bo_hash ?? '') : '';
|
|
||||||
$seedFilePath = $this->generateBarcodeSeedFile($uuid, $version, $lotNo, $orderData, $hashItems, $orderHash);
|
|
||||||
$blockPayload = [
|
|
||||||
'bo_idx' => $boIdx,
|
|
||||||
'bo_uuid' => $uuid,
|
|
||||||
'bo_version' => $version,
|
|
||||||
'bo_lot_no' => $lotNo,
|
|
||||||
'bo_hash' => $orderHash,
|
|
||||||
'seed_file' => $seedFilePath,
|
|
||||||
'hash_chain' => $beforeHash !== '' ? [$beforeHash, $orderHash] : [$orderHash],
|
|
||||||
'order' => $orderData,
|
|
||||||
'items' => $hashItems,
|
|
||||||
];
|
|
||||||
$ledger = new SqlLedger();
|
|
||||||
$ledger->appendBlock(
|
|
||||||
$sourceOrder ? 'ORDER_UPDATE' : 'ORDER_CREATE',
|
|
||||||
$blockPayload,
|
|
||||||
$uuid,
|
|
||||||
$version,
|
|
||||||
session()->get('mb_idx') ? (int) session()->get('mb_idx') : null,
|
|
||||||
$lgIdx
|
|
||||||
);
|
|
||||||
|
|
||||||
// CT-05: 감사 로그
|
|
||||||
helper('audit');
|
|
||||||
if ($sourceOrder) {
|
|
||||||
audit_log(
|
|
||||||
'update',
|
|
||||||
'bag_order',
|
|
||||||
$boIdx,
|
|
||||||
['bo_idx' => (int) $sourceOrder->bo_idx, 'bo_hash' => $beforeHash, 'bo_version' => (int) $sourceOrder->bo_version],
|
|
||||||
array_merge($orderData, ['bo_idx' => $boIdx, 'bo_hash' => $orderHash, 'items' => $hashItems, 'seed_file' => $seedFilePath])
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
audit_log('create', 'bag_order', $boIdx, null, array_merge($orderData, ['bo_idx' => $boIdx, 'bo_hash' => $orderHash, 'items' => $hashItems, 'seed_file' => $seedFilePath]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $db->transComplete()) {
|
$db->transComplete();
|
||||||
throw new \RuntimeException('Transaction did not complete');
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$db->transRollback();
|
|
||||||
log_message('error', 'BagOrder::store: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
|
|
||||||
|
|
||||||
return redirect()->back()->withInput()->with('error', '발주 저장 중 오류가 발생했습니다. 잠시 후 다시 시도하거나 관리자에게 문의하세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 등록되었습니다. LOT: ' . $lotNo);
|
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 등록되었습니다. LOT: ' . $lotNo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 효과 지자체(`local_government`)의 행정 구·군 코드(lg_code) */
|
|
||||||
private function resolveGugunCodeFromLg(int $lgIdx): string
|
|
||||||
{
|
|
||||||
$lg = model(LocalGovernmentModel::class)->find($lgIdx);
|
|
||||||
|
|
||||||
return $lg ? trim((string) ($lg->lg_code ?? '')) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generateUuidV4(): string
|
|
||||||
{
|
|
||||||
$bytes = random_bytes(16);
|
|
||||||
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
|
||||||
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
|
||||||
|
|
||||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function generateLotNo6(): string
|
|
||||||
{
|
|
||||||
// 문서의 "LOT 번호 6 Byte" 요구를 맞추기 위해 영숫자 6자리로 생성한다.
|
|
||||||
// 충돌 가능성을 낮추기 위해 최대 20회 재시도 후 timestamp 기반으로 fallback.
|
|
||||||
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
||||||
for ($attempt = 0; $attempt < 20; $attempt++) {
|
|
||||||
$lot = '';
|
|
||||||
for ($i = 0; $i < 6; $i++) {
|
|
||||||
$lot .= $chars[random_int(0, strlen($chars) - 1)];
|
|
||||||
}
|
|
||||||
|
|
||||||
$exists = $this->orderModel->where('bo_lot_no', $lot)->countAllResults() > 0;
|
|
||||||
if (! $exists) {
|
|
||||||
return $lot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strtoupper(substr(base_convert((string) time(), 10, 36), -6));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string,mixed> $orderData
|
|
||||||
* @param array<int,array<string,mixed>> $items
|
|
||||||
*/
|
|
||||||
private function generateBarcodeSeedFile(string $uuid, int $version, string $lotNo, array $orderData, array $items, string $orderHash): string
|
|
||||||
{
|
|
||||||
$baseDir = WRITEPATH . 'barcode-seeds';
|
|
||||||
if (! is_dir($baseDir)) {
|
|
||||||
mkdir($baseDir, 0775, true);
|
|
||||||
}
|
|
||||||
$keyDir = WRITEPATH . 'keys';
|
|
||||||
if (! is_dir($keyDir)) {
|
|
||||||
mkdir($keyDir, 0775, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$privateKeyPath = $keyDir . DIRECTORY_SEPARATOR . 'barcode_seed_private.pem';
|
|
||||||
$publicKeyPath = $keyDir . DIRECTORY_SEPARATOR . 'barcode_seed_public.pem';
|
|
||||||
if (! is_file($privateKeyPath) || ! is_file($publicKeyPath)) {
|
|
||||||
$config = [
|
|
||||||
'private_key_bits' => 2048,
|
|
||||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
|
||||||
];
|
|
||||||
$resource = openssl_pkey_new($config);
|
|
||||||
if ($resource !== false) {
|
|
||||||
$privatePem = '';
|
|
||||||
openssl_pkey_export($resource, $privatePem);
|
|
||||||
$details = openssl_pkey_get_details($resource);
|
|
||||||
$publicPem = $details['key'] ?? '';
|
|
||||||
if ($privatePem !== '' && $publicPem !== '') {
|
|
||||||
file_put_contents($privateKeyPath, $privatePem);
|
|
||||||
file_put_contents($publicKeyPath, $publicPem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$payload = [
|
|
||||||
'uuid' => $uuid,
|
|
||||||
'version' => $version,
|
|
||||||
'lot_no' => $lotNo,
|
|
||||||
'order_hash' => $orderHash,
|
|
||||||
'order' => $orderData,
|
|
||||||
'items' => $items,
|
|
||||||
];
|
|
||||||
$payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}';
|
|
||||||
|
|
||||||
$aesKey = random_bytes(32);
|
|
||||||
$iv = random_bytes(16);
|
|
||||||
$cipherRaw = openssl_encrypt($payloadJson, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
|
|
||||||
if ($cipherRaw === false) {
|
|
||||||
$cipherRaw = $payloadJson;
|
|
||||||
}
|
|
||||||
$encryptedKey = '';
|
|
||||||
$publicPem = is_file($publicKeyPath) ? file_get_contents($publicKeyPath) : '';
|
|
||||||
if (is_string($publicPem) && $publicPem !== '') {
|
|
||||||
openssl_public_encrypt($aesKey, $encryptedKey, $publicPem, OPENSSL_PKCS1_OAEP_PADDING);
|
|
||||||
}
|
|
||||||
|
|
||||||
$seed = [
|
|
||||||
'algorithm' => ['symmetric' => 'AES-256-CBC', 'asymmetric' => 'RSA-2048'],
|
|
||||||
'lot_no' => $lotNo,
|
|
||||||
'uuid' => $uuid,
|
|
||||||
'version' => $version,
|
|
||||||
'iv_b64' => base64_encode($iv),
|
|
||||||
'key_b64' => $encryptedKey !== '' ? base64_encode($encryptedKey) : '',
|
|
||||||
'cipher_b64' => base64_encode((string) $cipherRaw),
|
|
||||||
'payload_hash' => hash('sha256', $payloadJson),
|
|
||||||
'created_at' => date('c'),
|
|
||||||
];
|
|
||||||
|
|
||||||
$fileName = $lotNo . '_v' . $version . '.seed.json';
|
|
||||||
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $fileName;
|
|
||||||
file_put_contents($fullPath, json_encode($seed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
|
||||||
|
|
||||||
return $fullPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function detail(int $id)
|
public function detail(int $id)
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
@@ -815,11 +250,9 @@ class BagOrder extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$before = (array) $order;
|
$before = (array) $order;
|
||||||
$beforeHash = (string) ($order->bo_hash ?? '');
|
$this->orderModel->update($id, ['bo_status' => 'cancelled', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
||||||
$this->appendLedgerForStatusChange($order, $id, 'ORDER_CANCEL', 'cancelled', $beforeHash);
|
|
||||||
$after = (array) $this->orderModel->find($id);
|
|
||||||
helper('audit');
|
helper('audit');
|
||||||
audit_log('update', 'bag_order', $id, $before, $after);
|
audit_log('update', 'bag_order', $id, $before, ['bo_status' => 'cancelled']);
|
||||||
|
|
||||||
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 취소되었습니다.');
|
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 취소되었습니다.');
|
||||||
}
|
}
|
||||||
@@ -833,117 +266,10 @@ class BagOrder extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$before = (array) $order;
|
$before = (array) $order;
|
||||||
$beforeHash = (string) ($order->bo_hash ?? '');
|
$this->orderModel->update($id, ['bo_status' => 'deleted', 'bo_moddate' => date('Y-m-d H:i:s')]);
|
||||||
$this->appendLedgerForStatusChange($order, $id, 'ORDER_DELETE', 'deleted', $beforeHash);
|
|
||||||
$after = (array) $this->orderModel->find($id);
|
|
||||||
helper('audit');
|
helper('audit');
|
||||||
audit_log('delete', 'bag_order', $id, $before, $after);
|
audit_log('delete', 'bag_order', $id, $before, ['bo_status' => 'deleted']);
|
||||||
|
|
||||||
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 삭제 처리되었습니다.');
|
return redirect()->to(mgmt_url('bag-orders'))->with('success', '발주가 삭제 처리되었습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 상태 변경 시(취소/삭제) 무결성 검증을 위해 bo_hash 재계산 후
|
|
||||||
* SQL-Ledger(append-only)에 블록을 추가한다.
|
|
||||||
*
|
|
||||||
* @param object $order
|
|
||||||
* @param int $boIdx
|
|
||||||
* @param string $txType ORDER_CANCEL|ORDER_DELETE
|
|
||||||
* @param string $newStatus cancelled|deleted
|
|
||||||
* @param string $previousHash
|
|
||||||
*/
|
|
||||||
private function appendLedgerForStatusChange(object $order, int $boIdx, string $txType, string $newStatus, string $previousHash): void
|
|
||||||
{
|
|
||||||
// 품목은 상태 변경 시 그대로이므로, 동일 payload 형태로 items array를 만든다.
|
|
||||||
$items = $this->itemModel->where('boi_bo_idx', $boIdx)->findAll();
|
|
||||||
$hashItems = [];
|
|
||||||
foreach ($items as $it) {
|
|
||||||
$hashItems[] = [
|
|
||||||
'boi_bo_idx' => (int) $it->boi_bo_idx,
|
|
||||||
'boi_bag_code' => (string) $it->boi_bag_code,
|
|
||||||
'boi_bag_name' => (string) ($it->boi_bag_name ?? ''),
|
|
||||||
'boi_unit_price' => (float) $it->boi_unit_price,
|
|
||||||
'boi_qty_box' => (int) $it->boi_qty_box,
|
|
||||||
'boi_qty_sheet' => (int) $it->boi_qty_sheet,
|
|
||||||
'boi_amount' => (float) $it->boi_amount,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$newOrder = $order;
|
|
||||||
$newOrder->bo_status = $newStatus;
|
|
||||||
|
|
||||||
$newHash = $this->computeOrderHash($boIdx, $newOrder, $hashItems);
|
|
||||||
$actorIdx = session()->get('mb_idx') ? (int) session()->get('mb_idx') : null;
|
|
||||||
$lgIdx = (int) ($order->bo_lg_idx ?? 0);
|
|
||||||
|
|
||||||
$seedFilePath = '';
|
|
||||||
$ledgerPayload = [
|
|
||||||
'bo_idx' => $boIdx,
|
|
||||||
'bo_uuid' => (string) $order->bo_uuid,
|
|
||||||
'bo_version' => (int) $order->bo_version,
|
|
||||||
'bo_lot_no' => (string) $order->bo_lot_no,
|
|
||||||
'bo_hash' => $newHash,
|
|
||||||
'seed_file' => $seedFilePath,
|
|
||||||
'hash_chain' => [$previousHash, $newHash],
|
|
||||||
'order' => [
|
|
||||||
'bo_status' => $newStatus,
|
|
||||||
'bo_hash' => $newHash,
|
|
||||||
],
|
|
||||||
'items' => $hashItems,
|
|
||||||
];
|
|
||||||
|
|
||||||
$ledger = new SqlLedger();
|
|
||||||
$ledger->appendBlock(
|
|
||||||
$txType,
|
|
||||||
$ledgerPayload,
|
|
||||||
(string) $order->bo_uuid,
|
|
||||||
(int) $order->bo_version,
|
|
||||||
$actorIdx,
|
|
||||||
$lgIdx
|
|
||||||
);
|
|
||||||
|
|
||||||
// order row에 hash 반영
|
|
||||||
$this->orderModel->update($boIdx, [
|
|
||||||
'bo_status' => $newStatus,
|
|
||||||
'bo_moddate' => date('Y-m-d H:i:s'),
|
|
||||||
'bo_hash' => $newHash,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* store()에서 생성하는 bo_hash와 동일한 "헤더+items" 규격을 사용해 SHA-256을 계산한다.
|
|
||||||
*
|
|
||||||
* @param int $boIdx
|
|
||||||
* @param object $order
|
|
||||||
* @param array<int,array<string,mixed>> $hashItems
|
|
||||||
*/
|
|
||||||
private function computeOrderHash(int $boIdx, object $order, array $hashItems): string
|
|
||||||
{
|
|
||||||
$orderData = [
|
|
||||||
'bo_uuid' => (string) $order->bo_uuid,
|
|
||||||
'bo_version' => (int) $order->bo_version,
|
|
||||||
'bo_lg_idx' => (int) $order->bo_lg_idx,
|
|
||||||
'bo_gugun_code' => (string) ($order->bo_gugun_code ?? ''),
|
|
||||||
'bo_dong_code' => (string) ($order->bo_dong_code ?? ''),
|
|
||||||
'bo_company_idx' => $order->bo_company_idx !== null ? (int) $order->bo_company_idx : null,
|
|
||||||
'bo_agency_idx' => $order->bo_agency_idx !== null ? (int) $order->bo_agency_idx : null,
|
|
||||||
'bo_fee_rate' => (float) ($order->bo_fee_rate ?? 0),
|
|
||||||
'bo_order_date' => (string) $order->bo_order_date,
|
|
||||||
'bo_bag_types' => (string) ($order->bo_bag_types ?? ''),
|
|
||||||
'bo_unit_prices' => (string) ($order->bo_unit_prices ?? ''),
|
|
||||||
'bo_qty_boxes' => (string) ($order->bo_qty_boxes ?? ''),
|
|
||||||
'bo_lot_no' => (string) $order->bo_lot_no,
|
|
||||||
'bo_hash' => '',
|
|
||||||
'bo_status' => (string) $order->bo_status,
|
|
||||||
'bo_orderer_idx' => $order->bo_orderer_idx !== null ? (int) $order->bo_orderer_idx : null,
|
|
||||||
'bo_regdate' => (string) ($order->bo_regdate ?? ''),
|
|
||||||
];
|
|
||||||
|
|
||||||
$hashPayload = $orderData;
|
|
||||||
$hashPayload['bo_idx'] = $boIdx;
|
|
||||||
$hashPayload['items'] = $hashItems;
|
|
||||||
|
|
||||||
$hashJson = json_encode($hashPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
||||||
return hash('sha256', $hashJson !== false ? $hashJson : (string) $boIdx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,139 +27,14 @@ class BagPrice extends BaseController
|
|||||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$get = $this->request->getGet();
|
$list = $this->priceModel->where('bp_lg_idx', $lgIdx)
|
||||||
$readSrc = static function (array $src, string $key): ?string {
|
|
||||||
if (! array_key_exists($key, $src)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$v = $src[$key];
|
|
||||||
if ($v === null || is_array($v)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$s = trim((string) $v);
|
|
||||||
|
|
||||||
return $s === '' ? null : $s;
|
|
||||||
};
|
|
||||||
|
|
||||||
$sy = $readSrc($get, 'start_y');
|
|
||||||
$sm = $readSrc($get, 'start_m');
|
|
||||||
$sd = $readSrc($get, 'start_d');
|
|
||||||
$ey = $readSrc($get, 'end_y');
|
|
||||||
$em = $readSrc($get, 'end_m');
|
|
||||||
$ed = $readSrc($get, 'end_d');
|
|
||||||
|
|
||||||
$startDate = null;
|
|
||||||
if ($sy !== null && $sy !== '' && $sm !== null && $sm !== '' && $sd !== null && $sd !== '') {
|
|
||||||
$startDate = parse_ymd_from_triple($sy, $sm, $sd);
|
|
||||||
}
|
|
||||||
if ($startDate === null) {
|
|
||||||
$legacyStart = $readSrc($get, 'start_date');
|
|
||||||
$startDate = ($legacyStart !== null && $legacyStart !== '') ? $legacyStart : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$endDate = null;
|
|
||||||
if ($ey !== null && $ey !== '' && $em !== null && $em !== '' && $ed !== null && $ed !== '') {
|
|
||||||
$endDate = parse_ymd_from_triple($ey, $em, $ed);
|
|
||||||
}
|
|
||||||
if ($endDate === null) {
|
|
||||||
$legacyEnd = $readSrc($get, 'end_date');
|
|
||||||
$endDate = ($legacyEnd !== null && $legacyEnd !== '') ? $legacyEnd : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$startParts = ['y' => '', 'm' => '', 'd' => ''];
|
|
||||||
$endParts = ['y' => '', 'm' => '', 'd' => ''];
|
|
||||||
if ($startDate !== null && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $startDate, $m)) {
|
|
||||||
$startParts = ['y' => $m[1], 'm' => (int) $m[2], 'd' => (int) $m[3]];
|
|
||||||
}
|
|
||||||
if ($endDate !== null && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $endDate, $m)) {
|
|
||||||
$endParts = ['y' => $m[1], 'm' => (int) $m[2], 'd' => (int) $m[3]];
|
|
||||||
}
|
|
||||||
|
|
||||||
$bagKindE = $readSrc($get, 'bag_kind_e');
|
|
||||||
$bagCode = $readSrc($get, 'bag_code');
|
|
||||||
|
|
||||||
$builder = $this->priceModel->where('bp_lg_idx', $lgIdx);
|
|
||||||
if (($startDate !== null && $startDate !== '') || ($endDate !== null && $endDate !== '')) {
|
|
||||||
$qStart = ($startDate !== null && $startDate !== '') ? $startDate : $endDate;
|
|
||||||
$qEnd = ($endDate !== null && $endDate !== '') ? $endDate : $startDate;
|
|
||||||
if (strcmp((string) $qStart, (string) $qEnd) > 0) {
|
|
||||||
[$qStart, $qEnd] = [$qEnd, $qStart];
|
|
||||||
}
|
|
||||||
$builder->where('bp_start_date <=', $qEnd);
|
|
||||||
$builder->groupStart()
|
|
||||||
->where('bp_end_date IS NULL')
|
|
||||||
->orWhere('bp_end_date >=', $qStart)
|
|
||||||
->groupEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($bagKindE !== null && $bagKindE !== '') {
|
|
||||||
$kindE = model(CodeKindModel::class)->where('ck_code', 'E')->first();
|
|
||||||
if ($kindE) {
|
|
||||||
$detailE = model(CodeDetailModel::class)
|
|
||||||
->where('cd_ck_idx', (int) $kindE->ck_idx)
|
|
||||||
->where('cd_code', $bagKindE)
|
|
||||||
->where('cd_state', 1)
|
|
||||||
->first();
|
|
||||||
if ($detailE !== null) {
|
|
||||||
$builder->like('bp_bag_code', $bagKindE, 'after');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($bagCode !== null && $bagCode !== '') {
|
|
||||||
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
||||||
if ($kindO) {
|
|
||||||
$detailO = model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, $bagCode, $lgIdx);
|
|
||||||
if ($detailO !== null) {
|
|
||||||
$builder->where('bp_bag_code', $bagCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$list = $builder
|
|
||||||
->orderBy('bp_bag_code', 'ASC')
|
->orderBy('bp_bag_code', 'ASC')
|
||||||
->orderBy('bp_start_date', 'DESC')
|
->orderBy('bp_start_date', 'DESC')
|
||||||
->paginate(20);
|
->paginate(20);
|
||||||
|
|
||||||
$queryForPager = [];
|
|
||||||
if ($sy !== null && $sm !== null && $sd !== null && $sy !== '' && $sm !== '' && $sd !== '') {
|
|
||||||
$queryForPager['start_y'] = $sy;
|
|
||||||
$queryForPager['start_m'] = $sm;
|
|
||||||
$queryForPager['start_d'] = $sd;
|
|
||||||
}
|
|
||||||
if ($ey !== null && $em !== null && $ed !== null && $ey !== '' && $em !== '' && $ed !== '') {
|
|
||||||
$queryForPager['end_y'] = $ey;
|
|
||||||
$queryForPager['end_m'] = $em;
|
|
||||||
$queryForPager['end_d'] = $ed;
|
|
||||||
}
|
|
||||||
if ($bagKindE !== null && $bagKindE !== '') {
|
|
||||||
$queryForPager['bag_kind_e'] = $bagKindE;
|
|
||||||
}
|
|
||||||
if ($bagCode !== null && $bagCode !== '') {
|
|
||||||
$queryForPager['bag_code'] = $bagCode;
|
|
||||||
}
|
|
||||||
apply_pager_path($this->priceModel->pager, mgmt_path('bag-prices'), $queryForPager);
|
|
||||||
|
|
||||||
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
||||||
$bagCodes = $kindO
|
|
||||||
? model(CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx)
|
|
||||||
: [];
|
|
||||||
$kindE = model(CodeKindModel::class)->where('ck_code', 'E')->first();
|
|
||||||
$bagKindOptions = $kindE
|
|
||||||
? model(CodeDetailModel::class)->getByKind((int) $kindE->ck_idx, true, null)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return $this->renderWorkPage('봉투 단가 관리', 'admin/bag_price/index', [
|
return $this->renderWorkPage('봉투 단가 관리', 'admin/bag_price/index', [
|
||||||
'list' => $list,
|
'list' => $list,
|
||||||
'pager' => $this->priceModel->pager,
|
'pager' => $this->priceModel->pager,
|
||||||
'startParts' => $startParts,
|
|
||||||
'endParts' => $endParts,
|
|
||||||
'dateYearMin' => (int) date('Y') - 12,
|
|
||||||
'dateYearMax' => (int) date('Y') + 2,
|
|
||||||
'bag_kind_e' => $bagKindE,
|
|
||||||
'bag_code' => $bagCode,
|
|
||||||
'bag_codes' => $bagCodes,
|
|
||||||
'bag_kind_options' => $bagKindOptions,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class BagReceiving extends BaseController
|
|||||||
return redirect()->to(mgmt_url('bag-receivings'))->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(mgmt_url('bag-receivings'))->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$orders = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->whereLatestHead($lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll();
|
$orders = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->where('bo_status', 'normal')->orderBy('bo_order_date', 'DESC')->findAll();
|
||||||
|
|
||||||
return $this->renderWorkPage('입고 처리', 'admin/bag_receiving/create', compact('orders'));
|
return $this->renderWorkPage('입고 처리', 'admin/bag_receiving/create', compact('orders'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class BagSale extends BaseController
|
|||||||
$shop = model(DesignatedShopModel::class)->find($dsIdx);
|
$shop = model(DesignatedShopModel::class)->find($dsIdx);
|
||||||
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
$kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
|
$detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, (string) $bagCode, $lgIdx) : null;
|
||||||
$price = model(BagPriceModel::class)->latestActiveByBagCode($lgIdx, (string) $bagCode);
|
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $bagCode)->where('bp_state', 1)->first();
|
||||||
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
||||||
|
|
||||||
$actualQty = ($type === 'return') ? -$qty : $qty;
|
$actualQty = ($type === 'return') ? -$qty : $qty;
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ class Company extends BaseController
|
|||||||
{
|
{
|
||||||
private CompanyModel $model;
|
private CompanyModel $model;
|
||||||
|
|
||||||
private function companyTypeOptions(): array
|
|
||||||
{
|
|
||||||
return ['협회', '제작업체', '회수업체'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->model = model(CompanyModel::class);
|
$this->model = model(CompanyModel::class);
|
||||||
@@ -27,29 +22,10 @@ class Company extends BaseController
|
|||||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$companyType = trim((string) ($this->request->getGet('cp_type') ?? ''));
|
$list = $this->model->where('cp_lg_idx', $lgIdx)->orderBy('cp_idx', 'DESC')->paginate(20);
|
||||||
$typeOptions = $this->companyTypeOptions();
|
|
||||||
|
|
||||||
$builder = $this->model->where('cp_lg_idx', $lgIdx);
|
|
||||||
if ($companyType !== '' && in_array($companyType, $typeOptions, true)) {
|
|
||||||
$builder->where('cp_type', $companyType);
|
|
||||||
}
|
|
||||||
|
|
||||||
$list = $builder->orderBy('cp_idx', 'DESC')->paginate(20);
|
|
||||||
$pager = $this->model->pager;
|
$pager = $this->model->pager;
|
||||||
|
|
||||||
$queryForPager = [];
|
return $this->renderWorkPage('업체 관리', 'admin/company/index', ['list' => $list, 'pager' => $pager]);
|
||||||
if ($companyType !== '' && in_array($companyType, $typeOptions, true)) {
|
|
||||||
$queryForPager['cp_type'] = $companyType;
|
|
||||||
}
|
|
||||||
apply_pager_path($pager, mgmt_path('companies'), $queryForPager);
|
|
||||||
|
|
||||||
return $this->renderWorkPage('업체 관리', 'admin/company/index', [
|
|
||||||
'list' => $list,
|
|
||||||
'pager' => $pager,
|
|
||||||
'cpType' => $companyType,
|
|
||||||
'typeOptions' => $typeOptions,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
|
|||||||
@@ -40,10 +40,7 @@ class Dashboard extends BaseController
|
|||||||
FROM bag_order_item GROUP BY boi_bo_idx
|
FROM bag_order_item GROUP BY boi_bo_idx
|
||||||
) sub ON sub.boi_bo_idx = bo.bo_idx
|
) sub ON sub.boi_bo_idx = bo.bo_idx
|
||||||
WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
|
WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
|
||||||
AND (bo.bo_uuid, bo.bo_version) IN (
|
", [$lgIdx])->getRow();
|
||||||
SELECT bo_uuid, MAX(bo_version) FROM bag_order WHERE bo_lg_idx = ? GROUP BY bo_uuid
|
|
||||||
)
|
|
||||||
", [$lgIdx, $lgIdx])->getRow();
|
|
||||||
$stats['order_count'] = (int) ($orderStats->cnt ?? 0);
|
$stats['order_count'] = (int) ($orderStats->cnt ?? 0);
|
||||||
$stats['order_amount'] = (int) ($orderStats->total_amount ?? 0);
|
$stats['order_amount'] = (int) ($orderStats->total_amount ?? 0);
|
||||||
|
|
||||||
@@ -75,12 +72,9 @@ class Dashboard extends BaseController
|
|||||||
SELECT bo_idx, bo_lot_no, bo_order_date, bo_status
|
SELECT bo_idx, bo_lot_no, bo_order_date, bo_status
|
||||||
FROM bag_order
|
FROM bag_order
|
||||||
WHERE bo_lg_idx = ?
|
WHERE bo_lg_idx = ?
|
||||||
AND (bo_uuid, bo_version) IN (
|
|
||||||
SELECT bo_uuid, MAX(bo_version) FROM bag_order WHERE bo_lg_idx = ? GROUP BY bo_uuid
|
|
||||||
)
|
|
||||||
ORDER BY bo_order_date DESC, bo_idx DESC
|
ORDER BY bo_order_date DESC, bo_idx DESC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
", [$lgIdx, $lgIdx])->getResult();
|
", [$lgIdx])->getResult();
|
||||||
|
|
||||||
// 최근 판매 5건
|
// 최근 판매 5건
|
||||||
$stats['recent_sales'] = $db->query("
|
$stats['recent_sales'] = $db->query("
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,19 +16,6 @@ class FreeRecipient extends BaseController
|
|||||||
$this->model = model(FreeRecipientModel::class);
|
$this->model = model(FreeRecipientModel::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 무료용 대상 구분(스크린샷 기준): 사람뿐 아니라 동사무소 자체도 등록 가능.
|
|
||||||
*
|
|
||||||
* @return array<string,string>
|
|
||||||
*/
|
|
||||||
private function recipientTypeOptions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'office' => '읍.면.동 사무소',
|
|
||||||
'target' => '무료 대상자',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getCodeOptions(string $ckCode): array
|
private function getCodeOptions(string $ckCode): array
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
@@ -46,42 +33,16 @@ class FreeRecipient extends BaseController
|
|||||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = $this->model
|
$list = $this->model->where('fr_lg_idx', $lgIdx)->orderBy('fr_idx', 'DESC')->paginate(20);
|
||||||
->where('fr_lg_idx', $lgIdx)
|
|
||||||
->orderBy('fr_type_code', 'ASC')
|
|
||||||
->orderBy('fr_name', 'ASC')
|
|
||||||
->orderBy('fr_idx', 'DESC')
|
|
||||||
->paginate(20);
|
|
||||||
$pager = $this->model->pager;
|
$pager = $this->model->pager;
|
||||||
$perPage = 20;
|
|
||||||
$currentPage = (int) ($pager->getCurrentPage() ?: 1);
|
|
||||||
$totalCount = (int) $this->model
|
|
||||||
->where('fr_lg_idx', $lgIdx)
|
|
||||||
->countAllResults();
|
|
||||||
$dongNameMap = [];
|
|
||||||
foreach ($this->getCodeOptions('D') as $dong) {
|
|
||||||
$code = (string) ($dong->cd_code ?? '');
|
|
||||||
if ($code === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$dongNameMap[$code] = (string) ($dong->cd_name ?? $code);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->renderWorkPage('무료용 대상자 관리', 'admin/free_recipient/index', [
|
return $this->renderWorkPage('무료용 대상자 관리', 'admin/free_recipient/index', ['list' => $list, 'pager' => $pager]);
|
||||||
'list' => $list,
|
|
||||||
'pager' => $pager,
|
|
||||||
'recipientTypeOptions' => $this->recipientTypeOptions(),
|
|
||||||
'dongNameMap' => $dongNameMap,
|
|
||||||
'totalCount' => $totalCount,
|
|
||||||
'currentPage' => $currentPage,
|
|
||||||
'perPage' => $perPage,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
return $this->renderWorkPage('무료용 대상자 등록', 'admin/free_recipient/create', [
|
return $this->renderWorkPage('무료용 대상자 등록', 'admin/free_recipient/create', [
|
||||||
'recipientTypeOptions' => $this->recipientTypeOptions(),
|
'typeCodes' => $this->getCodeOptions('H'),
|
||||||
'dongCodes' => $this->getCodeOptions('D'),
|
'dongCodes' => $this->getCodeOptions('D'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -124,7 +85,7 @@ class FreeRecipient extends BaseController
|
|||||||
|
|
||||||
return $this->renderWorkPage('무료용 대상자 수정', 'admin/free_recipient/edit', [
|
return $this->renderWorkPage('무료용 대상자 수정', 'admin/free_recipient/edit', [
|
||||||
'item' => $item,
|
'item' => $item,
|
||||||
'recipientTypeOptions' => $this->recipientTypeOptions(),
|
'typeCodes' => $this->getCodeOptions('H'),
|
||||||
'dongCodes' => $this->getCodeOptions('D'),
|
'dongCodes' => $this->getCodeOptions('D'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,15 +25,6 @@ class Manager extends BaseController
|
|||||||
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
return $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function managerCategoryOptions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'company' => '제작업체',
|
|
||||||
'district' => '구·군',
|
|
||||||
'agency' => '대행소',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
helper('admin');
|
helper('admin');
|
||||||
@@ -44,29 +35,16 @@ class Manager extends BaseController
|
|||||||
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$category = (string) ($this->request->getGet('category') ?? '');
|
$list = $this->model->where('mg_lg_idx', $lgIdx)->orderBy('mg_idx', 'DESC')->paginate(20);
|
||||||
$categories = $this->managerCategoryOptions();
|
|
||||||
|
|
||||||
$builder = $this->model->where('mg_lg_idx', $lgIdx);
|
|
||||||
if ($category !== '' && isset($categories[$category])) {
|
|
||||||
$builder->where('mg_dept_code', $category);
|
|
||||||
}
|
|
||||||
|
|
||||||
$list = $builder->orderBy('mg_idx', 'DESC')->paginate(20);
|
|
||||||
$pager = $this->model->pager;
|
$pager = $this->model->pager;
|
||||||
|
|
||||||
return $this->renderWorkPage('담당자 관리', 'admin/manager/index', [
|
return $this->renderWorkPage('담당자 관리', 'admin/manager/index', ['list' => $list, 'pager' => $pager]);
|
||||||
'list' => $list,
|
|
||||||
'pager' => $pager,
|
|
||||||
'categories' => $categories,
|
|
||||||
'category' => $category,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
return $this->renderWorkPage('담당자 등록', 'admin/manager/create', [
|
return $this->renderWorkPage('담당자 등록', 'admin/manager/create', [
|
||||||
'categories' => $this->managerCategoryOptions(),
|
'deptCodes' => $this->getCodeOptions('S'),
|
||||||
'positionCodes' => $this->getCodeOptions('T'),
|
'positionCodes' => $this->getCodeOptions('T'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -76,7 +54,6 @@ class Manager extends BaseController
|
|||||||
helper(['admin', 'url']);
|
helper(['admin', 'url']);
|
||||||
$rules = [
|
$rules = [
|
||||||
'mg_name' => 'required|max_length[50]',
|
'mg_name' => 'required|max_length[50]',
|
||||||
'mg_category' => 'required|in_list[company,district,agency]',
|
|
||||||
'mg_tel' => 'permit_empty|max_length[20]',
|
'mg_tel' => 'permit_empty|max_length[20]',
|
||||||
'mg_phone' => 'permit_empty|max_length[20]',
|
'mg_phone' => 'permit_empty|max_length[20]',
|
||||||
'mg_email' => 'permit_empty|valid_email|max_length[100]',
|
'mg_email' => 'permit_empty|valid_email|max_length[100]',
|
||||||
@@ -88,7 +65,7 @@ class Manager extends BaseController
|
|||||||
$this->model->insert([
|
$this->model->insert([
|
||||||
'mg_lg_idx' => admin_effective_lg_idx(),
|
'mg_lg_idx' => admin_effective_lg_idx(),
|
||||||
'mg_name' => $this->request->getPost('mg_name'),
|
'mg_name' => $this->request->getPost('mg_name'),
|
||||||
'mg_dept_code' => (string) ($this->request->getPost('mg_category') ?? ''),
|
'mg_dept_code' => $this->request->getPost('mg_dept_code') ?? '',
|
||||||
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
|
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
|
||||||
'mg_tel' => $this->request->getPost('mg_tel') ?? '',
|
'mg_tel' => $this->request->getPost('mg_tel') ?? '',
|
||||||
'mg_phone' => $this->request->getPost('mg_phone') ?? '',
|
'mg_phone' => $this->request->getPost('mg_phone') ?? '',
|
||||||
@@ -110,7 +87,7 @@ class Manager extends BaseController
|
|||||||
|
|
||||||
return $this->renderWorkPage('담당자 수정', 'admin/manager/edit', [
|
return $this->renderWorkPage('담당자 수정', 'admin/manager/edit', [
|
||||||
'item' => $item,
|
'item' => $item,
|
||||||
'categories' => $this->managerCategoryOptions(),
|
'deptCodes' => $this->getCodeOptions('S'),
|
||||||
'positionCodes' => $this->getCodeOptions('T'),
|
'positionCodes' => $this->getCodeOptions('T'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -125,7 +102,6 @@ class Manager extends BaseController
|
|||||||
|
|
||||||
$rules = [
|
$rules = [
|
||||||
'mg_name' => 'required|max_length[50]',
|
'mg_name' => 'required|max_length[50]',
|
||||||
'mg_category' => 'required|in_list[company,district,agency]',
|
|
||||||
'mg_state' => 'required|in_list[0,1]',
|
'mg_state' => 'required|in_list[0,1]',
|
||||||
];
|
];
|
||||||
if (! $this->validate($rules)) {
|
if (! $this->validate($rules)) {
|
||||||
@@ -134,7 +110,7 @@ class Manager extends BaseController
|
|||||||
|
|
||||||
$this->model->update($id, [
|
$this->model->update($id, [
|
||||||
'mg_name' => $this->request->getPost('mg_name'),
|
'mg_name' => $this->request->getPost('mg_name'),
|
||||||
'mg_dept_code' => (string) ($this->request->getPost('mg_category') ?? ''),
|
'mg_dept_code' => $this->request->getPost('mg_dept_code') ?? '',
|
||||||
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
|
'mg_position_code' => $this->request->getPost('mg_position_code') ?? '',
|
||||||
'mg_tel' => $this->request->getPost('mg_tel') ?? '',
|
'mg_tel' => $this->request->getPost('mg_tel') ?? '',
|
||||||
'mg_phone' => $this->request->getPost('mg_phone') ?? '',
|
'mg_phone' => $this->request->getPost('mg_phone') ?? '',
|
||||||
|
|||||||
@@ -18,29 +18,11 @@ class Menu extends BaseController
|
|||||||
$this->typeModel = model(MenuTypeModel::class);
|
$this->typeModel = model(MenuTypeModel::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 메뉴 등록·수정·삭제·순서변경 후 항상 같은 메뉴 관리 화면(mt_idx 유지)으로 돌아간다.
|
|
||||||
* redirect()->back() 은 목록의 새 탭(target="_blank") 링크 클릭으로 세션 직전 URL(_ci_previous_url)이
|
|
||||||
* 메뉴 대상 페이지로 덮어써지면 그 페이지로 이탈하므로, 명시적으로 메뉴 화면 URL 을 사용한다.
|
|
||||||
*/
|
|
||||||
private function menusRedirect(int $mtIdx): \CodeIgniter\HTTP\RedirectResponse
|
|
||||||
{
|
|
||||||
$url = base_url('admin/menus');
|
|
||||||
if ($mtIdx > 0) {
|
|
||||||
$url .= '?mt_idx=' . $mtIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->to($url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 관리 화면 (목록 + 등록/수정 폼). 지자체별 메뉴만 조회·관리.
|
* 메뉴 관리 화면 (목록 + 등록/수정 폼). 지자체별 메뉴만 조회·관리.
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
if ($deny = $this->denyUnlessLevel4Plus()) {
|
|
||||||
return $deny;
|
|
||||||
}
|
|
||||||
helper('admin');
|
helper('admin');
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null) {
|
if ($lgIdx === null) {
|
||||||
@@ -78,11 +60,6 @@ class Menu extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($effectiveMtIdx > 0 && $currentTypeCode === 'site') {
|
|
||||||
$this->menuModel->pruneInventoryManagementMenus($effectiveMtIdx, $lgIdx);
|
|
||||||
$list = $this->menuModel->getAllByType($effectiveMtIdx, $lgIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 트리 순서대로 상위 메뉴 바로 아래에 하위 메뉴가 오도록 평탄화
|
// 트리 순서대로 상위 메뉴 바로 아래에 하위 메뉴가 오도록 평탄화
|
||||||
if (! empty($list)) {
|
if (! empty($list)) {
|
||||||
$tree = build_menu_tree($list);
|
$tree = build_menu_tree($list);
|
||||||
@@ -116,9 +93,6 @@ class Menu extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function list()
|
public function list()
|
||||||
{
|
{
|
||||||
if ($deny = $this->denyUnlessLevel4Plus(true)) {
|
|
||||||
return $deny;
|
|
||||||
}
|
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null) {
|
if ($lgIdx === null) {
|
||||||
return $this->response->setJSON(['status' => 0, 'msg' => '지자체를 선택하세요.']);
|
return $this->response->setJSON(['status' => 0, 'msg' => '지자체를 선택하세요.']);
|
||||||
@@ -129,10 +103,6 @@ class Menu extends BaseController
|
|||||||
if ($mtIdx <= 0) {
|
if ($mtIdx <= 0) {
|
||||||
return $this->response->setJSON(['status' => 0, 'msg' => 'mt_idx required']);
|
return $this->response->setJSON(['status' => 0, 'msg' => 'mt_idx required']);
|
||||||
}
|
}
|
||||||
$type = $this->typeModel->find($mtIdx);
|
|
||||||
if ($type && (string) ($type->mt_code ?? '') === 'site') {
|
|
||||||
$this->menuModel->pruneInventoryManagementMenus($mtIdx, $lgIdx);
|
|
||||||
}
|
|
||||||
$list = $this->menuModel->getAllByType($mtIdx, $lgIdx);
|
$list = $this->menuModel->getAllByType($mtIdx, $lgIdx);
|
||||||
return $this->response->setJSON(['status' => 1, 'data' => $list]);
|
return $this->response->setJSON(['status' => 1, 'data' => $list]);
|
||||||
}
|
}
|
||||||
@@ -142,9 +112,6 @@ class Menu extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function store()
|
public function store()
|
||||||
{
|
{
|
||||||
if ($deny = $this->denyUnlessLevel4Plus()) {
|
|
||||||
return $deny;
|
|
||||||
}
|
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null) {
|
if ($lgIdx === null) {
|
||||||
return redirect()->to(base_url('admin/select-local-government'))
|
return redirect()->to(base_url('admin/select-local-government'))
|
||||||
@@ -155,10 +122,10 @@ class Menu extends BaseController
|
|||||||
$mmDep = (int) $this->request->getPost('mm_dep');
|
$mmDep = (int) $this->request->getPost('mm_dep');
|
||||||
$mmName = trim((string) $this->request->getPost('mm_name'));
|
$mmName = trim((string) $this->request->getPost('mm_name'));
|
||||||
if ($mtIdx <= 0) {
|
if ($mtIdx <= 0) {
|
||||||
return $this->menusRedirect($mtIdx)->with('error', '메뉴 종류를 선택하세요.');
|
return redirect()->back()->with('error', '메뉴 종류를 선택하세요.');
|
||||||
}
|
}
|
||||||
if ($mmName === '') {
|
if ($mmName === '') {
|
||||||
return $this->menusRedirect($mtIdx)->with('error', '메뉴명을 입력하세요.');
|
return redirect()->back()->with('error', '메뉴명을 입력하세요.');
|
||||||
}
|
}
|
||||||
$mmNum = $this->menuModel->getNextNum($mtIdx, $lgIdx, $mmPidx, $mmDep);
|
$mmNum = $this->menuModel->getNextNum($mtIdx, $lgIdx, $mmPidx, $mmDep);
|
||||||
$data = [
|
$data = [
|
||||||
@@ -177,9 +144,7 @@ class Menu extends BaseController
|
|||||||
if ($mmPidx > 0) {
|
if ($mmPidx > 0) {
|
||||||
$this->menuModel->updateCnode($mmPidx, 1);
|
$this->menuModel->updateCnode($mmPidx, 1);
|
||||||
}
|
}
|
||||||
$this->menuModel->pruneInventoryManagementMenus($mtIdx, $lgIdx);
|
return redirect()->back()->with('success', '메뉴가 등록되었습니다.');
|
||||||
$this->menuModel->syncTypeToAllLgs($mtIdx, $lgIdx);
|
|
||||||
return $this->menusRedirect($mtIdx)->with('success', '메뉴가 등록되었습니다.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,9 +152,6 @@ class Menu extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function update(int $id)
|
public function update(int $id)
|
||||||
{
|
{
|
||||||
if ($deny = $this->denyUnlessLevel4Plus()) {
|
|
||||||
return $deny;
|
|
||||||
}
|
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null) {
|
if ($lgIdx === null) {
|
||||||
return redirect()->to(base_url('admin/select-local-government'))
|
return redirect()->to(base_url('admin/select-local-government'))
|
||||||
@@ -197,12 +159,10 @@ class Menu extends BaseController
|
|||||||
}
|
}
|
||||||
$row = $this->menuModel->find($id);
|
$row = $this->menuModel->find($id);
|
||||||
if (! $row) {
|
if (! $row) {
|
||||||
return $this->menusRedirect((int) $this->request->getPost('mt_idx'))
|
return redirect()->back()->with('error', '메뉴를 찾을 수 없습니다.');
|
||||||
->with('error', '메뉴를 찾을 수 없습니다.');
|
|
||||||
}
|
}
|
||||||
if ((int) $row->lg_idx !== $lgIdx) {
|
if ((int) $row->lg_idx !== $lgIdx) {
|
||||||
return $this->menusRedirect((int) $row->mt_idx)
|
return redirect()->back()->with('error', '해당 지자체의 메뉴만 수정할 수 있습니다.');
|
||||||
->with('error', '해당 지자체의 메뉴만 수정할 수 있습니다.');
|
|
||||||
}
|
}
|
||||||
$data = [
|
$data = [
|
||||||
'mm_name' => (string) $this->request->getPost('mm_name'),
|
'mm_name' => (string) $this->request->getPost('mm_name'),
|
||||||
@@ -211,9 +171,7 @@ class Menu extends BaseController
|
|||||||
'mm_is_view' => $this->request->getPost('mm_is_view') ? 'Y' : 'N',
|
'mm_is_view' => $this->request->getPost('mm_is_view') ? 'Y' : 'N',
|
||||||
];
|
];
|
||||||
$this->menuModel->update($id, $data);
|
$this->menuModel->update($id, $data);
|
||||||
$this->menuModel->pruneInventoryManagementMenus((int) $row->mt_idx, $lgIdx);
|
return redirect()->back()->with('success', '메뉴가 수정되었습니다.');
|
||||||
$this->menuModel->syncTypeToAllLgs((int) $row->mt_idx, $lgIdx);
|
|
||||||
return $this->menusRedirect((int) $row->mt_idx)->with('success', '메뉴가 수정되었습니다.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,9 +179,6 @@ class Menu extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function delete(int $id)
|
public function delete(int $id)
|
||||||
{
|
{
|
||||||
if ($deny = $this->denyUnlessLevel4Plus()) {
|
|
||||||
return $deny;
|
|
||||||
}
|
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null) {
|
if ($lgIdx === null) {
|
||||||
return redirect()->to(base_url('admin/select-local-government'))
|
return redirect()->to(base_url('admin/select-local-government'))
|
||||||
@@ -231,16 +186,13 @@ class Menu extends BaseController
|
|||||||
}
|
}
|
||||||
$row = $this->menuModel->find($id);
|
$row = $this->menuModel->find($id);
|
||||||
if (! $row || (int) $row->lg_idx !== $lgIdx) {
|
if (! $row || (int) $row->lg_idx !== $lgIdx) {
|
||||||
return $this->menusRedirect((int) $this->request->getPost('mt_idx'))
|
return redirect()->back()->with('error', '해당 지자체의 메뉴만 삭제할 수 있습니다.');
|
||||||
->with('error', '해당 지자체의 메뉴만 삭제할 수 있습니다.');
|
|
||||||
}
|
}
|
||||||
$result = $this->menuModel->deleteSafe($id);
|
$result = $this->menuModel->deleteSafe($id);
|
||||||
if ($result['ok']) {
|
if ($result['ok']) {
|
||||||
$this->menuModel->pruneInventoryManagementMenus((int) $row->mt_idx, $lgIdx);
|
return redirect()->back()->with('success', '메뉴가 삭제되었습니다.');
|
||||||
$this->menuModel->syncTypeToAllLgs((int) $row->mt_idx, $lgIdx);
|
|
||||||
return $this->menusRedirect((int) $row->mt_idx)->with('success', '메뉴가 삭제되었습니다.');
|
|
||||||
}
|
}
|
||||||
return $this->menusRedirect((int) $row->mt_idx)->with('error', $result['msg']);
|
return redirect()->back()->with('error', $result['msg']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,28 +200,17 @@ class Menu extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function move()
|
public function move()
|
||||||
{
|
{
|
||||||
if ($deny = $this->denyUnlessLevel4Plus()) {
|
|
||||||
return $deny;
|
|
||||||
}
|
|
||||||
$lgIdx = admin_effective_lg_idx();
|
$lgIdx = admin_effective_lg_idx();
|
||||||
if ($lgIdx === null) {
|
if ($lgIdx === null) {
|
||||||
return redirect()->to(base_url('admin/select-local-government'))
|
return redirect()->to(base_url('admin/select-local-government'))
|
||||||
->with('error', '지자체를 선택하세요.');
|
->with('error', '지자체를 선택하세요.');
|
||||||
}
|
}
|
||||||
$ids = $this->request->getPost('mm_idx');
|
$ids = $this->request->getPost('mm_idx');
|
||||||
$postMtIdx = (int) $this->request->getPost('mt_idx');
|
|
||||||
if (! is_array($ids) || empty($ids)) {
|
if (! is_array($ids) || empty($ids)) {
|
||||||
return $this->menusRedirect($postMtIdx)->with('error', '순서를 적용할 메뉴가 없습니다.');
|
return redirect()->back()->with('error', '순서를 적용할 메뉴가 없습니다.');
|
||||||
}
|
}
|
||||||
$firstId = (int) ($ids[0] ?? 0);
|
|
||||||
$firstRow = $firstId > 0 ? $this->menuModel->find($firstId) : null;
|
|
||||||
$this->menuModel->setOrder($ids, $lgIdx);
|
$this->menuModel->setOrder($ids, $lgIdx);
|
||||||
if ($firstRow && (int) $firstRow->lg_idx === $lgIdx) {
|
return redirect()->back()->with('success', '순서가 적용되었습니다.');
|
||||||
$this->menuModel->pruneInventoryManagementMenus((int) $firstRow->mt_idx, $lgIdx);
|
|
||||||
$this->menuModel->syncTypeToAllLgs((int) $firstRow->mt_idx, $lgIdx);
|
|
||||||
}
|
|
||||||
$mtIdx = $firstRow ? (int) $firstRow->mt_idx : $postMtIdx;
|
|
||||||
return $this->menusRedirect($mtIdx)->with('success', '순서가 적용되었습니다.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -325,27 +266,4 @@ class Menu extends BaseController
|
|||||||
|
|
||||||
return (int) $types[0]->mt_idx;
|
return (int) $types[0]->mt_idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 메뉴 관리는 레벨4 이상(슈퍼/본부 관리자)만 허용.
|
|
||||||
*
|
|
||||||
* @return \CodeIgniter\HTTP\RedirectResponse|\CodeIgniter\HTTP\ResponseInterface|null
|
|
||||||
*/
|
|
||||||
private function denyUnlessLevel4Plus(bool $json = false)
|
|
||||||
{
|
|
||||||
$level = (int) session()->get('mb_level');
|
|
||||||
if (Roles::isSuperAdminEquivalent($level)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($json) {
|
|
||||||
return $this->response->setJSON([
|
|
||||||
'status' => 0,
|
|
||||||
'msg' => '메뉴 관리는 레벨4 이상만 접근할 수 있습니다.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->to(base_url('admin'))
|
|
||||||
->with('error', '메뉴 관리는 레벨4 이상만 접근할 수 있습니다.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,31 +143,14 @@ class PackagingUnit extends BaseController
|
|||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$db->transStart();
|
$db->transStart();
|
||||||
|
|
||||||
$trackFields = ['pu_box_per_pack', 'pu_pack_per_sheet', 'pu_start_date', 'pu_end_date', 'pu_state'];
|
$trackFields = ['pu_box_per_pack', 'pu_pack_per_sheet'];
|
||||||
$fieldLabels = [
|
|
||||||
'pu_box_per_pack' => '박스당 팩 수',
|
|
||||||
'pu_pack_per_sheet' => '팩당 낱장 수',
|
|
||||||
'pu_start_date' => '적용시작일',
|
|
||||||
'pu_end_date' => '적용종료일',
|
|
||||||
'pu_state' => '상태',
|
|
||||||
];
|
|
||||||
foreach ($trackFields as $field) {
|
foreach ($trackFields as $field) {
|
||||||
$oldRaw = $item->$field;
|
$oldVal = (string) $item->$field;
|
||||||
$newRaw = $this->request->getPost($field);
|
$newVal = (string) $this->request->getPost($field);
|
||||||
if ($field === 'pu_end_date') {
|
|
||||||
$oldRaw = $oldRaw ?: '';
|
|
||||||
$newRaw = $newRaw ?: '';
|
|
||||||
}
|
|
||||||
if ($field === 'pu_state') {
|
|
||||||
$oldRaw = (int) $oldRaw === 1 ? '사용' : '미사용';
|
|
||||||
$newRaw = (int) $newRaw === 1 ? '사용' : '미사용';
|
|
||||||
}
|
|
||||||
$oldVal = (string) $oldRaw;
|
|
||||||
$newVal = (string) $newRaw;
|
|
||||||
if ($oldVal !== $newVal) {
|
if ($oldVal !== $newVal) {
|
||||||
$this->historyModel->insert([
|
$this->historyModel->insert([
|
||||||
'puh_pu_idx' => $id,
|
'puh_pu_idx' => $id,
|
||||||
'puh_field' => $fieldLabels[$field] ?? $field,
|
'puh_field' => $field,
|
||||||
'puh_old_value' => $oldVal,
|
'puh_old_value' => $oldVal,
|
||||||
'puh_new_value' => $newVal,
|
'puh_new_value' => $newVal,
|
||||||
'puh_changed_at' => date('Y-m-d H:i:s'),
|
'puh_changed_at' => date('Y-m-d H:i:s'),
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ class SalesAgency extends BaseController
|
|||||||
'sa_idx' => $saIdx,
|
'sa_idx' => $saIdx,
|
||||||
];
|
];
|
||||||
$queryForPager = array_filter($queryForPager, static fn ($v) => $v !== null && $v !== '');
|
$queryForPager = array_filter($queryForPager, static fn ($v) => $v !== null && $v !== '');
|
||||||
apply_pager_path($pager, mgmt_path('sales-agencies'), $queryForPager);
|
$pagerPath = mgmt_url('sales-agencies');
|
||||||
|
if ($queryForPager !== []) {
|
||||||
|
$pagerPath .= '?' . http_build_query($queryForPager);
|
||||||
|
}
|
||||||
|
$pager->setPath($pagerPath);
|
||||||
|
|
||||||
return $this->renderWorkPage('판매 대행소 관리', 'admin/sales_agency/index', [
|
return $this->renderWorkPage('판매 대행소 관리', 'admin/sales_agency/index', [
|
||||||
'list' => $list,
|
'list' => $list,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -57,21 +57,8 @@ class ShopOrder extends BaseController
|
|||||||
$shops = model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll();
|
$shops = model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll();
|
||||||
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
$kind = model(CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||||
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
$bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : [];
|
||||||
$priceMap = model(BagPriceModel::class)->latestActiveMapByBagCode($lgIdx);
|
|
||||||
$unitRows = model(PackagingUnitModel::class)
|
|
||||||
->where('pu_lg_idx', $lgIdx)
|
|
||||||
->where('pu_state', 1)
|
|
||||||
->findAll();
|
|
||||||
$unitMap = [];
|
|
||||||
foreach ($unitRows as $unit) {
|
|
||||||
$code = (string) ($unit->pu_bag_code ?? '');
|
|
||||||
if ($code === '' || isset($unitMap[$code])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$unitMap[$code] = $unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->renderWorkPage('주문 접수', 'admin/shop_order/create', compact('shops', 'bagCodes', 'priceMap', 'unitMap'));
|
return $this->renderWorkPage('주문 접수', 'admin/shop_order/create', compact('shops', 'bagCodes'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store()
|
public function store()
|
||||||
@@ -94,7 +81,7 @@ class ShopOrder extends BaseController
|
|||||||
$dsIdx = (int) $this->request->getPost('so_ds_idx');
|
$dsIdx = (int) $this->request->getPost('so_ds_idx');
|
||||||
$shop = model(DesignatedShopModel::class)->find($dsIdx);
|
$shop = model(DesignatedShopModel::class)->find($dsIdx);
|
||||||
|
|
||||||
$orderData = [
|
$this->orderModel->insert([
|
||||||
'so_lg_idx' => $lgIdx,
|
'so_lg_idx' => $lgIdx,
|
||||||
'so_ds_idx' => $dsIdx,
|
'so_ds_idx' => $dsIdx,
|
||||||
'so_ds_name' => $shop ? $shop->ds_name : '',
|
'so_ds_name' => $shop ? $shop->ds_name : '',
|
||||||
@@ -104,24 +91,8 @@ class ShopOrder extends BaseController
|
|||||||
'so_status' => 'normal',
|
'so_status' => 'normal',
|
||||||
'so_orderer_idx' => session()->get('mb_idx'),
|
'so_orderer_idx' => session()->get('mb_idx'),
|
||||||
'so_regdate' => date('Y-m-d H:i:s'),
|
'so_regdate' => date('Y-m-d H:i:s'),
|
||||||
];
|
]);
|
||||||
// shop_order.so_channel 이 아직 반영되지 않은 DB와의 호환 처리
|
|
||||||
if ($db->fieldExists('so_channel', 'shop_order')) {
|
|
||||||
$orderData['so_channel'] = 'phone';
|
|
||||||
}
|
|
||||||
|
|
||||||
$insertOk = $this->orderModel->insert($orderData);
|
|
||||||
if ($insertOk === false) {
|
|
||||||
$db->transRollback();
|
|
||||||
$errors = $this->orderModel->errors();
|
|
||||||
$msg = ! empty($errors) ? implode(' / ', array_values($errors)) : '주문 저장에 실패했습니다.';
|
|
||||||
return redirect()->back()->withInput()->with('error', $msg);
|
|
||||||
}
|
|
||||||
$soIdx = (int) $this->orderModel->getInsertID();
|
$soIdx = (int) $this->orderModel->getInsertID();
|
||||||
if ($soIdx <= 0) {
|
|
||||||
$db->transRollback();
|
|
||||||
return redirect()->back()->withInput()->with('error', '주문번호 생성에 실패했습니다. DB 스키마를 확인해 주세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$bagCodes = $this->request->getPost('item_bag_code') ?? [];
|
$bagCodes = $this->request->getPost('item_bag_code') ?? [];
|
||||||
$qtys = $this->request->getPost('item_qty') ?? [];
|
$qtys = $this->request->getPost('item_qty') ?? [];
|
||||||
@@ -134,7 +105,7 @@ class ShopOrder extends BaseController
|
|||||||
}
|
}
|
||||||
$qty = (int) $qtys[$i];
|
$qty = (int) $qtys[$i];
|
||||||
|
|
||||||
$price = model(BagPriceModel::class)->latestActiveByBagCode($lgIdx, (string) $code);
|
$price = model(BagPriceModel::class)->where('bp_lg_idx', $lgIdx)->where('bp_bag_code', $code)->where('bp_state', 1)->first();
|
||||||
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
$unitPrice = $price ? (float) $price->bp_consumer : 0;
|
||||||
$amount = $unitPrice * $qty;
|
$amount = $unitPrice * $qty;
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ class Auth extends BaseController
|
|||||||
return redirect()->to('/');
|
return redirect()->to('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('auth/login', [
|
return view('auth/login');
|
||||||
'pageTitle' => '로그인 - 종량제 시스템',
|
|
||||||
'cardMax' => 'max-w-md',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function login()
|
public function login()
|
||||||
@@ -160,8 +157,6 @@ class Auth extends BaseController
|
|||||||
|
|
||||||
return view('auth/login_two_factor', [
|
return view('auth/login_two_factor', [
|
||||||
'memberId' => $member->mb_id,
|
'memberId' => $member->mb_id,
|
||||||
'pageTitle' => '2차 인증 - 종량제 시스템',
|
|
||||||
'cardMax' => 'max-w-md',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,8 +239,6 @@ class Auth extends BaseController
|
|||||||
'memberId' => $member->mb_id,
|
'memberId' => $member->mb_id,
|
||||||
'qrDataUri' => $qrDataUri,
|
'qrDataUri' => $qrDataUri,
|
||||||
'secret' => $secret,
|
'secret' => $secret,
|
||||||
'pageTitle' => '2차 인증 등록 - 종량제 시스템',
|
|
||||||
'cardMax' => 'max-w-lg',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,8 +341,6 @@ class Auth extends BaseController
|
|||||||
|
|
||||||
return view('auth/register', [
|
return view('auth/register', [
|
||||||
'localGovernments' => $localGovernments,
|
'localGovernments' => $localGovernments,
|
||||||
'pageTitle' => '회원가입 - 종량제 시스템',
|
|
||||||
'cardMax' => 'max-w-md',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -51,18 +51,14 @@ abstract class BaseController extends Controller
|
|||||||
protected function renderWorkPage(string $title, string $contentView, array $contentData = []): string
|
protected function renderWorkPage(string $title, string $contentView, array $contentData = []): string
|
||||||
{
|
{
|
||||||
$content = view($contentView, $contentData);
|
$content = view($contentView, $contentData);
|
||||||
helper('admin');
|
|
||||||
$path = function_exists('current_nav_request_path') ? current_nav_request_path() : '';
|
|
||||||
if ($path === '') {
|
|
||||||
$uri = service('request')->getUri();
|
$uri = service('request')->getUri();
|
||||||
$path = trim((string) $uri->getPath(), '/');
|
$seg1 = $uri->getSegment(1);
|
||||||
}
|
$seg2 = $uri->getSegment(2);
|
||||||
while (str_starts_with($path, 'index.php/')) {
|
|
||||||
$path = substr($path, strlen('index.php/'));
|
// 지정판매소 관리는 관리자 전용 기능으로, /bag 경로여도 관리자 레이아웃을 유지한다.
|
||||||
}
|
$forceAdminLayoutOnBag = ($seg1 === 'bag' && $seg2 === 'designated-shops');
|
||||||
if ($path === 'bag' || str_starts_with($path, 'bag/')) {
|
if ($seg1 === 'bag' && ! $forceAdminLayoutOnBag) {
|
||||||
// 사이트 업무 페이지: gov-portal 디자인 셸 적용
|
return view('bag/layout/main', [
|
||||||
return view('bag/layout/portal', [
|
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'content' => $content,
|
'content' => $content,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use App\Libraries\GovPortalCodeKindsPage;
|
|
||||||
use App\Models\LocalGovernmentModel;
|
use App\Models\LocalGovernmentModel;
|
||||||
|
|
||||||
class Home extends BaseController
|
class Home extends BaseController
|
||||||
@@ -10,141 +9,12 @@ class Home extends BaseController
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
if (session()->get('logged_in')) {
|
if (session()->get('logged_in')) {
|
||||||
// 메인(/) — gov-portal 디자인 셸 + 종량제 실데이터 대시보드.
|
return $this->dashboard();
|
||||||
helper('admin');
|
|
||||||
|
|
||||||
return view('bag/layout/portal', [
|
|
||||||
'title' => '업무 현황',
|
|
||||||
'bare' => true,
|
|
||||||
'content' => view('bag/dashboard_portal', $this->portalDashboardData()),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('welcome_message');
|
return view('welcome_message');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 메인 대시보드용 — 종량제 시스템에 실제 존재하는 데이터만 집계.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function portalDashboardData(): array
|
|
||||||
{
|
|
||||||
helper('admin');
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
$lgIdx = function_exists('admin_effective_lg_idx') ? admin_effective_lg_idx() : null;
|
|
||||||
if ($lgIdx === null) {
|
|
||||||
$raw = session()->get('mb_lg_idx');
|
|
||||||
$lgIdx = ($raw !== null && $raw !== '') ? (int) $raw : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$inventory = [];
|
|
||||||
$totalQty = 0;
|
|
||||||
$orderCount = 0;
|
|
||||||
$palette = ['#3b82f6', '#10b981', '#f59e0b', '#6366f1', '#ef4444', '#0ea5e9', '#14b8a6', '#a855f7', '#f97316'];
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($lgIdx !== null && $db->tableExists('bag_inventory')) {
|
|
||||||
$rows = $db->table('bag_inventory')
|
|
||||||
->select('bi_bag_name, bi_bag_code, bi_qty')
|
|
||||||
->where('bi_lg_idx', $lgIdx)
|
|
||||||
->orderBy('bi_qty', 'DESC')
|
|
||||||
->get()->getResultArray();
|
|
||||||
foreach ($rows as $r) {
|
|
||||||
$inventory[] = [
|
|
||||||
'name' => (string) ($r['bi_bag_name'] ?? $r['bi_bag_code'] ?? ''),
|
|
||||||
'qty' => (int) ($r['bi_qty'] ?? 0),
|
|
||||||
];
|
|
||||||
$totalQty += (int) ($r['bi_qty'] ?? 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$inventory = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 재고 구성(상위 품목 비율)
|
|
||||||
$stockMix = [];
|
|
||||||
foreach (array_slice($inventory, 0, 6) as $i => $item) {
|
|
||||||
$stockMix[] = [
|
|
||||||
'name' => $item['name'],
|
|
||||||
'value' => $totalQty > 0 ? (int) round($item['qty'] / $totalQty * 100) : 0,
|
|
||||||
'color' => $palette[$i % count($palette)],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
// 부족 재고(수량 적은 하위 품목) — 최대 재고 대비 비율
|
|
||||||
$maxQty = $inventory !== [] ? max(array_column($inventory, 'qty')) : 0;
|
|
||||||
$lowStock = [];
|
|
||||||
foreach (array_slice(array_reverse($inventory), 0, 5) as $item) {
|
|
||||||
$lowStock[] = [
|
|
||||||
'name' => $item['name'],
|
|
||||||
'qty' => $item['qty'],
|
|
||||||
'percent' => $maxQty > 0 ? (int) round($item['qty'] / $maxQty * 100) : 0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($lgIdx !== null && $db->tableExists('shop_order')) {
|
|
||||||
$orderCount = (int) $db->table('shop_order')
|
|
||||||
->where('so_lg_idx', $lgIdx)
|
|
||||||
->where('so_status', 'normal')
|
|
||||||
->countAllResults();
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$orderCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pendingApprovals = 0;
|
|
||||||
try {
|
|
||||||
if ($db->tableExists('member_approval_request')) {
|
|
||||||
$pendingApprovals = (int) $db->table('member_approval_request')
|
|
||||||
->where('mar_status', 'pending')
|
|
||||||
->countAllResults();
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$pendingApprovals = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 최근 활동(activity_log) — 실제 변경 이력
|
|
||||||
$actionLabel = ['create' => '등록', 'update' => '수정', 'delete' => '삭제', 'cancel' => '취소'];
|
|
||||||
$tableLabel = [
|
|
||||||
'bag_order' => '발주', 'bag_receiving' => '입고', 'bag_sale' => '판매',
|
|
||||||
'bag_issue' => '불출', 'bag_inventory' => '재고', 'shop_order' => '주문접수',
|
|
||||||
'designated_shop' => '지정판매소', 'bag_price' => '단가', 'member' => '회원',
|
|
||||||
];
|
|
||||||
$recent = [];
|
|
||||||
try {
|
|
||||||
if ($db->tableExists('activity_log')) {
|
|
||||||
$logs = $db->table('activity_log')
|
|
||||||
->select('al_action, al_table, al_regdate')
|
|
||||||
->orderBy('al_idx', 'DESC')->limit(6)->get()->getResultArray();
|
|
||||||
foreach ($logs as $l) {
|
|
||||||
$t = (string) ($l['al_regdate'] ?? '');
|
|
||||||
$recent[] = [
|
|
||||||
'time' => $t !== '' ? date('m.d H:i', strtotime($t)) : '',
|
|
||||||
'text' => ($tableLabel[$l['al_table']] ?? (string) $l['al_table'])
|
|
||||||
. ' ' . ($actionLabel[$l['al_action']] ?? (string) $l['al_action']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$recent = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'lgLabel' => $this->resolveLgLabel(),
|
|
||||||
'mbName' => (string) (session()->get('mb_name') ?? '담당자'),
|
|
||||||
'mbId' => (string) (session()->get('mb_id') ?? ''),
|
|
||||||
'levelName' => config(\Config\Roles::class)->getLevelName((int) session()->get('mb_level')),
|
|
||||||
'totalQty' => $totalQty,
|
|
||||||
'itemCount' => count($inventory),
|
|
||||||
'orderCount' => $orderCount,
|
|
||||||
'pendingApprovals' => $pendingApprovals,
|
|
||||||
'stockMix' => $stockMix,
|
|
||||||
'lowStock' => $lowStock,
|
|
||||||
'recentActivity' => $recent,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그인 후 메인 — site 메뉴 레이아웃 + 종합·그래프(blend) 본문
|
* 로그인 후 메인 — site 메뉴 레이아웃 + 종합·그래프(blend) 본문
|
||||||
*/
|
*/
|
||||||
@@ -158,34 +28,6 @@ class Home extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 로그인 후 메인 — 단순형 요약 대시보드. URL: /dashboard/simple
|
|
||||||
* 기존 /dashboard 화면이 복잡하다는 피드백용으로, 핵심 지표·링크만 노출.
|
|
||||||
*/
|
|
||||||
public function dashboardSimple()
|
|
||||||
{
|
|
||||||
return view('bag/layout/main', [
|
|
||||||
'title' => '업무 현황 · 요약',
|
|
||||||
'content' => view('bag/lg_dashboard_simple', [
|
|
||||||
'lgLabel' => $this->resolveLgLabel(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 로그인 후 메인 — 중간 밀도 대시보드. URL: /dashboard/compact
|
|
||||||
* /dashboard 보다 단순하지만 simple 보다 정보량을 늘린 화면.
|
|
||||||
*/
|
|
||||||
public function dashboardCompact()
|
|
||||||
{
|
|
||||||
return view('bag/layout/main', [
|
|
||||||
'title' => '업무 현황 · 컴팩트',
|
|
||||||
'content' => view('bag/lg_dashboard_compact', [
|
|
||||||
'lgLabel' => $this->resolveLgLabel(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 디자인 시안(기존 /dashboard 연결 화면)
|
* 디자인 시안(기존 /dashboard 연결 화면)
|
||||||
*/
|
*/
|
||||||
@@ -232,121 +74,6 @@ class Home extends BaseController
|
|||||||
return $this->dashboard();
|
return $this->dashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 로그인 후 메인 — 라이트(축약) 대시보드. URL: /dashboard/lite
|
|
||||||
* dashboard_blend 의 일부 KPI/표/차트만 남긴 단순화 화면.
|
|
||||||
*/
|
|
||||||
public function dashboardLite()
|
|
||||||
{
|
|
||||||
return view('bag/layout/main', [
|
|
||||||
'title' => '업무 현황 · 라이트',
|
|
||||||
'content' => view('bag/dashboard_blend_lite_inner', [
|
|
||||||
'lgLabel' => $this->resolveLgLabel(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 공공 포털형(국가재난관리정보시스템 스타일) 메인 시안.
|
|
||||||
* URL: /dashboard/gov-portal — 기능은 요약 대시보드와 동일, UI만 별도 레이아웃.
|
|
||||||
*/
|
|
||||||
public function dashboardGovPortal()
|
|
||||||
{
|
|
||||||
if (! session()->get('logged_in')) {
|
|
||||||
return redirect()->to(base_url('login'));
|
|
||||||
}
|
|
||||||
|
|
||||||
helper('admin');
|
|
||||||
|
|
||||||
return view('home/dashboard_gov_portal', gov_portal_dashboard_view_data($this->resolveLgLabel(), 'base'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 공공 포털형 변형 — 가로 MY MENU·와이드 맵·KPI 띠. URL: /dashboard/gov-portal-strip
|
|
||||||
*/
|
|
||||||
public function dashboardGovPortalStrip()
|
|
||||||
{
|
|
||||||
if (! session()->get('logged_in')) {
|
|
||||||
return redirect()->to(base_url('login'));
|
|
||||||
}
|
|
||||||
|
|
||||||
helper('admin');
|
|
||||||
|
|
||||||
return view('home/_dashboard_gov_portal_strip_layout', array_merge(
|
|
||||||
gov_portal_dashboard_view_data($this->resolveLgLabel(), 'strip'),
|
|
||||||
['stripInnerView' => 'home/_dashboard_gov_portal_strip_home_inner']
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 공공 포털형(기본) — 기본 코드관리 UI 시안. URL: /dashboard/gov-portal/code-kinds
|
|
||||||
*/
|
|
||||||
public function dashboardGovPortalCodeKinds()
|
|
||||||
{
|
|
||||||
return $this->renderGovPortalCodeKinds('base');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 공공 포털형(변형 strip) — 기본 코드관리 UI 시안. URL: /dashboard/gov-portal-strip/code-kinds
|
|
||||||
*/
|
|
||||||
public function dashboardGovPortalStripCodeKinds()
|
|
||||||
{
|
|
||||||
return $this->renderGovPortalCodeKinds('strip');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return \CodeIgniter\HTTP\RedirectResponse|string
|
|
||||||
*/
|
|
||||||
private function renderGovPortalCodeKinds(string $variant)
|
|
||||||
{
|
|
||||||
if (! session()->get('logged_in')) {
|
|
||||||
return redirect()->to(base_url('login'));
|
|
||||||
}
|
|
||||||
|
|
||||||
helper('admin');
|
|
||||||
|
|
||||||
$portalPath = gov_portal_code_kinds_portal_path($variant);
|
|
||||||
$lgIdx = admin_effective_lg_idx();
|
|
||||||
$level = (int) session()->get('mb_level');
|
|
||||||
$filters = [
|
|
||||||
'q_code' => $this->request->getGet('q_code'),
|
|
||||||
'q_name' => $this->request->getGet('q_name'),
|
|
||||||
'q_state' => $this->request->getGet('q_state'),
|
|
||||||
];
|
|
||||||
$builder = new GovPortalCodeKindsPage();
|
|
||||||
$ckIdx = (int) ($this->request->getGet('ck_idx') ?? 0);
|
|
||||||
$pageData = $builder->buildPageData($lgIdx, $level, $lgIdx, $ckIdx, $filters);
|
|
||||||
|
|
||||||
if ($ckIdx === 0 && $pageData['codeKinds'] !== []) {
|
|
||||||
$pageData = $builder->buildPageData(
|
|
||||||
$lgIdx,
|
|
||||||
$level,
|
|
||||||
$lgIdx,
|
|
||||||
(int) $pageData['codeKinds'][0]->ck_idx,
|
|
||||||
$filters
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$viewData = array_merge(
|
|
||||||
gov_portal_dashboard_view_data($this->resolveLgLabel(), $variant),
|
|
||||||
$pageData,
|
|
||||||
[
|
|
||||||
'govActiveChildHref' => $portalPath,
|
|
||||||
'pageBaseUrl' => site_url($portalPath),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($variant === 'strip') {
|
|
||||||
return view('home/_dashboard_gov_portal_strip_layout', array_merge($viewData, [
|
|
||||||
'stripInnerView' => 'home/_gov_portal_code_kinds_body',
|
|
||||||
'stripIncludeWorkCss' => true,
|
|
||||||
'stripShowProfileLinks' => true,
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('home/dashboard_gov_portal_code_kinds', $viewData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 재고 조회(수불) 화면 (목업)
|
* 재고 조회(수불) 화면 (목업)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
# 시작하기 · 시스템 개요
|
|
||||||
|
|
||||||
종량제 쓰레기봉투 물류 시스템 사용자 매뉴얼입니다. 이 문서는 실제 업무를 처리하는 담당자(지자체 관리자·지정판매소)를 위한 **빠른 시작 안내서**입니다.
|
|
||||||
|
|
||||||
## 1. 시스템은 무엇을 하나요?
|
|
||||||
|
|
||||||
지자체 종량제 쓰레기봉투의 **발주 → 입고 → 재고 → 판매/불출 → 정산·통계** 전 과정을 관리합니다.
|
|
||||||
|
|
||||||
- 봉투를 제작업체에 **발주**하고, 들어온 물량을 **입고** 처리합니다.
|
|
||||||
- 현재 **재고**를 확인하고, 실사로 실제 수량과 맞춥니다.
|
|
||||||
- 지정판매소에 **판매**하거나 무료 대상자에게 **불출**합니다.
|
|
||||||
- 일계표·기간별·연간 등 **판매현황**과 **수불·통계**를 조회합니다.
|
|
||||||
|
|
||||||
## 2. 로그인과 화면 구성
|
|
||||||
|
|
||||||
1. 발급받은 아이디·비밀번호로 로그인합니다.
|
|
||||||
2. 관리자(지자체/슈퍼)는 보안을 위해 **2차 인증(OTP)** 이 적용될 수 있습니다.
|
|
||||||
3. 로그인하면 상단에 **대메뉴**가 보이고, 각 대메뉴에 마우스를 올리면 **소메뉴**가 펼쳐집니다.
|
|
||||||
4. 화면 상단의 제목 바에 현재 보고 있는 화면 이름이 표시됩니다.
|
|
||||||
|
|
||||||
> 슈퍼 관리자는 작업할 **지자체를 먼저 선택**해야 업무 화면이 열립니다.
|
|
||||||
|
|
||||||
## 3. 사용자 역할(권한)
|
|
||||||
|
|
||||||
시스템은 4단계 역할로 접근 권한을 구분합니다.
|
|
||||||
|
|
||||||
| 레벨 | 역할 | 할 수 있는 일 |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | 일반 사용자 | 기본 조회 |
|
|
||||||
| 2 | 지정판매소 | 봉투 판매·반품, 자기 판매 현황 조회 |
|
|
||||||
| 3 | 지자체 관리자 | 소속 지자체의 발주·입고·재고·불출·판매·리포트 전반 |
|
|
||||||
| 4 | 슈퍼 관리자 | 전체 시스템 관리 + 기본코드 등 마스터 관리 (지자체 선택 후 작업) |
|
|
||||||
|
|
||||||
### 역할별 접근 한눈에 보기
|
|
||||||
|
|
||||||
| 기능군 | 일반 | 판매소 | 지자체관리자 | 슈퍼 |
|
|
||||||
|---|:--:|:--:|:--:|:--:|
|
|
||||||
| 기본정보 조회(코드·단가·포장) | △ | ○ | ○ | ○ |
|
|
||||||
| 기본코드·마스터 편집 | ✕ | ✕ | △(지자체분) | ○ |
|
|
||||||
| 발주·입고·재고·불출 | ✕ | ✕ | ○ | ○ |
|
|
||||||
| 판매·반품 등록 | ✕ | ○ | ○ | ○ |
|
|
||||||
| 판매현황·수불·통계 | ✕ | △(자기분) | ○ | ○ |
|
|
||||||
|
|
||||||
(○ 사용 가능 · △ 제한적 · ✕ 불가)
|
|
||||||
|
|
||||||
## 4. 다음 단계
|
|
||||||
|
|
||||||
- 전체 업무가 어떻게 이어지는지 먼저 보려면 **[핵심 업무 흐름]** 으로 이동하세요.
|
|
||||||
- 특정 작업 방법은 좌측 목차에서 해당 항목(발주·입고 / 재고·실사 / 판매·불출 / 판매현황·수불·통계)을 선택하세요.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# 핵심 업무 흐름
|
|
||||||
|
|
||||||
봉투 한 묶음이 시스템에서 거치는 전체 흐름입니다. 처음 사용하신다면 이 순서대로 익히는 것을 권장합니다.
|
|
||||||
|
|
||||||
## 전체 흐름
|
|
||||||
|
|
||||||
```
|
|
||||||
발주 ─→ 입고 ─→ 재고(실사) ─→ 판매 / 불출 ─→ 판매현황 · 수불 · 통계
|
|
||||||
```
|
|
||||||
|
|
||||||
| 단계 | 무엇을 하나 | 주요 메뉴 |
|
|
||||||
|---|---|---|
|
|
||||||
| ① 발주 | 봉투 종류·수량을 제작업체에 주문 | 발주 입고 관리 › 발주 등록 |
|
|
||||||
| ② 입고 | 도착한 물량을 시스템에 등록(스캐너/일괄) | 발주 입고 관리 › 입고 |
|
|
||||||
| ③ 재고 | 현재 보유 수량 확인, 실사로 실수량 보정 | 재고 관리 |
|
|
||||||
| ④ 판매 | 지정판매소에 판매·반품 처리 | 판매 관리 |
|
|
||||||
| ④ 불출 | 무료 대상자에게 무상 지급 | 불출 관리 |
|
|
||||||
| ⑤ 현황 | 일·기간·연간 판매 및 수불·통계 조회 | 판매 현황 / 봉투 수불 / 통계 분석 |
|
|
||||||
|
|
||||||
## 각 단계 한 줄 요약
|
|
||||||
|
|
||||||
1. **발주** — 봉투 품목·수량·납기를 입력해 발주서를 만들면 추적용 **LOT 번호**가 부여됩니다.
|
|
||||||
2. **입고** — 발주분이 도착하면 입고로 등록합니다. 이때 박스·팩·낱장 단위의 **바코드**가 생성됩니다.
|
|
||||||
3. **재고** — 품목별 현재 재고를 조회하고, 정기적으로 **실사**(선별 → 등록 → 적용)로 실제 수량과 맞춥니다.
|
|
||||||
4. **판매/불출** — 지정판매소 판매·반품, 또는 무료 대상자 불출로 재고가 감소합니다.
|
|
||||||
5. **현황·통계** — 일계표·기간별·연간 판매와 봉투 수불, 전년대비/월별/계절 추이를 확인합니다.
|
|
||||||
|
|
||||||
## 봉투 추적 단위
|
|
||||||
|
|
||||||
봉투는 다음 계층으로 추적됩니다. 자세한 코드 규칙은 **[봉투·LOT·바코드 코드체계]** 문서를 참고하세요.
|
|
||||||
|
|
||||||
```
|
|
||||||
LOT(발주 묶음) ─→ 박스(B) ─→ 팩(P) ─→ 낱장(S, 봉투 한 장)
|
|
||||||
```
|
|
||||||
|
|
||||||
> 코드 한 건이 어떤 바코드·인쇄숫자·인식번호인지 확인하려면 **도움말 › 번호알기** 화면을 사용하세요.
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 발주 · 입고
|
|
||||||
|
|
||||||
제작업체에 봉투를 주문(발주)하고, 도착한 물량을 시스템에 등록(입고)하는 단계입니다. **지자체 관리자** 이상이 사용합니다.
|
|
||||||
|
|
||||||
## 발주
|
|
||||||
|
|
||||||
### 발주 등록
|
|
||||||
|
|
||||||
**발주 입고 관리 › 발주 등록**
|
|
||||||
|
|
||||||
1. 봉투 **품목**(종류·용량)과 **수량**, 납품 관련 정보를 입력합니다.
|
|
||||||
2. 박스/낱장 수량과 금액·총계가 자동으로 계산됩니다.
|
|
||||||
3. 저장하면 발주 건이 생성되고, 추적용 **LOT 번호**가 자동 부여됩니다.
|
|
||||||
|
|
||||||
> 발주 내용은 무결성 보호를 위해 버전·해시로 관리됩니다. 수정(재발주) 시 기존 LOT는 유지됩니다.
|
|
||||||
|
|
||||||
### 발주 변경 · 현황
|
|
||||||
|
|
||||||
| 작업 | 메뉴 | 설명 |
|
|
||||||
|---|---|---|
|
|
||||||
| 발주 변경 | 발주 변경 | 기존 발주 수정·재발주 |
|
|
||||||
| 발주 현황 | 발주 현황 | 발주 목록을 기간·상태로 조회, 엑셀 내보내기 |
|
|
||||||
| 발주 상세 | (현황에서 행 선택) | 개별 발주 상세 확인, 취소 처리 |
|
|
||||||
|
|
||||||
## 입고
|
|
||||||
|
|
||||||
발주분이 실제 도착하면 입고로 등록합니다. 입고 시 **박스·팩·낱장 바코드**가 생성되어 재고에 반영됩니다.
|
|
||||||
|
|
||||||
| 방식 | 메뉴 | 언제 사용 |
|
|
||||||
|---|---|---|
|
|
||||||
| 스캐너 입고 | 발주 입고[스캐너] | 바코드를 스캔하며 입고 |
|
|
||||||
| 일괄 입고 | 일괄입고 | 다량을 한 번에 입고 |
|
|
||||||
| 입고 현황 | 입고 현황 | 입고 기록 조회, 엑셀 내보내기 |
|
|
||||||
|
|
||||||
### 입고 처리 순서
|
|
||||||
|
|
||||||
1. 입고할 발주 건(LOT)을 선택합니다.
|
|
||||||
2. 도착 수량(박스/낱장)을 확인·입력합니다.
|
|
||||||
3. 저장하면 재고가 증가하고, 단위별 바코드가 부여됩니다.
|
|
||||||
|
|
||||||
> 입고가 끝나면 **재고 관리**에서 수량이 정상 반영됐는지 확인하세요.
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# 재고 · 실사
|
|
||||||
|
|
||||||
현재 보유 봉투 수량을 확인하고, 정기적으로 실사를 통해 실제 수량과 맞추는 단계입니다.
|
|
||||||
|
|
||||||
## 재고 현황
|
|
||||||
|
|
||||||
**재고 관리 › 재고 현황**
|
|
||||||
|
|
||||||
- 품목별·상태별 현재 재고를 조회합니다.
|
|
||||||
- 지자체·봉투 종류 등으로 필터링할 수 있습니다.
|
|
||||||
- **엑셀 내보내기**로 목록을 저장할 수 있습니다.
|
|
||||||
|
|
||||||
| 항목 | 설명 |
|
|
||||||
|---|---|
|
|
||||||
| 품목 | 봉투 종류·용량 |
|
|
||||||
| 재고 수량 | 입고 − (판매 + 불출 + 파기) |
|
|
||||||
| 상태 | 재고/판매 등 단위별 상태 |
|
|
||||||
|
|
||||||
## 실사 (재고 조사)
|
|
||||||
|
|
||||||
장부상 재고와 실제 창고 수량을 맞추는 작업입니다. 다음 순서로 진행합니다.
|
|
||||||
|
|
||||||
```
|
|
||||||
실사 선별 ─→ 실사 등록(작업) ─→ 적용
|
|
||||||
```
|
|
||||||
|
|
||||||
| 단계 | 메뉴 | 하는 일 |
|
|
||||||
|---|---|---|
|
|
||||||
| ① 선별 | 실사 선별 조회 | 실사 대상 범위를 골라 선별 |
|
|
||||||
| ② 작업 | 실사 선별 관리 | 실제 수량을 입력·기록 |
|
|
||||||
| ③ 상세·적용 | 실사 상세 | 실사 결과를 확인하고 재고에 **적용** |
|
|
||||||
|
|
||||||
### 실사 진행 순서
|
|
||||||
|
|
||||||
1. **선별**: 실사할 품목·구간을 선택해 실사 건을 만듭니다.
|
|
||||||
2. **등록**: 실제 센 수량을 입력합니다. 장부 수량과의 차이가 표시됩니다.
|
|
||||||
3. **적용**: 검토 후 적용하면 차이가 재고에 반영됩니다.
|
|
||||||
|
|
||||||
> 적용 전까지는 재고에 영향을 주지 않으므로, 입력 도중 중단해도 안전합니다.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 판매 · 불출
|
|
||||||
|
|
||||||
재고를 외부로 내보내는 두 가지 경로입니다. **판매**는 지정판매소 대상 유상 거래, **불출**은 무료 대상자에 대한 무상 지급입니다.
|
|
||||||
|
|
||||||
## 판매 (지정판매소)
|
|
||||||
|
|
||||||
**판매 관리** 메뉴에서 처리합니다.
|
|
||||||
|
|
||||||
| 작업 | 메뉴 | 설명 |
|
|
||||||
|---|---|---|
|
|
||||||
| 판매 등록 | 지정 판매소 판매 | 판매소를 선택해 판매 거래 기록 |
|
|
||||||
| 스캔 판매 | 지정 판매소 판매[스캔] | 바코드를 스캔하며 판매(판매소 현장용) |
|
|
||||||
| 판매 취소 | 지정 판매소 판매 취소 | 기존 판매 거래 취소 |
|
|
||||||
| 반품 | 지정 판매소 반품 | 판매분 반품 등록 |
|
|
||||||
| 반품 취소 | 지정 판매소 반품 취소 | 반품 취소 처리 |
|
|
||||||
|
|
||||||
### 판매 등록 순서
|
|
||||||
|
|
||||||
1. 판매할 **지정판매소**를 선택합니다.
|
|
||||||
2. 봉투 **품목·수량**을 입력(또는 바코드 스캔)합니다.
|
|
||||||
3. 저장하면 재고가 감소하고 판매 내역이 기록됩니다.
|
|
||||||
|
|
||||||
### 전화 접수(주문)
|
|
||||||
|
|
||||||
| 작업 | 메뉴 |
|
|
||||||
|---|---|
|
|
||||||
| 전화 접수(신규) | 전화 접수 |
|
|
||||||
| 전화 접수 관리 | 전화 접수 관리(수정·취소) |
|
|
||||||
|
|
||||||
## 불출 (무료 대상자)
|
|
||||||
|
|
||||||
**불출 관리** 메뉴에서 무료 대상자에게 봉투를 무상 지급합니다.
|
|
||||||
|
|
||||||
| 작업 | 메뉴 | 설명 |
|
|
||||||
|---|---|---|
|
|
||||||
| 불출 처리 | 무료용 불출 처리 | 무료 배분 등록(재고 감소) |
|
|
||||||
| 불출 취소 | 무료용 불출 취소 | 불출 취소(재고 복원) |
|
|
||||||
| 불출 현황 | 무료 불출 현황 | 기간별 불출 기록 조회 |
|
|
||||||
|
|
||||||
### 불출 처리 순서
|
|
||||||
|
|
||||||
1. 무료 **대상처**와 봉투 **품목·수량**을 선택합니다.
|
|
||||||
2. 저장하면 재고가 감소하고 불출 내역이 기록됩니다.
|
|
||||||
3. 잘못 처리한 경우 **무료용 불출 취소**로 되돌리면 재고가 복원됩니다.
|
|
||||||
|
|
||||||
> 판매·불출 모두 재고를 감소시키므로, 처리 후 **재고 현황**에서 반영 결과를 확인하세요.
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 판매현황 · 수불 · 통계
|
|
||||||
|
|
||||||
판매·재고 흐름을 집계해 보여주는 조회·리포트 화면 모음입니다. 대부분 **기간을 지정해 조회**하고 인쇄·엑셀로 내보낼 수 있습니다.
|
|
||||||
|
|
||||||
## 판매 현황
|
|
||||||
|
|
||||||
| 메뉴 | 내용 |
|
|
||||||
|---|---|
|
|
||||||
| 지정 판매소 일/기간 판매대장 | 판매소별 일자·기간 거래 장부 |
|
|
||||||
| 일계표 | 하루 전체 판매 요약(일계 + 월 누계) |
|
|
||||||
| 기간별 판매현황 | 지정 기간의 판매 집계(일집계/기간집계) |
|
|
||||||
| 년 판매 현황 | 연간 판매 통계(월별/분기별) |
|
|
||||||
| 지정 판매소별 판매현황 | 판매소별 수량·금액 비교 |
|
|
||||||
| 홈텍스 처리 | 세금계산서용 데이터(엑셀) 생성 |
|
|
||||||
|
|
||||||
## 봉투 수불 관리
|
|
||||||
|
|
||||||
입고·판매·불출·반품·파기를 한데 모아 **수불(수입·불출)** 흐름을 봅니다.
|
|
||||||
|
|
||||||
| 메뉴 | 내용 |
|
|
||||||
|---|---|
|
|
||||||
| 기간별 봉투 수불 현황 | 기간 내 재고/입고/판매/불출 종합(엑셀 가능) |
|
|
||||||
| 기타 입출고 | 손상·기증·폐기 등 기타 입출고 등록·조회 |
|
|
||||||
| 반품/파기 현황 | 반품 및 파기 내역 |
|
|
||||||
| LOT 수불 조회 | 봉투번호(바코드)·LOT 단위 이력 추적 |
|
|
||||||
| 쓰레기 봉투 수급 계획 | 공급·수요 계획 |
|
|
||||||
|
|
||||||
### LOT 수불 조회 사용법
|
|
||||||
|
|
||||||
1. **봉투번호(바코드)** 또는 LOT 번호를 입력합니다.
|
|
||||||
2. 조회하면 해당 단위의 입고·판매·반품 이력이 표시됩니다.
|
|
||||||
3. 입력할 코드 형식이 헷갈리면 **도움말 › 번호알기**로 먼저 확인하세요.
|
|
||||||
|
|
||||||
## 통계 분석
|
|
||||||
|
|
||||||
| 메뉴 | 내용 |
|
|
||||||
|---|---|
|
|
||||||
| 전년 대비 판매 분석 | 작년 동기 대비 비교(차트) |
|
|
||||||
| 월별 판매 추이 분석 | 월별 추이 시각화 |
|
|
||||||
| 계절별 판매 추이 분석 | 계절 패턴 분석 |
|
|
||||||
|
|
||||||
> 리포트 화면은 조회 조건(기간·품목·판매소)을 바꿔가며 반복 조회할 수 있습니다. 결과는 인쇄 버튼 또는 엑셀 내보내기로 저장하세요.
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# 봉투 · LOT · 바코드 코드체계
|
|
||||||
|
|
||||||
봉투번호(바코드)·LOT 번호·품목코드가 무엇을 뜻하는지 정리한 안내입니다. LOT 수불 조회나 번호알기 화면에서 코드를 입력할 때 참고하세요.
|
|
||||||
|
|
||||||
## 추적 단위 계층
|
|
||||||
|
|
||||||
```
|
|
||||||
LOT(발주 묶음) ─→ 박스(B) ─→ 팩(P) ─→ 낱장(S, 봉투 한 장)
|
|
||||||
```
|
|
||||||
|
|
||||||
| 단위 | 예시 | 용도 |
|
|
||||||
|---|---|---|
|
|
||||||
| LOT | `OQXCKH` | 발주 단위, LOT 수불·시드 |
|
|
||||||
| 박스 | `OQXCKH-000008-B001` | 박스 단위 입출고 |
|
|
||||||
| 팩 | `OQXCKH-000008-P299` | 팩 단위(스캔 가능) |
|
|
||||||
| 낱장 | `OQXCKH-000008-P299-S00125` | 봉투 한 장 단위 판매·반품 |
|
|
||||||
|
|
||||||
> `000008`은 "8번째 봉투"가 아니라 **입고 건 번호를 6자리로 채운 값**입니다.
|
|
||||||
|
|
||||||
## 바코드 형식 읽는 법
|
|
||||||
|
|
||||||
`OQXCKH-000008-P299-S00125` 를 예로 들면:
|
|
||||||
|
|
||||||
| 구간 | 값 | 의미 |
|
|
||||||
|---|---|---|
|
|
||||||
| 접두 | `OQXCKH` | 발주 LOT 번호 |
|
|
||||||
| 입고번호 | `000008` | 입고 건 번호(6자리) |
|
|
||||||
| 팩 | `P299` | 그 입고 건의 299번째 팩 |
|
|
||||||
| 낱장 | `S00125` | 그 팩의 125번째 낱장(봉투 한 장) |
|
|
||||||
|
|
||||||
- 박스는 `B001`, 팩은 `P299` 처럼 접두 문자로 구분합니다.
|
|
||||||
- LOT 번호는 발주 시 자동 부여되는 영문·숫자 6자리입니다.
|
|
||||||
|
|
||||||
## 품목코드 (봉투 종류)
|
|
||||||
|
|
||||||
품목코드는 봉투의 **종류·용량**을 식별하는 마스터 코드로, 바코드와는 별개입니다.
|
|
||||||
|
|
||||||
| 자리 | 의미 | 예 |
|
|
||||||
|---|---|---|
|
|
||||||
| 앞 2자리 | 봉투 구분(10 일반, 20 공공, 30 무료 …) | `10` |
|
|
||||||
| 다음 2자리 | 용량 | `15` |
|
|
||||||
| 마지막 1자리 | 재질 등 | `2` |
|
|
||||||
|
|
||||||
**예:** `10152` = 일반용 20L. 같은 품목이라도 발주·입고 건마다 바코드(LOT) 접두는 달라집니다.
|
|
||||||
|
|
||||||
## 번호알기로 확인하기
|
|
||||||
|
|
||||||
코드 한 건이 어떤 바코드·인쇄숫자·인식번호인지 확인하려면 **도움말 › 번호알기(봉투번호확인)** 화면에 코드를 입력하세요.
|
|
||||||
|
|
||||||
입력 가능한 형식:
|
|
||||||
|
|
||||||
| 단위 | 입력 형식 | 예시 |
|
|
||||||
|---|---|---|
|
|
||||||
| 낱장 | `{LOT}-{입고번호}-P{팩}-S{낱장}` | `OQXCKH-000008-P299-S00125` |
|
|
||||||
| 팩 | `{LOT}-{입고번호}-P{팩}` | `OQXCKH-000008-P299` |
|
|
||||||
| 박스 | `{LOT}-{입고번호}-B{박스}` | `OQXCKH-000008-B001` |
|
|
||||||
| LOT | 영문·숫자 4~8자리 | `OQXCKH` |
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# 자주 묻는 질문 · 문의
|
|
||||||
|
|
||||||
## 자주 묻는 질문
|
|
||||||
|
|
||||||
### Q. 로그인 후 업무 화면이 안 열려요.
|
|
||||||
슈퍼 관리자는 **작업할 지자체를 먼저 선택**해야 합니다. 상단 안내에 따라 지자체를 선택하세요. 일반/판매소 계정은 권한 범위 내 메뉴만 보입니다.
|
|
||||||
|
|
||||||
### Q. 메뉴가 안 보여요.
|
|
||||||
역할(권한)에 따라 노출 메뉴가 다릅니다. **시작하기 › 역할별 접근 한눈에 보기** 표를 확인하세요. 그래도 필요한 메뉴가 없으면 관리자에게 문의하세요.
|
|
||||||
|
|
||||||
### Q. 입고했는데 재고에 안 보여요.
|
|
||||||
입고가 정상 저장됐는지 **발주 입고 관리 › 입고 현황**에서 확인하고, **재고 관리 › 재고 현황**에서 품목·지자체 필터를 점검하세요.
|
|
||||||
|
|
||||||
### Q. 판매/불출을 잘못 처리했어요.
|
|
||||||
- 판매: **지정 판매소 판매 취소** 또는 **반품**으로 되돌립니다.
|
|
||||||
- 불출: **무료용 불출 취소**로 되돌리면 재고가 복원됩니다.
|
|
||||||
|
|
||||||
### Q. 봉투 코드(바코드)가 무슨 뜻인지 모르겠어요.
|
|
||||||
**도움말 › 번호알기(봉투번호확인)** 에 코드를 입력하면 바코드·인쇄숫자·인식번호로 분해해 보여줍니다. 형식은 **봉투·LOT·바코드 코드체계** 문서를 참고하세요.
|
|
||||||
|
|
||||||
### Q. 비밀번호를 바꾸고 싶어요.
|
|
||||||
**기본정보관리 › PASSWORD 변경** 에서 변경할 수 있습니다.
|
|
||||||
|
|
||||||
### Q. 리포트를 엑셀/인쇄로 저장할 수 있나요?
|
|
||||||
대부분의 현황·리포트 화면에 **엑셀 내보내기**와 **인쇄** 기능이 있습니다. 이 매뉴얼 화면도 우측 상단 **인쇄** 버튼으로 출력할 수 있습니다.
|
|
||||||
|
|
||||||
## 문의
|
|
||||||
|
|
||||||
시스템 사용 중 문제가 있으면 시스템 운영 담당자 또는 소속 지자체 관리자에게 문의하세요.
|
|
||||||
@@ -264,12 +264,13 @@ if (! function_exists('normalize_menu_link_for_url')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('mgmt_path')) {
|
if (! function_exists('mgmt_url')) {
|
||||||
/**
|
/**
|
||||||
* 업무 화면 상대 경로 (페이저 setPath 등). 선행 슬래시 없음.
|
* 업무 화면 링크: 정식 URL 은 /bag/* (adminAuth). 포장 단위 CRUD 는 packaging-units/manage 로 치환.
|
||||||
*/
|
*/
|
||||||
function mgmt_path(string $path): string
|
function mgmt_url(string $path): string
|
||||||
{
|
{
|
||||||
|
helper('url');
|
||||||
$path = trim($path, '/');
|
$path = trim($path, '/');
|
||||||
// bag/packaging-units 는 조회 전용(Bag) — CRUD 는 /bag/packaging-units/manage/* 로 분리
|
// bag/packaging-units 는 조회 전용(Bag) — CRUD 는 /bag/packaging-units/manage/* 로 분리
|
||||||
if ($path === 'packaging-units') {
|
if ($path === 'packaging-units') {
|
||||||
@@ -278,35 +279,7 @@ if (! function_exists('mgmt_path')) {
|
|||||||
$path = 'packaging-units/manage/' . substr($path, strlen('packaging-units/'));
|
$path = 'packaging-units/manage/' . substr($path, strlen('packaging-units/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'bag/' . $path;
|
return site_url('bag/' . $path);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('mgmt_url')) {
|
|
||||||
/**
|
|
||||||
* 업무 화면 링크: 정식 URL 은 /bag/* (adminAuth). 포장 단위 CRUD 는 packaging-units/manage 로 치환.
|
|
||||||
*/
|
|
||||||
function mgmt_url(string $path): string
|
|
||||||
{
|
|
||||||
helper('url');
|
|
||||||
|
|
||||||
return site_url(mgmt_path($path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('apply_pager_path')) {
|
|
||||||
/**
|
|
||||||
* CI4 페이저: setPath 는 상대 경로만 허용(전체 URL 시 baseURL 이중 결합).
|
|
||||||
* 검색 조건은 only() 로 유지합니다.
|
|
||||||
*
|
|
||||||
* @param \CodeIgniter\Pager\Pager $pager
|
|
||||||
*/
|
|
||||||
function apply_pager_path($pager, string $path, array $queryForPager = []): void
|
|
||||||
{
|
|
||||||
$pager->setPath($path);
|
|
||||||
if ($queryForPager !== []) {
|
|
||||||
$pager->only(array_keys($queryForPager));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,10 +367,6 @@ if (! function_exists('menu_link_candidate_paths')) {
|
|||||||
$cands[] = 'admin/packaging-units' . ($m[1] ?? '');
|
$cands[] = 'admin/packaging-units' . ($m[1] ?? '');
|
||||||
} elseif (preg_match('#^admin/packaging-units(/.*)?$#', $p, $m)) {
|
} elseif (preg_match('#^admin/packaging-units(/.*)?$#', $p, $m)) {
|
||||||
$cands[] = 'bag/packaging-units/manage' . ($m[1] ?? '');
|
$cands[] = 'bag/packaging-units/manage' . ($m[1] ?? '');
|
||||||
} elseif ($p === 'bag/inventory/inspection-select') {
|
|
||||||
// 실사 선별 조회 메뉴는 작업 화면(inspection-work)도 동일 메뉴로 활성 처리
|
|
||||||
$cands[] = 'bag/inventory/inspection-work';
|
|
||||||
$cands[] = 'bag/inventory/inspection';
|
|
||||||
} elseif (str_starts_with($p, 'admin/')) {
|
} elseif (str_starts_with($p, 'admin/')) {
|
||||||
$cands[] = 'bag/' . substr($p, strlen('admin/'));
|
$cands[] = 'bag/' . substr($p, strlen('admin/'));
|
||||||
} elseif (str_starts_with($p, 'bag/')) {
|
} elseif (str_starts_with($p, 'bag/')) {
|
||||||
@@ -506,68 +475,6 @@ if (! function_exists('site_nav_link_matches_current')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('menu_active_child_for_parent')) {
|
|
||||||
/**
|
|
||||||
* 같은 부모 아래 형제 소메뉴 중, 현재 요청에 해당하는 항목을 하나만 고른다.
|
|
||||||
*
|
|
||||||
* 짧은 mm_link(예: bag/designated-shops)가 긴 경로(bag/designated-shops/browse)와
|
|
||||||
* 동시에 prefix 규칙으로 매칭될 때, 가장 구체적인 경로(일치한 후보 문자열 길이 최대)만 활성으로 본다.
|
|
||||||
* 길이가 같으면 mm_num이 작은 항목을 선택(동일 URL이 여러 메뉴에 매핑된 경우 등).
|
|
||||||
*
|
|
||||||
* @param object{children?: array<int, object>} $parentNavItem
|
|
||||||
* @param list<string> $dashboardPathAliases
|
|
||||||
*
|
|
||||||
* @return object|null 활성으로 표시할 자식 노드(mm_idx 등 포함), 없으면 null
|
|
||||||
*/
|
|
||||||
function menu_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object
|
|
||||||
{
|
|
||||||
$children = $parentNavItem->children ?? [];
|
|
||||||
if ($children === []) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$best = null;
|
|
||||||
$bestLen = -1;
|
|
||||||
$bestMmNum = PHP_INT_MAX;
|
|
||||||
|
|
||||||
foreach ($children as $child) {
|
|
||||||
$mmLink = $child->mm_link ?? null;
|
|
||||||
$maxLen = -1;
|
|
||||||
foreach (menu_link_candidate_paths($mmLink, $currentPath) as $cand) {
|
|
||||||
if (menu_single_path_matches_request($cand, $currentPath, $dashboardPathAliases)) {
|
|
||||||
$maxLen = max($maxLen, strlen($cand));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($maxLen < 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$mmNum = (int) ($child->mm_num ?? 0);
|
|
||||||
if ($maxLen > $bestLen || ($maxLen === $bestLen && $mmNum < $bestMmNum)) {
|
|
||||||
$bestLen = $maxLen;
|
|
||||||
$bestMmNum = $mmNum;
|
|
||||||
$best = $child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $best;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('site_nav_active_child_for_parent')) {
|
|
||||||
/**
|
|
||||||
* 사이트 상단 메뉴 전용 호환 래퍼.
|
|
||||||
*
|
|
||||||
* @param object{children?: array<int, object>} $parentNavItem
|
|
||||||
* @param list<string> $dashboardPathAliases
|
|
||||||
*
|
|
||||||
* @return object|null
|
|
||||||
*/
|
|
||||||
function site_nav_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object
|
|
||||||
{
|
|
||||||
return menu_active_child_for_parent($parentNavItem, $currentPath, $dashboardPathAliases);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('session_user_nav_display')) {
|
if (! function_exists('session_user_nav_display')) {
|
||||||
/**
|
/**
|
||||||
* 상단 메뉴바용: 로그인 사용자 이름·역할 표시
|
* 상단 메뉴바용: 로그인 사용자 이름·역할 표시
|
||||||
@@ -594,345 +501,3 @@ if (! function_exists('session_user_nav_display')) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('gov_portal_active_variant')) {
|
|
||||||
/**
|
|
||||||
* 공공 포털 시안 변형: base(좌측 MY MENU) | strip(호버 상단 메뉴).
|
|
||||||
*/
|
|
||||||
function gov_portal_active_variant(?string $fromPath = null): string
|
|
||||||
{
|
|
||||||
$path = strtolower(ltrim($fromPath ?? current_nav_request_path(), '/'));
|
|
||||||
|
|
||||||
return str_starts_with($path, 'dashboard/gov-portal-strip') ? 'strip' : 'base';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('gov_portal_code_kinds_portal_path')) {
|
|
||||||
/**
|
|
||||||
* 포털 UI 기본 코드관리 시안 경로 (변형별).
|
|
||||||
*/
|
|
||||||
function gov_portal_code_kinds_portal_path(?string $variant = null): string
|
|
||||||
{
|
|
||||||
return gov_portal_active_variant($variant) === 'strip'
|
|
||||||
? 'dashboard/gov-portal-strip/code-kinds'
|
|
||||||
: 'dashboard/gov-portal/code-kinds';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('gov_portal_menu_href_remap')) {
|
|
||||||
/**
|
|
||||||
* gov-portal 상·좌측·드롭다운 메뉴 전용: 기본 코드관리 → 변형별 포털 시안 URL.
|
|
||||||
*/
|
|
||||||
function gov_portal_menu_href_remap(string $href, ?string $variant = null): string
|
|
||||||
{
|
|
||||||
if (strtolower(ltrim($href, '/')) !== 'bag/code-kinds') {
|
|
||||||
return $href;
|
|
||||||
}
|
|
||||||
|
|
||||||
return gov_portal_code_kinds_portal_path($variant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('gov_portal_nav_match_path')) {
|
|
||||||
/**
|
|
||||||
* 포털 시안 URL 접속 시 사이트 메뉴(mm_link) 활성 판별용.
|
|
||||||
*/
|
|
||||||
function gov_portal_nav_match_path(string $currentPath): string
|
|
||||||
{
|
|
||||||
$key = strtolower(ltrim($currentPath, '/'));
|
|
||||||
|
|
||||||
return match ($key) {
|
|
||||||
'dashboard/gov-portal/code-kinds',
|
|
||||||
'dashboard/gov-portal-strip/code-kinds' => 'bag/code-kinds',
|
|
||||||
default => $currentPath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('gov_portal_dashboard_aliases')) {
|
|
||||||
/**
|
|
||||||
* 포털 대시보드 현재 경로·메뉴 활성 판별용 별칭.
|
|
||||||
*
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
function gov_portal_dashboard_aliases(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'dashboard',
|
|
||||||
'dashboard/blend',
|
|
||||||
'dashboard/simple',
|
|
||||||
'dashboard/lite',
|
|
||||||
'dashboard/compact',
|
|
||||||
'dashboard/dense',
|
|
||||||
'dashboard/charts',
|
|
||||||
'dashboard/modern',
|
|
||||||
'dashboard/gov-portal',
|
|
||||||
'dashboard/gov-portal-strip',
|
|
||||||
'dashboard/gov-portal/code-kinds',
|
|
||||||
'dashboard/gov-portal-strip/code-kinds',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('gov_portal_nav_context')) {
|
|
||||||
/**
|
|
||||||
* 공공 포털형 대시보드(gov-portal)용 사이트 메뉴 트리·JSON·활성 대메뉴 인덱스.
|
|
||||||
*
|
|
||||||
* @return array{
|
|
||||||
* siteNavTree: array<int, object>,
|
|
||||||
* navItems: list<array{idx:int,name:string,href:string,url:string,children:list<array>,hasChildren:bool}>,
|
|
||||||
* navJson: string,
|
|
||||||
* activeParentIdx: int,
|
|
||||||
* currentPath: string,
|
|
||||||
* dashboardAliases: list<string>
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
function gov_portal_nav_context(bool $remapLinks = true, ?array $tree = null): array
|
|
||||||
{
|
|
||||||
helper('url');
|
|
||||||
|
|
||||||
$tree = $tree ?? get_site_nav_tree();
|
|
||||||
$rawPath = current_nav_request_path();
|
|
||||||
$variant = gov_portal_active_variant($rawPath);
|
|
||||||
$currentPath = gov_portal_nav_match_path($rawPath);
|
|
||||||
$aliases = gov_portal_dashboard_aliases();
|
|
||||||
$items = [];
|
|
||||||
$activeParentIdx = 0;
|
|
||||||
|
|
||||||
foreach ($tree as $pIdx => $parent) {
|
|
||||||
$children = [];
|
|
||||||
foreach ($parent->children ?? [] as $child) {
|
|
||||||
$href = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
|
|
||||||
if ($remapLinks) {
|
|
||||||
$href = gov_portal_menu_href_remap($href, $variant);
|
|
||||||
}
|
|
||||||
$children[] = [
|
|
||||||
'idx' => (int) ($child->mm_idx ?? 0),
|
|
||||||
'name' => (string) ($child->mm_name ?? ''),
|
|
||||||
'href' => $href,
|
|
||||||
'url' => $href !== '' ? base_url($href) : '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$parentHref = menu_link_preferred_href_path($parent->mm_link ?? null, $currentPath);
|
|
||||||
$isParentActive = site_nav_link_matches_current($parent->mm_link ?? null, $currentPath, $aliases);
|
|
||||||
$activeChild = menu_active_child_for_parent($parent, $currentPath, $aliases);
|
|
||||||
if ($isParentActive || $activeChild !== null) {
|
|
||||||
$activeParentIdx = (int) $pIdx;
|
|
||||||
}
|
|
||||||
|
|
||||||
$items[] = [
|
|
||||||
'idx' => (int) ($parent->mm_idx ?? 0),
|
|
||||||
'name' => (string) ($parent->mm_name ?? ''),
|
|
||||||
'href' => $parentHref,
|
|
||||||
'url' => $parentHref !== '' ? base_url($parentHref) : '',
|
|
||||||
'children' => $children,
|
|
||||||
'hasChildren' => $children !== [],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'siteNavTree' => $tree,
|
|
||||||
'navItems' => $items,
|
|
||||||
'navJson' => json_encode($items, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS),
|
|
||||||
'activeParentIdx' => $activeParentIdx,
|
|
||||||
'currentPath' => $rawPath,
|
|
||||||
'dashboardAliases'=> $aliases,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('gov_portal_menu_search_options')) {
|
|
||||||
/**
|
|
||||||
* 메뉴검색 입력 아래 표시할 바로가기(사이트 메뉴에서 추출, 최대 N개).
|
|
||||||
*
|
|
||||||
* @param list<array> $navItems gov_portal_nav_context()['navItems']
|
|
||||||
* @return list<array{label:string,url:string,keyword:string}>
|
|
||||||
*/
|
|
||||||
function gov_portal_menu_search_options(array $navItems, int $max = 8): array
|
|
||||||
{
|
|
||||||
$picked = [];
|
|
||||||
$seen = [];
|
|
||||||
$prefer = ['재고', '발주', '수불', '판매', '통계', '실사', '발급', '주문', '코드', '지정판매', '구매'];
|
|
||||||
|
|
||||||
$push = static function (array $child) use (&$picked, &$seen, $max): bool {
|
|
||||||
$href = (string) ($child['href'] ?? '');
|
|
||||||
if ($href === '' || isset($seen[$href])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$seen[$href] = true;
|
|
||||||
$picked[] = [
|
|
||||||
'label' => (string) ($child['name'] ?? ''),
|
|
||||||
'url' => (string) ($child['url'] ?? ''),
|
|
||||||
'keyword' => (string) ($child['name'] ?? ''),
|
|
||||||
];
|
|
||||||
|
|
||||||
return count($picked) >= $max;
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach ($prefer as $needle) {
|
|
||||||
if (count($picked) >= $max) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
foreach ($navItems as $parent) {
|
|
||||||
foreach ($parent['children'] ?? [] as $child) {
|
|
||||||
$name = (string) ($child['name'] ?? '');
|
|
||||||
if ($name === '' || ! str_contains($name, $needle)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($push($child)) {
|
|
||||||
break 3;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (count($picked) >= $max) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($navItems as $parent) {
|
|
||||||
if (count($picked) >= $max) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
foreach ($parent['children'] ?? [] as $child) {
|
|
||||||
if ($push($child)) {
|
|
||||||
break 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $picked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('gov_portal_dashboard_view_data')) {
|
|
||||||
/**
|
|
||||||
* 공공 포털형 대시보드(gov-portal / gov-portal-strip) 뷰 데이터.
|
|
||||||
* 컨트롤러에서 view() 두 번째 인자로 전달한다.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
function gov_portal_dashboard_view_data(string $lgLabel, string $activeVariant): array
|
|
||||||
{
|
|
||||||
helper('url');
|
|
||||||
$govNav = gov_portal_nav_context();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'lgLabel' => $lgLabel,
|
|
||||||
'activeVariant' => $activeVariant,
|
|
||||||
'mbName' => (string) (session()->get('mb_name') ?? '담당자'),
|
|
||||||
'mbId' => (string) (session()->get('mb_id') ?? ''),
|
|
||||||
'levelName' => config('Roles')->getLevelName((int) session()->get('mb_level')),
|
|
||||||
'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],
|
|
||||||
],
|
|
||||||
'stockAlerts' => [
|
|
||||||
[
|
|
||||||
'count' => 18,
|
|
||||||
'label' => '정상',
|
|
||||||
'class' => 'al-blue',
|
|
||||||
'bags' => ['일반 10L', '일반 20L', '음식물 2L', '음식물 5L'],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'count' => 3,
|
|
||||||
'label' => '주의',
|
|
||||||
'class' => 'al-yellow',
|
|
||||||
'bags' => ['일반 50L', '특수 소형', '음식물 10L'],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'count' => 2,
|
|
||||||
'label' => '경계',
|
|
||||||
'class' => 'al-orange',
|
|
||||||
'bags' => ['특수규격 A', '재사용봉투'],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'count' => 1,
|
|
||||||
'label' => '부족',
|
|
||||||
'class' => 'al-red',
|
|
||||||
'bags' => ['일반 20L'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'quickLinks' => [
|
|
||||||
['label' => '기본 코드관리', 'desc' => '포털 UI · 코드 종류·세부코드', 'url' => base_url(gov_portal_code_kinds_portal_path($activeVariant)), 'icon' => 'fa-code'],
|
|
||||||
['label' => '창고 재고 조회', 'desc' => '품목별 현재 재고', 'url' => base_url('bag/inventory'), 'icon' => 'fa-boxes-stacked'],
|
|
||||||
['label' => '발주(구매신청) 등록', 'desc' => '지정판매소 발주 입력', 'url' => base_url('bag/order/create'), 'icon' => 'fa-cart-shopping'],
|
|
||||||
['label' => '수불 흐름 보기', 'desc' => '입고·출고 내역', 'url' => base_url('bag/flow'), 'icon' => 'fa-arrow-right-arrow-left'],
|
|
||||||
['label' => '판매 내역 조회', 'desc' => '기간별 판매 현황', 'url' => base_url('bag/sales'), 'icon' => 'fa-receipt'],
|
|
||||||
['label' => '전년 대비 통계', 'desc' => '통계분석 · YoY', 'url' => base_url('bag/analytics/year-over-year'), 'icon' => 'fa-chart-line'],
|
|
||||||
['label' => '도움말 / 매뉴얼', 'desc' => '업무별 사용 안내', 'url' => base_url('bag/help'), 'icon' => 'fa-circle-question'],
|
|
||||||
],
|
|
||||||
'notices' => [
|
|
||||||
['title' => '봉투 단가 조정 예고 — 3/1 적용 예정', 'date' => '2026.05.12'],
|
|
||||||
['title' => '실사·재고 점검 일정 안내', 'date' => '2026.05.08'],
|
|
||||||
['title' => '지정판매소 바코드 연동 점검 완료', 'date' => '2026.04.29'],
|
|
||||||
],
|
|
||||||
'timeline' => [
|
|
||||||
['time' => '14:32', 'text' => 'GS25 검단점 — 일반 20L 판매 3건'],
|
|
||||||
['time' => '13:10', 'text' => '북구 창고 — 입고 확인 완료'],
|
|
||||||
['time' => '11:45', 'text' => '구매신청 #1042 승인 대기'],
|
|
||||||
['time' => '09:20', 'text' => '회원 가입 승인 1건 처리'],
|
|
||||||
],
|
|
||||||
'govMapPanel' => [
|
|
||||||
'centerLat' => 35.8714,
|
|
||||||
'centerLng' => 128.6014,
|
|
||||||
'zoom' => 11,
|
|
||||||
'markers' => [
|
|
||||||
['lat' => 35.8852, 'lng' => 128.5821, 'kind' => 'warehouse', 'title' => '북구 종량제 창고'],
|
|
||||||
['lat' => 35.8684, 'lng' => 128.6243, 'kind' => 'shop', 'title' => 'GS25 검단점'],
|
|
||||||
['lat' => 35.8621, 'lng' => 128.5948, 'kind' => 'shop', 'title' => 'CU 칠곡중앙점'],
|
|
||||||
['lat' => 35.8776, 'lng' => 128.6479, 'kind' => 'flow', 'title' => '동구 입고·출고 거점'],
|
|
||||||
['lat' => 35.8512, 'lng' => 128.6112, 'kind' => 'shop', 'title' => '이마트 월배점'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'portalVariants' => [
|
|
||||||
['label' => '기본', 'url' => base_url('dashboard/gov-portal')],
|
|
||||||
['label' => '변형', 'url' => base_url('dashboard/gov-portal-strip')],
|
|
||||||
],
|
|
||||||
'siteNavTree' => $govNav['siteNavTree'],
|
|
||||||
'govNavItems' => $govNav['navItems'],
|
|
||||||
'menuSearchOptions' => gov_portal_menu_search_options($govNav['navItems']),
|
|
||||||
'govNavJson' => $govNav['navJson'],
|
|
||||||
'govActiveParentIdx' => $govNav['activeParentIdx'],
|
|
||||||
'govCurrentPath' => gov_portal_nav_match_path($govNav['currentPath']),
|
|
||||||
'govDashboardAliases' => $govNav['dashboardAliases'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('gov_portal_nav_partial_vars')) {
|
|
||||||
/**
|
|
||||||
* CI4 view() partial에 넘길 사이트 메뉴·네비 변수만 추출.
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $viewData
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
function gov_portal_nav_partial_vars(array $viewData): array
|
|
||||||
{
|
|
||||||
$keys = [
|
|
||||||
'siteNavTree',
|
|
||||||
'govNavItems',
|
|
||||||
'govNavJson',
|
|
||||||
'govActiveParentIdx',
|
|
||||||
'govCurrentPath',
|
|
||||||
'govDashboardAliases',
|
|
||||||
'govActiveChildHref',
|
|
||||||
];
|
|
||||||
$out = [];
|
|
||||||
foreach ($keys as $key) {
|
|
||||||
if (array_key_exists($key, $viewData)) {
|
|
||||||
$out[$key] = $viewData[$key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -67,441 +67,3 @@ if (! function_exists('csv_encode_row')) {
|
|||||||
return implode(',', $escaped) . "\r\n";
|
return implode(',', $escaped) . "\r\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('export_excel_2003_xml')) {
|
|
||||||
/**
|
|
||||||
* Excel 2003 XML(SpreadsheetML)로 브라우저 다운로드 (.xls 확장자, 별도 라이브러리 불필요)
|
|
||||||
*
|
|
||||||
* @param string $filename 저장 파일명(확장자는 .xls로 정규화)
|
|
||||||
* @param string $sheetName 시트 이름(Excel 제한: 길이·일부 문자)
|
|
||||||
* @param string[] $headers 컬럼 헤더
|
|
||||||
* @param array $rows 데이터 행(각 행은 배열, 값은 문자열로 출력)
|
|
||||||
*/
|
|
||||||
function export_excel_2003_xml(string $filename, string $sheetName, array $headers, array $rows): void
|
|
||||||
{
|
|
||||||
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
|
|
||||||
|
|
||||||
$safeSheet = str_replace(['/', '\\', '?', '*', '[', ']'], '', $sheetName);
|
|
||||||
$safeSheet = function_exists('mb_substr')
|
|
||||||
? mb_substr($safeSheet, 0, 31, 'UTF-8')
|
|
||||||
: substr($safeSheet, 0, 31);
|
|
||||||
|
|
||||||
$esc = static function (mixed $v): string {
|
|
||||||
return htmlspecialchars((string) ($v ?? ''), ENT_XML1 | ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
||||||
};
|
|
||||||
|
|
||||||
$parts = [];
|
|
||||||
$parts[] = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
||||||
$parts[] = '<?mso-application progid="Excel.Sheet"?>';
|
|
||||||
$parts[] = '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">';
|
|
||||||
$parts[] = '<Worksheet ss:Name="' . $esc($safeSheet) . '">';
|
|
||||||
$parts[] = '<Table>';
|
|
||||||
|
|
||||||
$parts[] = '<Row>';
|
|
||||||
foreach ($headers as $h) {
|
|
||||||
$parts[] = '<Cell><Data ss:Type="String">' . $esc($h) . '</Data></Cell>';
|
|
||||||
}
|
|
||||||
$parts[] = '</Row>';
|
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$parts[] = '<Row>';
|
|
||||||
foreach (array_values((array) $row) as $cell) {
|
|
||||||
$parts[] = '<Cell><Data ss:Type="String">' . $esc($cell) . '</Data></Cell>';
|
|
||||||
}
|
|
||||||
$parts[] = '</Row>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts[] = '</Table>';
|
|
||||||
$parts[] = '</Worksheet>';
|
|
||||||
$parts[] = '</Workbook>';
|
|
||||||
|
|
||||||
$output = implode('', $parts);
|
|
||||||
|
|
||||||
$response = service('response');
|
|
||||||
$response->setHeader('Content-Type', 'application/vnd.ms-excel; charset=UTF-8');
|
|
||||||
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
|
||||||
$response->setHeader('Pragma', 'no-cache');
|
|
||||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
||||||
$response->setBody($output);
|
|
||||||
$response->send();
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('export_excel_2003_xml_workbook')) {
|
|
||||||
/**
|
|
||||||
* Excel 2003 XML — 다중 시트, 인쇄 미리보기와 유사한 헤더·줄바꿈·열 너비
|
|
||||||
*
|
|
||||||
* @param string $filename 저장 파일명
|
|
||||||
* @param list<array{name: string, headers: list<string>, rows: list<list<string>>, col_widths?: list<int>}> $sheets
|
|
||||||
*/
|
|
||||||
function export_excel_2003_xml_workbook(string $filename, array $sheets): void
|
|
||||||
{
|
|
||||||
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
|
|
||||||
|
|
||||||
$esc = static function (mixed $v): string {
|
|
||||||
return htmlspecialchars((string) ($v ?? ''), ENT_XML1 | ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
||||||
};
|
|
||||||
|
|
||||||
$safeSheetName = static function (string $name) use ($esc): string {
|
|
||||||
$safe = str_replace(['/', '\\', '?', '*', '[', ']', ':'], '', $name);
|
|
||||||
$safe = function_exists('mb_substr') ? mb_substr($safe, 0, 31, 'UTF-8') : substr($safe, 0, 31);
|
|
||||||
|
|
||||||
return $esc($safe !== '' ? $safe : 'Sheet');
|
|
||||||
};
|
|
||||||
|
|
||||||
$parts = [];
|
|
||||||
$parts[] = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
||||||
$parts[] = '<?mso-application progid="Excel.Sheet"?>';
|
|
||||||
$parts[] = '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">';
|
|
||||||
$parts[] = '<Styles>';
|
|
||||||
$parts[] = '<Style ss:ID="Default"><Alignment ss:Vertical="Top" ss:WrapText="1" ss:Horizontal="Left"/><Font ss:FontName="맑은 고딕" x:CharSet="129" ss:Size="9"/></Style>';
|
|
||||||
$parts[] = '<Style ss:ID="Header"><Font ss:Bold="1" ss:Size="9" ss:FontName="맑은 고딕" x:CharSet="129"/><Interior ss:Color="#F3F4F6" ss:Pattern="Solid"/><Alignment ss:Horizontal="Left" ss:Vertical="Center" ss:WrapText="1"/><Borders><Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/></Borders></Style>';
|
|
||||||
$parts[] = '</Styles>';
|
|
||||||
|
|
||||||
foreach ($sheets as $sheet) {
|
|
||||||
$sheetName = $safeSheetName((string) ($sheet['name'] ?? 'Sheet'));
|
|
||||||
$headers = array_values((array) ($sheet['headers'] ?? []));
|
|
||||||
$rows = (array) ($sheet['rows'] ?? []);
|
|
||||||
$colWidths = array_values((array) ($sheet['col_widths'] ?? []));
|
|
||||||
|
|
||||||
$parts[] = '<Worksheet ss:Name="' . $sheetName . '">';
|
|
||||||
$parts[] = '<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel"><PageSetup><Layout x:Orientation="Landscape"/></PageSetup></WorksheetOptions>';
|
|
||||||
$parts[] = '<Table>';
|
|
||||||
|
|
||||||
$colCount = max(count($headers), 1);
|
|
||||||
for ($i = 0; $i < $colCount; $i++) {
|
|
||||||
$px = (int) ($colWidths[$i] ?? 72);
|
|
||||||
$width = max(48, min(280, $px));
|
|
||||||
$excelW = round($width / 6.5, 1);
|
|
||||||
$parts[] = '<Column ss:Index="' . ($i + 1) . '" ss:AutoFitWidth="0" ss:Width="' . $excelW . '"/>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts[] = '<Row ss:StyleID="Header">';
|
|
||||||
foreach ($headers as $h) {
|
|
||||||
$parts[] = '<Cell ss:StyleID="Header"><Data ss:Type="String">' . $esc($h) . '</Data></Cell>';
|
|
||||||
}
|
|
||||||
$parts[] = '</Row>';
|
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$parts[] = '<Row>';
|
|
||||||
foreach (array_values((array) $row) as $cell) {
|
|
||||||
$parts[] = '<Cell ss:StyleID="Default"><Data ss:Type="String">' . $esc($cell) . '</Data></Cell>';
|
|
||||||
}
|
|
||||||
$parts[] = '</Row>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts[] = '</Table>';
|
|
||||||
$parts[] = '</Worksheet>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts[] = '</Workbook>';
|
|
||||||
|
|
||||||
$output = implode('', $parts);
|
|
||||||
|
|
||||||
$response = service('response');
|
|
||||||
$response->setHeader('Content-Type', 'application/vnd.ms-excel; charset=UTF-8');
|
|
||||||
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
|
||||||
$response->setHeader('Pragma', 'no-cache');
|
|
||||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
||||||
$response->setBody($output);
|
|
||||||
$response->send();
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('bag_flow_report_build_spreadsheet')) {
|
|
||||||
/**
|
|
||||||
* 기간별 봉투 수불 엑셀 통합문서 생성 (PhpSpreadsheet — 열 너비·병합 안정)
|
|
||||||
*
|
|
||||||
* @param list<array<string, mixed>> $reportRows
|
|
||||||
* @param list<string> $metaLines
|
|
||||||
*/
|
|
||||||
function bag_flow_report_build_spreadsheet(
|
|
||||||
string $lgName,
|
|
||||||
string $title,
|
|
||||||
array $metaLines,
|
|
||||||
array $reportRows
|
|
||||||
): \PhpOffice\PhpSpreadsheet\Spreadsheet {
|
|
||||||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
|
||||||
$spreadsheet->getDefaultStyle()->getFont()->setName('맑은 고딕')->setSize(10);
|
|
||||||
$sheet = $spreadsheet->getActiveSheet();
|
|
||||||
$sheet->setTitle('수불현황');
|
|
||||||
$bodyFontSize = 10;
|
|
||||||
|
|
||||||
$lastCol = 'N';
|
|
||||||
$colWidths = [
|
|
||||||
'A' => 22.0,
|
|
||||||
'B' => 26.0,
|
|
||||||
'C' => 12.0,
|
|
||||||
'D' => 11.0,
|
|
||||||
'E' => 11.0,
|
|
||||||
'F' => 11.0,
|
|
||||||
'G' => 12.0,
|
|
||||||
'H' => 11.0,
|
|
||||||
'I' => 12.0,
|
|
||||||
'J' => 12.0,
|
|
||||||
'K' => 12.0,
|
|
||||||
'L' => 11.0,
|
|
||||||
'M' => 12.0,
|
|
||||||
'N' => 12.0,
|
|
||||||
];
|
|
||||||
foreach ($colWidths as $col => $width) {
|
|
||||||
$sheet->getColumnDimension($col)->setWidth($width);
|
|
||||||
$sheet->getColumnDimension($col)->setAutoSize(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
$r = 1;
|
|
||||||
if ($lgName !== '') {
|
|
||||||
$sheet->setCellValue("A{$r}", $lgName);
|
|
||||||
$sheet->getStyle("A{$r}")->getFont()->setSize(10)->getColor()->setRGB('666666');
|
|
||||||
$r++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sheet->setCellValue("A{$r}", $title);
|
|
||||||
$sheet->mergeCells("A{$r}:{$lastCol}{$r}");
|
|
||||||
$sheet->getStyle("A{$r}")->getFont()->setBold(true)->setSize($bodyFontSize);
|
|
||||||
$r++;
|
|
||||||
|
|
||||||
foreach ($metaLines as $line) {
|
|
||||||
$sheet->setCellValue("A{$r}", $line);
|
|
||||||
$sheet->mergeCells("A{$r}:{$lastCol}{$r}");
|
|
||||||
$sheet->getStyle("A{$r}")->getFont()->setSize(10)->getColor()->setRGB('555555');
|
|
||||||
$r++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$r++;
|
|
||||||
|
|
||||||
$h1 = $r;
|
|
||||||
$h2 = $r + 1;
|
|
||||||
|
|
||||||
$sheet->setCellValue("A{$h1}", '일자');
|
|
||||||
$sheet->mergeCells("A{$h1}:A{$h2}");
|
|
||||||
$sheet->setCellValue("B{$h1}", '품목');
|
|
||||||
$sheet->mergeCells("B{$h1}:B{$h2}");
|
|
||||||
$sheet->setCellValue("C{$h1}", '전일');
|
|
||||||
$sheet->mergeCells("C{$h1}:C{$h2}");
|
|
||||||
$sheet->setCellValue("D{$h1}", '입고');
|
|
||||||
$sheet->mergeCells("D{$h1}:G{$h1}");
|
|
||||||
$sheet->setCellValue("H{$h1}", '출고');
|
|
||||||
$sheet->mergeCells("H{$h1}:M{$h1}");
|
|
||||||
$sheet->setCellValue("N{$h1}", '잔량');
|
|
||||||
$sheet->mergeCells("N{$h1}:N{$h2}");
|
|
||||||
|
|
||||||
$subHeaders = ['입고', '반품', '기타', '입계', '판매', '일반', '무료', '반품', '기타', '출계'];
|
|
||||||
foreach ($subHeaders as $i => $label) {
|
|
||||||
$col = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex(4 + $i);
|
|
||||||
$sheet->setCellValue("{$col}{$h2}", $label);
|
|
||||||
}
|
|
||||||
|
|
||||||
$headerStyle = [
|
|
||||||
'font' => ['bold' => true, 'size' => $bodyFontSize],
|
|
||||||
'alignment' => [
|
|
||||||
'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
|
|
||||||
'vertical' => \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER,
|
|
||||||
'wrapText' => false,
|
|
||||||
],
|
|
||||||
'fill' => [
|
|
||||||
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
|
||||||
'startColor' => ['rgb' => 'E9ECEF'],
|
|
||||||
],
|
|
||||||
'borders' => [
|
|
||||||
'bottom' => ['borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$sheet->getStyle("A{$h1}:{$lastCol}{$h2}")->applyFromArray($headerStyle);
|
|
||||||
|
|
||||||
$dataRow = $h2 + 1;
|
|
||||||
foreach ($reportRows as $row) {
|
|
||||||
$rowType = (string) ($row['row_type'] ?? 'data');
|
|
||||||
if (! in_array($rowType, ['data', 'subtotal', 'grand'], true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sheet->fromArray([
|
|
||||||
(string) ($row['date'] ?? ''),
|
|
||||||
(string) ($row['item_name'] ?? ''),
|
|
||||||
(int) ($row['prev_stock'] ?? 0),
|
|
||||||
(int) ($row['recv_in'] ?? 0),
|
|
||||||
(int) ($row['recv_return'] ?? 0),
|
|
||||||
(int) ($row['recv_misc'] ?? 0),
|
|
||||||
(int) ($row['recv_total'] ?? 0),
|
|
||||||
(int) ($row['out_sale'] ?? 0),
|
|
||||||
(int) ($row['out_issue_gen'] ?? 0),
|
|
||||||
(int) ($row['out_issue_free'] ?? 0),
|
|
||||||
(int) ($row['out_return'] ?? 0),
|
|
||||||
(int) ($row['out_misc'] ?? 0),
|
|
||||||
(int) ($row['out_total'] ?? 0),
|
|
||||||
(int) ($row['balance'] ?? 0),
|
|
||||||
], null, "A{$dataRow}", true);
|
|
||||||
|
|
||||||
$sheet->getStyle("C{$dataRow}:{$lastCol}{$dataRow}")
|
|
||||||
->getNumberFormat()
|
|
||||||
->setFormatCode('#,##0');
|
|
||||||
$sheet->getStyle("C{$dataRow}:{$lastCol}{$dataRow}")
|
|
||||||
->getAlignment()
|
|
||||||
->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_RIGHT);
|
|
||||||
$sheet->getStyle("A{$dataRow}:B{$dataRow}")
|
|
||||||
->getAlignment()
|
|
||||||
->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT)
|
|
||||||
->setWrapText(false);
|
|
||||||
$sheet->getStyle("A{$dataRow}:{$lastCol}{$dataRow}")
|
|
||||||
->getFont()
|
|
||||||
->setSize($bodyFontSize);
|
|
||||||
|
|
||||||
if (in_array($rowType, ['subtotal', 'grand'], true)) {
|
|
||||||
$sheet->getStyle("A{$dataRow}:{$lastCol}{$dataRow}")->applyFromArray([
|
|
||||||
'font' => ['bold' => true, 'size' => $bodyFontSize],
|
|
||||||
'fill' => [
|
|
||||||
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
|
||||||
'startColor' => ['rgb' => 'FFF8E1'],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$dataRow++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($dataRow > $h2 + 1) {
|
|
||||||
$sheet->getStyle('A' . ($h2 + 1) . ':' . $lastCol . ($dataRow - 1))
|
|
||||||
->getBorders()
|
|
||||||
->getAllBorders()
|
|
||||||
->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_HAIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
$sheet->getPageSetup()->setOrientation(\PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT);
|
|
||||||
$sheet->getPageSetup()->setPaperSize(\PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::PAPERSIZE_A4);
|
|
||||||
$sheet->getPageSetup()->setFitToWidth(1);
|
|
||||||
$sheet->getPageSetup()->setFitToHeight(0);
|
|
||||||
|
|
||||||
return $spreadsheet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('export_bag_flow_report_excel')) {
|
|
||||||
/**
|
|
||||||
* 기간별 봉투 수불 (/bag/flow) — 인쇄와 동일한 헤더·2단 표 (xlsx, PhpSpreadsheet)
|
|
||||||
*
|
|
||||||
* @param list<array<string, mixed>> $reportRows
|
|
||||||
* @param list<string> $metaLines
|
|
||||||
*/
|
|
||||||
function export_bag_flow_report_excel(
|
|
||||||
string $filename,
|
|
||||||
string $lgName,
|
|
||||||
string $title,
|
|
||||||
array $metaLines,
|
|
||||||
array $reportRows
|
|
||||||
): void {
|
|
||||||
$baseName = preg_replace('/\.[^.]+$/u', '', $filename);
|
|
||||||
$baseName = preg_replace('/[^\p{L}\p{N}_\-]+/u', '_', $baseName) ?? 'bag_flow';
|
|
||||||
$baseName = trim($baseName, '_') !== '' ? trim($baseName, '_') : 'bag_flow';
|
|
||||||
$filename = $baseName . '.xlsx';
|
|
||||||
|
|
||||||
$spreadsheet = bag_flow_report_build_spreadsheet($lgName, $title, $metaLines, $reportRows);
|
|
||||||
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
|
|
||||||
|
|
||||||
ob_start();
|
|
||||||
try {
|
|
||||||
$writer->save('php://output');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
ob_end_clean();
|
|
||||||
$spreadsheet->disconnectWorksheets();
|
|
||||||
unset($spreadsheet);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
$spreadsheet->disconnectWorksheets();
|
|
||||||
unset($spreadsheet);
|
|
||||||
$output = ob_get_clean();
|
|
||||||
if ($output === false) {
|
|
||||||
$output = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = service('response');
|
|
||||||
$response->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
||||||
$asciiName = preg_replace('/[^\x20-\x7E]+/', '_', $filename) ?? 'bag_flow.xlsx';
|
|
||||||
$response->setHeader(
|
|
||||||
'Content-Disposition',
|
|
||||||
'attachment; filename="' . $asciiName . '"; filename*=UTF-8\'\'' . rawurlencode($filename)
|
|
||||||
);
|
|
||||||
$response->setHeader('Pragma', 'no-cache');
|
|
||||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
||||||
$response->setBody($output);
|
|
||||||
$response->send();
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! function_exists('export_xlsx')) {
|
|
||||||
/**
|
|
||||||
* Office Open XML(.xlsx) 브라우저 다운로드 (PhpSpreadsheet)
|
|
||||||
*
|
|
||||||
* @param string $filename 저장 파일명(확장자는 .xlsx로 정규화)
|
|
||||||
* @param string $sheetName 시트 이름(Excel 제한: 길이·일부 문자)
|
|
||||||
* @param string[] $headers 컬럼 헤더
|
|
||||||
* @param array $rows 데이터 행(각 행은 배열)
|
|
||||||
*/
|
|
||||||
function export_xlsx(string $filename, string $sheetName, array $headers, array $rows): void
|
|
||||||
{
|
|
||||||
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xlsx';
|
|
||||||
|
|
||||||
$safeSheet = str_replace(['/', '\\', '?', '*', '[', ']'], '', $sheetName);
|
|
||||||
$safeSheet = function_exists('mb_substr')
|
|
||||||
? mb_substr($safeSheet, 0, 31, 'UTF-8')
|
|
||||||
: substr($safeSheet, 0, 31);
|
|
||||||
if ($safeSheet === '') {
|
|
||||||
$safeSheet = 'Sheet1';
|
|
||||||
}
|
|
||||||
|
|
||||||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
|
||||||
$sheet = $spreadsheet->getActiveSheet();
|
|
||||||
$sheet->setTitle($safeSheet);
|
|
||||||
|
|
||||||
$data = [array_map(static fn ($v): string => (string) ($v ?? ''), array_values($headers))];
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$data[] = array_map(static fn ($v): string => (string) ($v ?? ''), array_values((array) $row));
|
|
||||||
}
|
|
||||||
$sheet->fromArray($data, null, 'A1', true);
|
|
||||||
|
|
||||||
$headerCount = max(1, count($headers));
|
|
||||||
$rowCount = max(1, count($data));
|
|
||||||
$lastCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($headerCount);
|
|
||||||
$fullRange = 'A1:' . $lastCol . $rowCount;
|
|
||||||
|
|
||||||
// 헤더/데이터 모두 좌측 정렬(요구사항)
|
|
||||||
$sheet->getStyle($fullRange)->getAlignment()->setHorizontal(
|
|
||||||
\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT
|
|
||||||
);
|
|
||||||
|
|
||||||
// 가독성을 위해 기본 열 너비를 넓게 지정
|
|
||||||
for ($i = 1; $i <= $headerCount; $i++) {
|
|
||||||
$col = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i);
|
|
||||||
$sheet->getColumnDimension($col)->setWidth(22);
|
|
||||||
}
|
|
||||||
|
|
||||||
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
|
|
||||||
ob_start();
|
|
||||||
try {
|
|
||||||
$writer->save('php://output');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
ob_end_clean();
|
|
||||||
$spreadsheet->disconnectWorksheets();
|
|
||||||
unset($spreadsheet);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
$spreadsheet->disconnectWorksheets();
|
|
||||||
unset($spreadsheet);
|
|
||||||
$output = ob_get_clean();
|
|
||||||
if ($output === false) {
|
|
||||||
$output = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = service('response');
|
|
||||||
$response->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
||||||
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
|
||||||
$response->setHeader('Pragma', 'no-cache');
|
|
||||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
||||||
$response->setBody($output);
|
|
||||||
$response->send();
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,660 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Libraries;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 통계 분석 관리 (전년대비 / 월별·계절별 추이)
|
|
||||||
*
|
|
||||||
* 월별·계절별 추이·전년대비: bs_type = sale 판매량·판매금액만 집계 (반품·취소 제외)
|
|
||||||
*/
|
|
||||||
class BagAnalyticsReportBuilder
|
|
||||||
{
|
|
||||||
private \CodeIgniter\Database\BaseConnection $db;
|
|
||||||
|
|
||||||
/** @var array<string, string> */
|
|
||||||
private array $bagNames = [];
|
|
||||||
|
|
||||||
/** 판매(bs_type=sale) 낱장 수량만 합산 */
|
|
||||||
private function saleQtySql(): string
|
|
||||||
{
|
|
||||||
return "CASE WHEN bs.bs_type = 'sale' THEN ABS(bs.bs_qty) ELSE 0 END";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array{label: string, months_label: string, months: list<int>, cross_year: bool}>
|
|
||||||
*/
|
|
||||||
public static function seasonCatalog(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'spring' => ['label' => '봄', 'months_label' => '3~5월', 'months' => [3, 4, 5], 'cross_year' => false],
|
|
||||||
'summer' => ['label' => '여름', 'months_label' => '6~8월', 'months' => [6, 7, 8], 'cross_year' => false],
|
|
||||||
'autumn' => ['label' => '가을', 'months_label' => '9~11월', 'months' => [9, 10, 11], 'cross_year' => false],
|
|
||||||
'winter' => ['label' => '겨울', 'months_label' => '전년12·1~2월', 'months' => [12, 1, 2], 'cross_year' => true],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function normalizeSeason(string $season): string
|
|
||||||
{
|
|
||||||
$raw = trim($season);
|
|
||||||
$aliases = [
|
|
||||||
'봄' => 'spring',
|
|
||||||
'여름' => 'summer',
|
|
||||||
'가을' => 'autumn',
|
|
||||||
'겨울' => 'winter',
|
|
||||||
];
|
|
||||||
$key = $aliases[$raw] ?? strtolower($raw);
|
|
||||||
$catalog = self::seasonCatalog();
|
|
||||||
|
|
||||||
return isset($catalog[$key]) ? $key : 'spring';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __construct(?\CodeIgniter\Database\BaseConnection $db = null)
|
|
||||||
{
|
|
||||||
$this->db = $db ?? \Config\Database::connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{
|
|
||||||
* gugunOptions: list<array{code: string, name: string}>,
|
|
||||||
* agencies: list<object>,
|
|
||||||
* lgName: string,
|
|
||||||
* gugunLabel: string
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public function loadFilterOptions(int $lgIdx): array
|
|
||||||
{
|
|
||||||
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($lgIdx);
|
|
||||||
$lgName = $lgRow ? trim((string) ($lgRow->lg_name ?? '')) : '';
|
|
||||||
|
|
||||||
$gugunRows = $this->db->query("
|
|
||||||
SELECT DISTINCT ds_gugun_code AS code
|
|
||||||
FROM designated_shop
|
|
||||||
WHERE ds_lg_idx = ? AND ds_gugun_code != ''
|
|
||||||
ORDER BY ds_gugun_code
|
|
||||||
", [$lgIdx])->getResultArray();
|
|
||||||
|
|
||||||
$gugunOptions = [['code' => '', 'name' => '전체']];
|
|
||||||
foreach ($gugunRows as $row) {
|
|
||||||
$code = trim((string) ($row['code'] ?? ''));
|
|
||||||
if ($code === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$gugunOptions[] = ['code' => $code, 'name' => $this->gugunLabel($lgIdx, $code)];
|
|
||||||
}
|
|
||||||
|
|
||||||
$agencies = model(\App\Models\SalesAgencyModel::class)
|
|
||||||
->where('sa_lg_idx', $lgIdx)
|
|
||||||
->orderForDisplay()
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'gugunOptions' => $gugunOptions,
|
|
||||||
'agencies' => $agencies,
|
|
||||||
'lgName' => $lgName,
|
|
||||||
'gugunLabel' => '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{
|
|
||||||
* rows: list<array<string, mixed>>,
|
|
||||||
* months: list<int>,
|
|
||||||
* prevYear: int,
|
|
||||||
* year: int
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public function buildYearOverYear(
|
|
||||||
int $lgIdx,
|
|
||||||
int $year,
|
|
||||||
string $gugunCode,
|
|
||||||
int $dsIdx,
|
|
||||||
bool $queried
|
|
||||||
): array {
|
|
||||||
$prevYear = $year - 1;
|
|
||||||
$months = range(1, 12);
|
|
||||||
if (! $queried) {
|
|
||||||
return ['rows' => [], 'months' => $months, 'prevYear' => $prevYear, 'year' => $year];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->loadBagNames($lgIdx);
|
|
||||||
$agg = $this->aggregateMonthlyByBag($lgIdx, $prevYear, $year, $gugunCode, $dsIdx);
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
$codesFromAgg = array_map(static fn ($c): string => (string) $c, array_keys($agg));
|
|
||||||
foreach ($this->bagCodesForReport($lgIdx, $codesFromAgg) as $code) {
|
|
||||||
$code = (string) $code;
|
|
||||||
$name = $this->resolveBagName($code);
|
|
||||||
$rows[] = $this->yoyBlock($code, (string) $name, '수량', $agg, $prevYear, $year, $months, false);
|
|
||||||
$rows[] = $this->yoyBlock($code, (string) $name, '금액', $agg, $prevYear, $year, $months, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['rows' => $rows, 'months' => $months, 'prevYear' => $prevYear, 'year' => $year];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{rows: list<array<string, mixed>>, meta: array<string, int>}
|
|
||||||
*/
|
|
||||||
public function buildMonthlyTrend(
|
|
||||||
int $lgIdx,
|
|
||||||
string $baseYm,
|
|
||||||
string $trendBasis,
|
|
||||||
float $deviationMin,
|
|
||||||
string $gugunCode,
|
|
||||||
int $saIdx,
|
|
||||||
bool $queried
|
|
||||||
): array {
|
|
||||||
$empty = ['rows' => [], 'meta' => ['shopCount' => 0, 'monthSalesShops' => 0]];
|
|
||||||
if (! $queried || ! preg_match('/^(\d{4})-(\d{2})$/', $baseYm, $m)) {
|
|
||||||
return $empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
$year = (int) $m[1];
|
|
||||||
$month = (int) $m[2];
|
|
||||||
$shops = $this->loadShops($lgIdx, $gugunCode, $saIdx);
|
|
||||||
if ($shops === []) {
|
|
||||||
return $empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
$monthlyByShop = $this->monthlyNetByShop($lgIdx, $year, $month, $gugunCode, $saIdx);
|
|
||||||
$avgByShop = $this->averageNetByShop($lgIdx, $year - 1, $gugunCode, $trendBasis, $month, $saIdx);
|
|
||||||
$prevYearSameMonth = $trendBasis === 'year_avg'
|
|
||||||
? $this->monthlyNetByShop($lgIdx, $year - 1, $month, $gugunCode, $saIdx)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
$monthSalesShops = 0;
|
|
||||||
foreach ($shops as $shop) {
|
|
||||||
$sid = (int) ($shop['ds_idx'] ?? 0);
|
|
||||||
$monthly = (float) ($monthlyByShop[$sid] ?? 0.0);
|
|
||||||
$avg = (float) ($avgByShop[$sid] ?? 0.0);
|
|
||||||
if ($trendBasis === 'year_avg' && $avg <= 0) {
|
|
||||||
$avg = (float) ($prevYearSameMonth[$sid] ?? 0.0);
|
|
||||||
}
|
|
||||||
if ($monthly > 0) {
|
|
||||||
$monthSalesShops++;
|
|
||||||
}
|
|
||||||
$diff = $monthly - $avg;
|
|
||||||
$pct = $avg > 0 ? round(($diff / $avg) * 100, 2) : ($monthly > 0 ? 100.0 : 0.0);
|
|
||||||
// 편차 N% 이상 = |편차(%)| ≥ N (증가·감소 모두)
|
|
||||||
if ($deviationMin > 0 && abs($pct) < $deviationMin) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$rows[] = [
|
|
||||||
'agency_name' => (string) ($shop['agency_name'] ?? ''),
|
|
||||||
'shop_no' => (string) ($shop['ds_shop_no'] ?? ''),
|
|
||||||
'shop_name' => (string) ($shop['ds_name'] ?? ''),
|
|
||||||
'rep_name' => (string) ($shop['ds_rep_name'] ?? ''),
|
|
||||||
'prev_avg' => (int) round($avg),
|
|
||||||
'monthly_qty' => (int) round($monthly),
|
|
||||||
'avg_diff' => (int) round($diff),
|
|
||||||
'deviation_pct'=> $pct,
|
|
||||||
'designated_at'=> (string) ($shop['ds_designated_at'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($rows, static fn (array $a, array $b): int => strcmp((string) $a['shop_no'], (string) $b['shop_no']));
|
|
||||||
|
|
||||||
return [
|
|
||||||
'rows' => $rows,
|
|
||||||
'meta' => [
|
|
||||||
'shopCount' => count($shops),
|
|
||||||
'monthSalesShops' => $monthSalesShops,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array<string, mixed>>
|
|
||||||
*/
|
|
||||||
public function buildSeasonalTrend(
|
|
||||||
int $lgIdx,
|
|
||||||
int $baseYear,
|
|
||||||
string $season,
|
|
||||||
float $deviationMin,
|
|
||||||
string $gugunCode,
|
|
||||||
bool $queried
|
|
||||||
): array {
|
|
||||||
if (! $queried) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$seasonKey = self::normalizeSeason($season);
|
|
||||||
$seasonDef = self::seasonCatalog()[$seasonKey];
|
|
||||||
$months = $seasonDef['months'];
|
|
||||||
|
|
||||||
$saIdx = 0;
|
|
||||||
$shops = $this->loadShops($lgIdx, $gugunCode, $saIdx);
|
|
||||||
if ($shops === []) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$crossYear = (bool) ($seasonDef['cross_year'] ?? false);
|
|
||||||
$currentByShop = $this->seasonalNetByShop($lgIdx, $baseYear, $months, $gugunCode, $saIdx, $crossYear);
|
|
||||||
$prevByShop = $this->seasonalNetByShop($lgIdx, $baseYear - 1, $months, $gugunCode, $saIdx, $crossYear);
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
foreach ($shops as $shop) {
|
|
||||||
$sid = (int) ($shop['ds_idx'] ?? 0);
|
|
||||||
$curr = (float) ($currentByShop[$sid] ?? 0.0);
|
|
||||||
$prev = (float) ($prevByShop[$sid] ?? 0.0);
|
|
||||||
$diff = $curr - $prev;
|
|
||||||
$pct = $prev > 0 ? round(($diff / $prev) * 100, 2) : ($curr > 0 ? 100.0 : 0.0);
|
|
||||||
if ($deviationMin > 0 && abs($pct) < $deviationMin) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$rows[] = [
|
|
||||||
'agency_name' => (string) ($shop['agency_name'] ?? ''),
|
|
||||||
'shop_name' => (string) ($shop['ds_name'] ?? ''),
|
|
||||||
'shop_no' => (string) ($shop['ds_shop_no'] ?? ''),
|
|
||||||
'rep_name' => (string) ($shop['ds_rep_name'] ?? ''),
|
|
||||||
'prev_season_avg'=> (int) round($prev),
|
|
||||||
'base_season_avg'=> (int) round($curr),
|
|
||||||
'avg_diff' => (int) round($diff),
|
|
||||||
'deviation_pct' => $pct,
|
|
||||||
'designated_at' => (string) ($shop['ds_designated_at'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($rows, static fn (array $a, array $b): int => strcmp((string) $a['shop_no'], (string) $b['shop_no']));
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function gugunLabel(int $lgIdx, string $code): string
|
|
||||||
{
|
|
||||||
static $cache = [];
|
|
||||||
$key = $lgIdx . ':' . $code;
|
|
||||||
if (isset($cache[$key])) {
|
|
||||||
return $cache[$key];
|
|
||||||
}
|
|
||||||
$row = $this->db->table('code_detail cd')
|
|
||||||
->select('cd.cd_name')
|
|
||||||
->join('code_kind ck', 'ck.ck_idx = cd.cd_ck_idx', 'inner')
|
|
||||||
->where('ck.ck_code', 'G')
|
|
||||||
->where('cd.cd_lg_idx', $lgIdx)
|
|
||||||
->where('cd.cd_code', $code)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
$cache[$key] = trim((string) ($row['cd_name'] ?? $code));
|
|
||||||
|
|
||||||
return $cache[$key];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveBagName(string $code): string
|
|
||||||
{
|
|
||||||
if (isset($this->bagNames[$code])) {
|
|
||||||
return (string) $this->bagNames[$code];
|
|
||||||
}
|
|
||||||
if (ctype_digit($code) && isset($this->bagNames[(int) $code])) {
|
|
||||||
return (string) $this->bagNames[(int) $code];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadBagNames(int $lgIdx): void
|
|
||||||
{
|
|
||||||
if ($this->bagNames !== []) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
||||||
if (! $kindO) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
foreach (model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx) as $d) {
|
|
||||||
$code = trim((string) ($d->cd_code ?? ''));
|
|
||||||
if ($code !== '') {
|
|
||||||
$this->bagNames[$code] = trim((string) ($d->cd_name ?? $code));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<string> $codesFromAgg
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
private function bagCodesForReport(int $lgIdx, array $codesFromAgg): array
|
|
||||||
{
|
|
||||||
$this->loadBagNames($lgIdx);
|
|
||||||
$codes = array_keys($this->bagNames);
|
|
||||||
if ($codesFromAgg !== []) {
|
|
||||||
$merged = array_merge($codes, $codesFromAgg);
|
|
||||||
$codes = [];
|
|
||||||
foreach ($merged as $c) {
|
|
||||||
$codes[] = (string) $c;
|
|
||||||
}
|
|
||||||
$codes = array_values(array_unique($codes));
|
|
||||||
sort($codes, SORT_STRING);
|
|
||||||
} else {
|
|
||||||
$codes = array_map(static fn ($c): string => (string) $c, $codes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $codes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array<int, array<int, array{qty: float, amt: float}>>>
|
|
||||||
*/
|
|
||||||
private function aggregateMonthlyByBag(
|
|
||||||
int $lgIdx,
|
|
||||||
int $fromYear,
|
|
||||||
int $toYear,
|
|
||||||
string $gugunCode,
|
|
||||||
int $dsIdx
|
|
||||||
): array {
|
|
||||||
$saleQty = $this->saleQtySql();
|
|
||||||
$sql = "
|
|
||||||
SELECT bs.bs_bag_code AS bag_code, YEAR(bs.bs_sale_date) AS y, MONTH(bs.bs_sale_date) AS m,
|
|
||||||
SUM({$saleQty}) AS sale_qty,
|
|
||||||
SUM(CASE WHEN bs.bs_type = 'sale' THEN bs.bs_amount ELSE 0 END) AS sale_amt
|
|
||||||
FROM bag_sale bs
|
|
||||||
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
||||||
WHERE bs.bs_lg_idx = ?
|
|
||||||
AND YEAR(bs.bs_sale_date) BETWEEN ? AND ?
|
|
||||||
";
|
|
||||||
$params = [$lgIdx, $fromYear, $toYear];
|
|
||||||
if ($gugunCode !== '') {
|
|
||||||
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
||||||
$params[] = $gugunCode;
|
|
||||||
}
|
|
||||||
if ($dsIdx > 0) {
|
|
||||||
$sql .= ' AND bs.bs_ds_idx = ?';
|
|
||||||
$params[] = $dsIdx;
|
|
||||||
}
|
|
||||||
$sql .= ' GROUP BY bs.bs_bag_code, YEAR(bs.bs_sale_date), MONTH(bs.bs_sale_date)';
|
|
||||||
|
|
||||||
$agg = [];
|
|
||||||
foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
|
|
||||||
$code = (string) ($row['bag_code'] ?? '');
|
|
||||||
$y = (int) ($row['y'] ?? 0);
|
|
||||||
$m = (int) ($row['m'] ?? 0);
|
|
||||||
if ($code === '' || $y <= 0 || $m <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$agg[$code][$y][$m] = [
|
|
||||||
'qty' => (float) ($row['sale_qty'] ?? 0),
|
|
||||||
'amt' => (float) ($row['sale_amt'] ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $agg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, array<int, array<int, array{qty: float, amt: float}>>> $agg
|
|
||||||
* @param list<int> $months
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function yoyBlock(
|
|
||||||
string $code,
|
|
||||||
string $name,
|
|
||||||
string $section,
|
|
||||||
array $agg,
|
|
||||||
int $prevYear,
|
|
||||||
int $year,
|
|
||||||
array $months,
|
|
||||||
bool $useAmount
|
|
||||||
): array {
|
|
||||||
$key = $useAmount ? 'amt' : 'qty';
|
|
||||||
$lines = [];
|
|
||||||
foreach ([$prevYear => (string) $prevYear . '년', $year => (string) $year . '년', 0 => '증감'] as $y => $label) {
|
|
||||||
$monthVals = [];
|
|
||||||
$total = 0.0;
|
|
||||||
foreach ($months as $mo) {
|
|
||||||
$v = 0.0;
|
|
||||||
if ($y === 0) {
|
|
||||||
$p = (float) ($agg[$code][$prevYear][$mo][$key] ?? 0);
|
|
||||||
$c = (float) ($agg[$code][$year][$mo][$key] ?? 0);
|
|
||||||
$v = $c - $p;
|
|
||||||
} else {
|
|
||||||
$v = (float) ($agg[$code][$y][$mo][$key] ?? 0);
|
|
||||||
}
|
|
||||||
$monthVals[$mo] = (int) round($v);
|
|
||||||
$total += $v;
|
|
||||||
}
|
|
||||||
$lines[] = ['label' => $label, 'months' => $monthVals, 'total' => (int) round($total)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'bag_code' => $code,
|
|
||||||
'bag_name' => $name,
|
|
||||||
'section' => $section,
|
|
||||||
'lines' => $lines,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array<string, mixed>>
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @return array<string, string> 구·군코드 → 대행소명
|
|
||||||
*/
|
|
||||||
private function agencyNameByGugun(int $lgIdx): array
|
|
||||||
{
|
|
||||||
$best = [];
|
|
||||||
foreach ($this->db->query("
|
|
||||||
SELECT TRIM(bo.bo_gugun_code) AS code, bo.bo_agency_idx AS sa_idx, COUNT(*) AS cnt
|
|
||||||
FROM bag_order bo
|
|
||||||
WHERE bo.bo_lg_idx = ?
|
|
||||||
AND bo.bo_status = 'normal'
|
|
||||||
AND bo.bo_agency_idx IS NOT NULL
|
|
||||||
AND TRIM(bo.bo_gugun_code) != ''
|
|
||||||
GROUP BY TRIM(bo.bo_gugun_code), bo.bo_agency_idx
|
|
||||||
", [$lgIdx])->getResultArray() as $row) {
|
|
||||||
$code = (string) ($row['code'] ?? '');
|
|
||||||
$cnt = (int) ($row['cnt'] ?? 0);
|
|
||||||
if ($code === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (! isset($best[$code]) || $cnt > $best[$code]['cnt']) {
|
|
||||||
$best[$code] = ['cnt' => $cnt, 'sa_idx' => (int) ($row['sa_idx'] ?? 0)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$names = [];
|
|
||||||
foreach ($best as $code => $info) {
|
|
||||||
$sa = model(\App\Models\SalesAgencyModel::class)->find($info['sa_idx']);
|
|
||||||
$names[$code] = $sa ? trim((string) ($sa->sa_name ?? '')) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $names;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadShops(int $lgIdx, string $gugunCode, int $saIdx = 0): array
|
|
||||||
{
|
|
||||||
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
|
||||||
$agencyByGugun = $this->agencyNameByGugun($lgIdx);
|
|
||||||
|
|
||||||
$sql = '
|
|
||||||
SELECT ds.ds_idx, ds.ds_shop_no, ds.ds_name, ds.ds_rep_name, ds.ds_designated_at,
|
|
||||||
ds.ds_gugun_code';
|
|
||||||
if ($hasDsSa) {
|
|
||||||
$sql .= ', ds.ds_sa_idx';
|
|
||||||
}
|
|
||||||
$sql .= '
|
|
||||||
FROM designated_shop ds
|
|
||||||
WHERE ds.ds_lg_idx = ? AND ds.ds_state = 1
|
|
||||||
';
|
|
||||||
$params = [$lgIdx];
|
|
||||||
|
|
||||||
if ($gugunCode !== '') {
|
|
||||||
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
||||||
$params[] = $gugunCode;
|
|
||||||
}
|
|
||||||
if ($saIdx > 0 && $hasDsSa) {
|
|
||||||
$sql .= ' AND ds.ds_sa_idx = ?';
|
|
||||||
$params[] = $saIdx;
|
|
||||||
}
|
|
||||||
$sql .= ' ORDER BY ds.ds_shop_no ASC, ds.ds_idx ASC';
|
|
||||||
|
|
||||||
$rows = $this->db->query($sql, $params)->getResultArray();
|
|
||||||
$saNames = [];
|
|
||||||
if ($hasDsSa) {
|
|
||||||
foreach (model(\App\Models\SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->findAll() as $sa) {
|
|
||||||
$saNames[(int) ($sa->sa_idx ?? 0)] = trim((string) ($sa->sa_name ?? ''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($rows as &$row) {
|
|
||||||
$name = '';
|
|
||||||
if ($hasDsSa) {
|
|
||||||
$saidx = (int) ($row['ds_sa_idx'] ?? 0);
|
|
||||||
$name = $saNames[$saidx] ?? '';
|
|
||||||
}
|
|
||||||
if ($name === '') {
|
|
||||||
$code = trim((string) ($row['ds_gugun_code'] ?? ''));
|
|
||||||
$name = $agencyByGugun[$code] ?? '';
|
|
||||||
}
|
|
||||||
$row['agency_name'] = $name;
|
|
||||||
}
|
|
||||||
unset($row);
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, float>
|
|
||||||
*/
|
|
||||||
private function monthlyNetByShop(int $lgIdx, int $year, int $month, string $gugunCode, int $saIdx = 0): array
|
|
||||||
{
|
|
||||||
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
|
||||||
$saleQty = $this->saleQtySql();
|
|
||||||
$sql = "
|
|
||||||
SELECT bs.bs_ds_idx AS ds_idx,
|
|
||||||
SUM({$saleQty}) AS sale_qty
|
|
||||||
FROM bag_sale bs
|
|
||||||
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
||||||
WHERE bs.bs_lg_idx = ? AND YEAR(bs.bs_sale_date) = ? AND MONTH(bs.bs_sale_date) = ?
|
|
||||||
AND bs.bs_ds_idx IS NOT NULL
|
|
||||||
";
|
|
||||||
$params = [$lgIdx, $year, $month];
|
|
||||||
if ($gugunCode !== '') {
|
|
||||||
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
||||||
$params[] = $gugunCode;
|
|
||||||
}
|
|
||||||
if ($saIdx > 0 && $hasDsSa) {
|
|
||||||
$sql .= ' AND ds.ds_sa_idx = ?';
|
|
||||||
$params[] = $saIdx;
|
|
||||||
}
|
|
||||||
$sql .= ' GROUP BY bs.bs_ds_idx';
|
|
||||||
|
|
||||||
$map = [];
|
|
||||||
foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
|
|
||||||
$map[(int) ($row['ds_idx'] ?? 0)] = (float) ($row['sale_qty'] ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, float>
|
|
||||||
*/
|
|
||||||
private function averageNetByShop(
|
|
||||||
int $lgIdx,
|
|
||||||
int $year,
|
|
||||||
string $gugunCode,
|
|
||||||
string $trendBasis,
|
|
||||||
int $refMonth,
|
|
||||||
int $saIdx = 0
|
|
||||||
): array {
|
|
||||||
if ($trendBasis === 'month') {
|
|
||||||
return $this->monthlyNetByShop($lgIdx, $year, $refMonth, $gugunCode, $saIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
|
||||||
$saleQty = $this->saleQtySql();
|
|
||||||
$sql = "
|
|
||||||
SELECT bs.bs_ds_idx AS ds_idx,
|
|
||||||
SUM({$saleQty}) / 12 AS avg_qty
|
|
||||||
FROM bag_sale bs
|
|
||||||
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
||||||
WHERE bs.bs_lg_idx = ? AND YEAR(bs.bs_sale_date) = ?
|
|
||||||
AND bs.bs_ds_idx IS NOT NULL
|
|
||||||
";
|
|
||||||
$params = [$lgIdx, $year];
|
|
||||||
if ($gugunCode !== '') {
|
|
||||||
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
||||||
$params[] = $gugunCode;
|
|
||||||
}
|
|
||||||
if ($saIdx > 0 && $hasDsSa) {
|
|
||||||
$sql .= ' AND ds.ds_sa_idx = ?';
|
|
||||||
$params[] = $saIdx;
|
|
||||||
}
|
|
||||||
$sql .= ' GROUP BY bs.bs_ds_idx';
|
|
||||||
|
|
||||||
$map = [];
|
|
||||||
foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
|
|
||||||
$map[(int) ($row['ds_idx'] ?? 0)] = (float) ($row['avg_qty'] ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<int> $months
|
|
||||||
* @return array<int, float>
|
|
||||||
*/
|
|
||||||
private function seasonalNetByShop(
|
|
||||||
int $lgIdx,
|
|
||||||
int $year,
|
|
||||||
array $months,
|
|
||||||
string $gugunCode,
|
|
||||||
int $saIdx = 0,
|
|
||||||
bool $crossYearWinter = false
|
|
||||||
): array {
|
|
||||||
if ($months === []) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$divisor = count($months);
|
|
||||||
$saleQty = $this->saleQtySql();
|
|
||||||
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
|
||||||
|
|
||||||
if ($crossYearWinter) {
|
|
||||||
$sql = "
|
|
||||||
SELECT bs.bs_ds_idx AS ds_idx,
|
|
||||||
SUM({$saleQty}) / ? AS avg_qty
|
|
||||||
FROM bag_sale bs
|
|
||||||
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
||||||
WHERE bs.bs_lg_idx = ?
|
|
||||||
AND bs.bs_ds_idx IS NOT NULL
|
|
||||||
AND (
|
|
||||||
(YEAR(bs.bs_sale_date) = ? AND MONTH(bs.bs_sale_date) = 12)
|
|
||||||
OR (YEAR(bs.bs_sale_date) = ? AND MONTH(bs.bs_sale_date) IN (1, 2))
|
|
||||||
)
|
|
||||||
";
|
|
||||||
$params = [$divisor, $lgIdx, $year - 1, $year];
|
|
||||||
} else {
|
|
||||||
$placeholders = implode(',', array_fill(0, count($months), '?'));
|
|
||||||
$sql = "
|
|
||||||
SELECT bs.bs_ds_idx AS ds_idx,
|
|
||||||
SUM({$saleQty}) / ? AS avg_qty
|
|
||||||
FROM bag_sale bs
|
|
||||||
INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx
|
|
||||||
WHERE bs.bs_lg_idx = ? AND YEAR(bs.bs_sale_date) = ?
|
|
||||||
AND MONTH(bs.bs_sale_date) IN ({$placeholders})
|
|
||||||
AND bs.bs_ds_idx IS NOT NULL
|
|
||||||
";
|
|
||||||
$params = array_merge([$divisor], [$lgIdx, $year], $months);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($gugunCode !== '') {
|
|
||||||
$sql .= ' AND ds.ds_gugun_code = ?';
|
|
||||||
$params[] = $gugunCode;
|
|
||||||
}
|
|
||||||
if ($saIdx > 0 && $hasDsSa) {
|
|
||||||
$sql .= ' AND ds.ds_sa_idx = ?';
|
|
||||||
$params[] = $saIdx;
|
|
||||||
}
|
|
||||||
$sql .= ' GROUP BY bs.bs_ds_idx';
|
|
||||||
|
|
||||||
$map = [];
|
|
||||||
foreach ($this->db->query($sql, $params)->getResultArray() as $row) {
|
|
||||||
$map[(int) ($row['ds_idx'] ?? 0)] = (float) ($row['avg_qty'] ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Libraries;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 기간별 봉투 수불 현황 집계 (bag/flow)
|
|
||||||
*/
|
|
||||||
class BagFlowReportBuilder
|
|
||||||
{
|
|
||||||
private \CodeIgniter\Database\BaseConnection $db;
|
|
||||||
|
|
||||||
public function __construct(?\CodeIgniter\Database\BaseConnection $db = null)
|
|
||||||
{
|
|
||||||
$this->db = $db ?? \Config\Database::connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function bagCodeKey(mixed $code): string
|
|
||||||
{
|
|
||||||
return (string) $code;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{
|
|
||||||
* rows: list<array<string, mixed>>,
|
|
||||||
* bagKindLabels: array<string, string>,
|
|
||||||
* queried: bool
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public function build(
|
|
||||||
int $lgIdx,
|
|
||||||
string $startDate,
|
|
||||||
string $endDate,
|
|
||||||
string $aggMode,
|
|
||||||
string $bagCodeFilter,
|
|
||||||
string $bagKindFilter,
|
|
||||||
int $saIdx,
|
|
||||||
bool $queried
|
|
||||||
): array {
|
|
||||||
$bagKindLabels = $this->loadBagKindLabels();
|
|
||||||
$products = $this->loadProducts($lgIdx, $bagCodeFilter, $bagKindFilter);
|
|
||||||
if ($products === [] || ! $queried) {
|
|
||||||
return ['rows' => [], 'bagKindLabels' => $bagKindLabels, 'queried' => $queried];
|
|
||||||
}
|
|
||||||
|
|
||||||
$codes = array_keys($products);
|
|
||||||
$dayBefore = date('Y-m-d', strtotime($startDate . ' -1 day'));
|
|
||||||
$openingRaw = $this->aggregateMovements($lgIdx, $codes, $saIdx, null, $dayBefore);
|
|
||||||
$opening = $this->collapseOpeningBalances($openingRaw);
|
|
||||||
$periodMoves = $this->aggregateMovements($lgIdx, $codes, $saIdx, $startDate, $endDate);
|
|
||||||
|
|
||||||
if ($aggMode === 'daily') {
|
|
||||||
$rows = $this->buildDailyRows($products, $opening, $periodMoves, $startDate, $endDate, $bagKindLabels);
|
|
||||||
} else {
|
|
||||||
$rows = $this->buildPeriodRows($products, $opening, $periodMoves, $startDate, $endDate, $bagKindLabels);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['rows' => $rows, 'bagKindLabels' => $bagKindLabels, 'queried' => true];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string> code => name
|
|
||||||
*/
|
|
||||||
private function loadProducts(int $lgIdx, string $bagCodeFilter, string $bagKindFilter): array
|
|
||||||
{
|
|
||||||
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
||||||
if (! $kindO) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$details = model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx);
|
|
||||||
$products = [];
|
|
||||||
foreach ($details as $d) {
|
|
||||||
$code = (string) ($d->cd_code ?? '');
|
|
||||||
if ($code === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($bagCodeFilter !== '' && $code !== $bagCodeFilter) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($bagKindFilter !== '' && ! str_starts_with($code, $bagKindFilter)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$products[self::bagCodeKey($code)] = (string) ($d->cd_name ?? $code);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $products;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
private function loadBagKindLabels(): array
|
|
||||||
{
|
|
||||||
$kindE = model(\App\Models\CodeKindModel::class)->where('ck_code', 'E')->first();
|
|
||||||
if (! $kindE) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$labels = [];
|
|
||||||
foreach (model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindE->ck_idx, true, null) as $d) {
|
|
||||||
$labels[(string) $d->cd_code] = (string) $d->cd_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<string> $codes
|
|
||||||
* @return array<string, array<string, array<string, int>>> bag_code => date => metrics
|
|
||||||
*/
|
|
||||||
private function aggregateMovements(
|
|
||||||
int $lgIdx,
|
|
||||||
array $codes,
|
|
||||||
int $saIdx,
|
|
||||||
?string $fromDate,
|
|
||||||
?string $toDate
|
|
||||||
): array {
|
|
||||||
if ($codes === []) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$buckets = [];
|
|
||||||
$ensure = static function (string $code, string $date) use (&$buckets): array {
|
|
||||||
if (! isset($buckets[$code][$date])) {
|
|
||||||
$buckets[$code][$date] = self::emptyMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $buckets[$code][$date];
|
|
||||||
};
|
|
||||||
|
|
||||||
$hasMisc = $this->db->query("SHOW TABLES LIKE 'bag_misc_flow'")->getNumRows() > 0;
|
|
||||||
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
|
||||||
$codePlaceholders = implode(',', array_fill(0, count($codes), '?'));
|
|
||||||
|
|
||||||
// 입고(발주 입고)
|
|
||||||
$sql = "
|
|
||||||
SELECT br_bag_code AS bag_code, br_receive_date AS mv_date, SUM(br_qty_sheet) AS qty
|
|
||||||
FROM bag_receiving
|
|
||||||
WHERE br_lg_idx = ? AND br_bag_code IN ({$codePlaceholders})
|
|
||||||
";
|
|
||||||
$params = array_merge([$lgIdx], $codes);
|
|
||||||
if ($fromDate !== null) {
|
|
||||||
$sql .= ' AND br_receive_date >= ?';
|
|
||||||
$params[] = $fromDate;
|
|
||||||
}
|
|
||||||
if ($toDate !== null) {
|
|
||||||
$sql .= ' AND br_receive_date <= ?';
|
|
||||||
$params[] = $toDate;
|
|
||||||
}
|
|
||||||
$sql .= ' GROUP BY br_bag_code, br_receive_date';
|
|
||||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
|
||||||
$code = (string) $row->bag_code;
|
|
||||||
$date = (string) $row->mv_date;
|
|
||||||
$m = $ensure($code, $date);
|
|
||||||
$m['recv_in'] += (int) $row->qty;
|
|
||||||
$buckets[$code][$date] = $m;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 판매·반품(반품=입고)
|
|
||||||
$sql = "
|
|
||||||
SELECT bs.bs_bag_code AS bag_code, bs.bs_sale_date AS mv_date, bs.bs_type AS mv_type,
|
|
||||||
SUM(ABS(bs.bs_qty)) AS qty
|
|
||||||
FROM bag_sale bs
|
|
||||||
";
|
|
||||||
if ($saIdx > 0 && $hasDsSa) {
|
|
||||||
$sql .= ' INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx AND ds.ds_sa_idx = ?';
|
|
||||||
}
|
|
||||||
$sql .= " WHERE bs.bs_lg_idx = ? AND bs.bs_bag_code IN ({$codePlaceholders})";
|
|
||||||
$params = $saIdx > 0 && $hasDsSa ? [$saIdx, $lgIdx] : [$lgIdx];
|
|
||||||
$params = array_merge($params, $codes);
|
|
||||||
if ($fromDate !== null) {
|
|
||||||
$sql .= ' AND bs.bs_sale_date >= ?';
|
|
||||||
$params[] = $fromDate;
|
|
||||||
}
|
|
||||||
if ($toDate !== null) {
|
|
||||||
$sql .= ' AND bs.bs_sale_date <= ?';
|
|
||||||
$params[] = $toDate;
|
|
||||||
}
|
|
||||||
$sql .= ' GROUP BY bs.bs_bag_code, bs.bs_sale_date, bs.bs_type';
|
|
||||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
|
||||||
$code = (string) $row->bag_code;
|
|
||||||
$date = (string) $row->mv_date;
|
|
||||||
$qty = (int) $row->qty;
|
|
||||||
$m = $ensure($code, $date);
|
|
||||||
$type = (string) $row->mv_type;
|
|
||||||
if ($type === 'return') {
|
|
||||||
$m['recv_return'] += $qty;
|
|
||||||
} else {
|
|
||||||
$m['out_sale'] += $qty;
|
|
||||||
}
|
|
||||||
$buckets[$code][$date] = $m;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 불출
|
|
||||||
$sql = "
|
|
||||||
SELECT bi2_bag_code AS bag_code, bi2_issue_date AS mv_date, bi2_issue_type AS issue_type,
|
|
||||||
SUM(bi2_qty) AS qty
|
|
||||||
FROM bag_issue
|
|
||||||
WHERE bi2_lg_idx = ? AND bi2_status = 'normal' AND bi2_bag_code IN ({$codePlaceholders})
|
|
||||||
";
|
|
||||||
$params = array_merge([$lgIdx], $codes);
|
|
||||||
if ($fromDate !== null) {
|
|
||||||
$sql .= ' AND bi2_issue_date >= ?';
|
|
||||||
$params[] = $fromDate;
|
|
||||||
}
|
|
||||||
if ($toDate !== null) {
|
|
||||||
$sql .= ' AND bi2_issue_date <= ?';
|
|
||||||
$params[] = $toDate;
|
|
||||||
}
|
|
||||||
$sql .= ' GROUP BY bi2_bag_code, bi2_issue_date, bi2_issue_type';
|
|
||||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
|
||||||
$code = (string) $row->bag_code;
|
|
||||||
$date = (string) $row->mv_date;
|
|
||||||
$qty = (int) $row->qty;
|
|
||||||
$m = $ensure($code, $date);
|
|
||||||
$issueType = (string) $row->issue_type;
|
|
||||||
if (str_contains($issueType, '무료')) {
|
|
||||||
$m['out_issue_free'] += $qty;
|
|
||||||
} else {
|
|
||||||
$m['out_issue_gen'] += $qty;
|
|
||||||
}
|
|
||||||
$buckets[$code][$date] = $m;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($hasMisc) {
|
|
||||||
$sql = "
|
|
||||||
SELECT bmf_bag_code AS bag_code, bmf_date AS mv_date, bmf_type AS mv_type,
|
|
||||||
SUM(bmf_qty) AS qty
|
|
||||||
FROM bag_misc_flow
|
|
||||||
WHERE bmf_lg_idx = ? AND bmf_bag_code IN ({$codePlaceholders})
|
|
||||||
";
|
|
||||||
$params = array_merge([$lgIdx], $codes);
|
|
||||||
if ($fromDate !== null) {
|
|
||||||
$sql .= ' AND bmf_date >= ?';
|
|
||||||
$params[] = $fromDate;
|
|
||||||
}
|
|
||||||
if ($toDate !== null) {
|
|
||||||
$sql .= ' AND bmf_date <= ?';
|
|
||||||
$params[] = $toDate;
|
|
||||||
}
|
|
||||||
$sql .= ' GROUP BY bmf_bag_code, bmf_date, bmf_type';
|
|
||||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
|
||||||
$code = (string) $row->bag_code;
|
|
||||||
$date = (string) $row->mv_date;
|
|
||||||
$qty = (int) $row->qty;
|
|
||||||
$m = $ensure($code, $date);
|
|
||||||
if ((string) $row->mv_type === 'in') {
|
|
||||||
$m['recv_misc'] += $qty;
|
|
||||||
} else {
|
|
||||||
$m['out_misc'] += $qty;
|
|
||||||
}
|
|
||||||
$buckets[$code][$date] = $m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$normalized = [];
|
|
||||||
foreach ($buckets as $code => $byDate) {
|
|
||||||
$key = self::bagCodeKey($code);
|
|
||||||
foreach ($byDate as $date => $m) {
|
|
||||||
$normalized[$key][$date] = self::finalizeMetrics($m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, string> $products
|
|
||||||
* @param array<string, array<string, int>> $opening date key '_open'
|
|
||||||
* @param array<string, array<string, array<string, int>>> $periodMoves
|
|
||||||
* @param array<string, string> $bagKindLabels
|
|
||||||
* @return list<array<string, mixed>>
|
|
||||||
*/
|
|
||||||
private function buildPeriodRows(
|
|
||||||
array $products,
|
|
||||||
array $opening,
|
|
||||||
array $periodMoves,
|
|
||||||
string $startDate,
|
|
||||||
string $endDate,
|
|
||||||
array $bagKindLabels
|
|
||||||
): array {
|
|
||||||
$periodKey = $startDate . '~' . $endDate;
|
|
||||||
$grouped = [];
|
|
||||||
foreach ($products as $codeKey => $name) {
|
|
||||||
$code = self::bagCodeKey($codeKey);
|
|
||||||
$kind = strlen($code) >= 2 ? substr($code, 0, 2) : '';
|
|
||||||
$grouped[$kind][] = ['code' => $code, 'name' => $name];
|
|
||||||
}
|
|
||||||
ksort($grouped);
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
$grand = self::emptyMetrics();
|
|
||||||
$grand['row_type'] = 'grand';
|
|
||||||
$grand['date'] = '';
|
|
||||||
$grand['item_name'] = '총계';
|
|
||||||
|
|
||||||
foreach ($grouped as $kind => $items) {
|
|
||||||
$sub = self::emptyMetrics();
|
|
||||||
$sub['row_type'] = 'subtotal';
|
|
||||||
$sub['date'] = '';
|
|
||||||
$sub['item_name'] = ($bagKindLabels[$kind] ?? '기타') . ' 소계';
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$code = self::bagCodeKey($item['code']);
|
|
||||||
$m = self::emptyMetrics();
|
|
||||||
foreach ($periodMoves[$code] ?? [] as $dayMetrics) {
|
|
||||||
$m = self::mergeMetrics($m, $dayMetrics);
|
|
||||||
}
|
|
||||||
$m = self::finalizeMetrics($m);
|
|
||||||
$m['prev_stock'] = (int) ($opening[$code] ?? 0);
|
|
||||||
$m['balance'] = $m['prev_stock'] + $m['recv_total'] - $m['out_total'];
|
|
||||||
$m['row_type'] = 'data';
|
|
||||||
$m['date'] = $periodKey;
|
|
||||||
$m['item_name'] = $item['name'];
|
|
||||||
$m['bag_code'] = $code;
|
|
||||||
$m['bag_kind'] = $kind;
|
|
||||||
$rows[] = $m;
|
|
||||||
$sub = self::mergeMetrics($sub, $m);
|
|
||||||
}
|
|
||||||
|
|
||||||
$sub = self::finalizeMetrics($sub);
|
|
||||||
$sub['balance'] = $sub['prev_stock'] + $sub['recv_total'] - $sub['out_total'];
|
|
||||||
$rows[] = $sub;
|
|
||||||
$grand = self::mergeMetrics($grand, $sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
$grand = self::finalizeMetrics($grand);
|
|
||||||
$grand['balance'] = $grand['prev_stock'] + $grand['recv_total'] - $grand['out_total'];
|
|
||||||
$rows[] = $grand;
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, array<string, array<string, int>>> $openingRaw
|
|
||||||
* @return array<string, int> bag_code => 전일(기간 전) 재고
|
|
||||||
*/
|
|
||||||
private function collapseOpeningBalances(array $openingRaw): array
|
|
||||||
{
|
|
||||||
$out = [];
|
|
||||||
foreach ($openingRaw as $code => $byDate) {
|
|
||||||
$net = self::emptyMetrics();
|
|
||||||
foreach ($byDate as $m) {
|
|
||||||
$net = self::mergeMetrics($net, $m);
|
|
||||||
}
|
|
||||||
$net = self::finalizeMetrics($net);
|
|
||||||
$out[self::bagCodeKey($code)] = $net['recv_total'] - $net['out_total'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, string> $products
|
|
||||||
* @param array<string, array<string, array<string, int>>> $opening
|
|
||||||
* @param array<string, array<string, array<string, int>>> $periodMoves
|
|
||||||
* @param array<string, string> $bagKindLabels
|
|
||||||
* @return list<array<string, mixed>>
|
|
||||||
*/
|
|
||||||
private function buildDailyRows(
|
|
||||||
array $products,
|
|
||||||
array $opening,
|
|
||||||
array $periodMoves,
|
|
||||||
string $startDate,
|
|
||||||
string $endDate,
|
|
||||||
array $bagKindLabels
|
|
||||||
): array {
|
|
||||||
$dates = [];
|
|
||||||
$cursor = strtotime($startDate);
|
|
||||||
$endTs = strtotime($endDate);
|
|
||||||
while ($cursor <= $endTs) {
|
|
||||||
$dates[] = date('Y-m-d', $cursor);
|
|
||||||
$cursor = strtotime('+1 day', $cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
foreach ($products as $codeKey => $name) {
|
|
||||||
$code = self::bagCodeKey($codeKey);
|
|
||||||
$kind = strlen($code) >= 2 ? substr($code, 0, 2) : '';
|
|
||||||
$running = (int) ($opening[$code] ?? 0);
|
|
||||||
foreach ($dates as $date) {
|
|
||||||
$dayM = $periodMoves[$code][$date] ?? self::emptyMetrics();
|
|
||||||
$dayM = self::finalizeMetrics($dayM);
|
|
||||||
$prev = $running;
|
|
||||||
$running = $prev + $dayM['recv_total'] - $dayM['out_total'];
|
|
||||||
$dayM['prev_stock'] = $prev;
|
|
||||||
$dayM['balance'] = $running;
|
|
||||||
$dayM['row_type'] = 'data';
|
|
||||||
$dayM['date'] = $date;
|
|
||||||
$dayM['item_name'] = $name;
|
|
||||||
$dayM['bag_code'] = $code;
|
|
||||||
$dayM['bag_kind'] = $kind;
|
|
||||||
if ($this->rowHasActivity($dayM)) {
|
|
||||||
$rows[] = $dayM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, int|float> $m
|
|
||||||
*/
|
|
||||||
private function rowHasActivity(array $m): bool
|
|
||||||
{
|
|
||||||
foreach (['recv_in', 'recv_return', 'recv_misc', 'out_sale', 'out_issue_gen', 'out_issue_free', 'out_return', 'out_misc'] as $k) {
|
|
||||||
if ((int) ($m[$k] ?? 0) !== 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) ($m['prev_stock'] ?? 0) !== 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, int>
|
|
||||||
*/
|
|
||||||
private static function emptyMetrics(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'prev_stock' => 0,
|
|
||||||
'recv_in' => 0,
|
|
||||||
'recv_return' => 0,
|
|
||||||
'recv_misc' => 0,
|
|
||||||
'recv_total' => 0,
|
|
||||||
'out_sale' => 0,
|
|
||||||
'out_issue_gen' => 0,
|
|
||||||
'out_issue_free' => 0,
|
|
||||||
'out_return' => 0,
|
|
||||||
'out_misc' => 0,
|
|
||||||
'out_total' => 0,
|
|
||||||
'balance' => 0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, int> $m
|
|
||||||
* @return array<string, int>
|
|
||||||
*/
|
|
||||||
private static function finalizeMetrics(array $m): array
|
|
||||||
{
|
|
||||||
$m['recv_total'] = (int) $m['recv_in'] + (int) $m['recv_return'] + (int) $m['recv_misc'];
|
|
||||||
$m['out_total'] = (int) $m['out_sale'] + (int) $m['out_issue_gen'] + (int) $m['out_issue_free']
|
|
||||||
+ (int) $m['out_return'] + (int) $m['out_misc'];
|
|
||||||
|
|
||||||
return $m;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, int> $a
|
|
||||||
* @param array<string, int> $b
|
|
||||||
* @return array<string, int>
|
|
||||||
*/
|
|
||||||
private static function mergeMetrics(array $a, array $b): array
|
|
||||||
{
|
|
||||||
foreach (self::emptyMetrics() as $k => $_) {
|
|
||||||
$a[$k] = (int) ($a[$k] ?? 0) + (int) ($b[$k] ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,900 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Libraries;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LOT 수불 조회 (레거시 w_gd033a)
|
|
||||||
* — 바코드(팩/박스/낱장) 또는 LOT 번호로 일자·입출고처·구분 이력
|
|
||||||
*/
|
|
||||||
class BagLotFlowBuilder
|
|
||||||
{
|
|
||||||
private \CodeIgniter\Database\BaseConnection $db;
|
|
||||||
|
|
||||||
public function __construct(?\CodeIgniter\Database\BaseConnection $db = null)
|
|
||||||
{
|
|
||||||
$this->db = $db ?? \Config\Database::connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{
|
|
||||||
* ok: bool,
|
|
||||||
* message: string,
|
|
||||||
* barcode: string,
|
|
||||||
* unit: string,
|
|
||||||
* bag_code: string,
|
|
||||||
* bag_name: string,
|
|
||||||
* lot_no: string,
|
|
||||||
* box_code: string,
|
|
||||||
* pack_code: string,
|
|
||||||
* qty_box: int,
|
|
||||||
* qty_pack: int,
|
|
||||||
* qty_sheet: int,
|
|
||||||
* rows: list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public function buildByBarcode(int $lgIdx, string $barcode, bool $queried): array
|
|
||||||
{
|
|
||||||
$empty = $this->emptyResult($barcode);
|
|
||||||
if (! $queried || trim($barcode) === '') {
|
|
||||||
return $empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
$resolved = $this->resolveBarcode($lgIdx, trim($barcode));
|
|
||||||
if (! $resolved['ok']) {
|
|
||||||
return array_merge($empty, [
|
|
||||||
'message' => (string) ($resolved['message'] ?? '등록되지 않은 바코드입니다.'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = $this->collectFlowRows($lgIdx, $resolved);
|
|
||||||
usort($rows, static fn (array $a, array $b): int => ($a['sort_ts'] ?? 0) <=> ($b['sort_ts'] ?? 0));
|
|
||||||
|
|
||||||
return array_merge($empty, [
|
|
||||||
'ok' => true,
|
|
||||||
'message' => '',
|
|
||||||
'barcode' => (string) ($resolved['barcode'] ?? $barcode),
|
|
||||||
'unit' => (string) ($resolved['unit'] ?? ''),
|
|
||||||
'bag_code' => (string) ($resolved['bag_code'] ?? ''),
|
|
||||||
'bag_name' => (string) ($resolved['bag_name'] ?? ''),
|
|
||||||
'lot_no' => (string) ($resolved['lot_no'] ?? ''),
|
|
||||||
'box_code' => (string) ($resolved['box_code'] ?? ''),
|
|
||||||
'pack_code' => (string) ($resolved['pack_code'] ?? ''),
|
|
||||||
'qty_box' => (int) ($resolved['qty_box'] ?? 0),
|
|
||||||
'qty_pack' => (int) ($resolved['qty_pack'] ?? 0),
|
|
||||||
'qty_sheet' => (int) ($resolved['qty_sheet'] ?? 0),
|
|
||||||
'rows' => $rows,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function buildByLotNo(int $lgIdx, string $lotNo, bool $queried): array
|
|
||||||
{
|
|
||||||
$empty = $this->emptyResult('');
|
|
||||||
if (! $queried || trim($lotNo) === '') {
|
|
||||||
return $empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lotNo = trim($lotNo);
|
|
||||||
if (! $this->db->tableExists('bag_receiving_pack_code')) {
|
|
||||||
return array_merge($empty, ['message' => '바코드(팩) 데이터가 없습니다.']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$packRows = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->select('brpc_pack_code, brpc_box_code, brpc_bag_code, brpc_bag_name, brpc_lot_no')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_lot_no', $lotNo)
|
|
||||||
->limit(500)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
|
|
||||||
if ($packRows === []) {
|
|
||||||
$order = $this->db->table('bag_order')
|
|
||||||
->where('bo_lg_idx', $lgIdx)
|
|
||||||
->where('bo_lot_no', $lotNo)
|
|
||||||
->orderBy('bo_version', 'DESC')
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if (! $order) {
|
|
||||||
return array_merge($empty, ['message' => '해당 LOT·바코드를 찾을 수 없습니다.', 'lot_no' => $lotNo]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->buildLotFromOrderOnly($lgIdx, $lotNo, $order);
|
|
||||||
}
|
|
||||||
|
|
||||||
$codes = [];
|
|
||||||
$bagCode = '';
|
|
||||||
$bagName = '';
|
|
||||||
foreach ($packRows as $p) {
|
|
||||||
$codes[] = (string) ($p['brpc_pack_code'] ?? '');
|
|
||||||
$box = (string) ($p['brpc_box_code'] ?? '');
|
|
||||||
if ($box !== '') {
|
|
||||||
$codes[] = $box;
|
|
||||||
}
|
|
||||||
if ($bagCode === '') {
|
|
||||||
$bagCode = (string) ($p['brpc_bag_code'] ?? '');
|
|
||||||
$bagName = (string) ($p['brpc_bag_name'] ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$codes = array_values(array_unique(array_filter($codes, static fn (string $c): bool => $c !== '')));
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
foreach ($this->loadReceivingEventsForLot($lgIdx, $lotNo) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
foreach ($this->loadScanEventsForCodes($lgIdx, $codes) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
foreach ($this->loadReturnEventsForCodes($lgIdx, $codes) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($rows, static fn (array $a, array $b): int => ($a['sort_ts'] ?? 0) <=> ($b['sort_ts'] ?? 0));
|
|
||||||
if (count($rows) > 500) {
|
|
||||||
$rows = array_slice($rows, -500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_merge($empty, [
|
|
||||||
'ok' => true,
|
|
||||||
'lot_no' => $lotNo,
|
|
||||||
'bag_code' => $bagCode,
|
|
||||||
'bag_name' => $bagName,
|
|
||||||
'barcode' => $lotNo,
|
|
||||||
'rows' => $rows,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function emptyResult(string $barcode): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'ok' => false,
|
|
||||||
'message' => '',
|
|
||||||
'barcode' => $barcode,
|
|
||||||
'unit' => '',
|
|
||||||
'bag_code' => '',
|
|
||||||
'bag_name' => '',
|
|
||||||
'lot_no' => '',
|
|
||||||
'box_code' => '',
|
|
||||||
'pack_code' => '',
|
|
||||||
'qty_box' => 0,
|
|
||||||
'qty_pack' => 0,
|
|
||||||
'qty_sheet' => 0,
|
|
||||||
'rows' => [],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{ok: bool, message?: string, barcode?: string, unit?: string, bag_code?: string, bag_name?: string, lot_no?: string, box_code?: string, pack_code?: string, pack_ids?: list<int>, qty_box?: int, qty_pack?: int, qty_sheet?: int}
|
|
||||||
*/
|
|
||||||
private function resolveBarcode(int $lgIdx, string $barcode): array
|
|
||||||
{
|
|
||||||
if (! $this->db->tableExists('bag_receiving_pack_code')) {
|
|
||||||
return ['ok' => false, 'message' => '바코드(팩) 데이터가 없습니다.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$pack = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_pack_code', $barcode)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if ($pack) {
|
|
||||||
return $this->resolvedFromPackRow($barcode, '팩', $pack, 0, 1, (int) ($pack['brpc_sheet_qty'] ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
$boxRows = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_box_code', $barcode)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
if ($boxRows !== []) {
|
|
||||||
$first = $boxRows[0];
|
|
||||||
$sheetQty = 0;
|
|
||||||
foreach ($boxRows as $row) {
|
|
||||||
$sheetQty += (int) ($row['brpc_sheet_qty'] ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->resolvedFromPackRow($barcode, '박스', $first, 1, count($boxRows), $sheetQty);
|
|
||||||
}
|
|
||||||
|
|
||||||
$exactSheet = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_sheet_start_code', $barcode)
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if (is_array($exactSheet) && $exactSheet !== []) {
|
|
||||||
return $this->resolvedFromPackRow($barcode, '낱장', $exactSheet, 0, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$sheetRows = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_sheet_start_code !=', '')
|
|
||||||
->where('brpc_sheet_end_code !=', '')
|
|
||||||
->limit(200)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
foreach ($sheetRows as $row) {
|
|
||||||
$start = (string) ($row['brpc_sheet_start_code'] ?? '');
|
|
||||||
$end = (string) ($row['brpc_sheet_end_code'] ?? '');
|
|
||||||
if ($this->barcodeInRange($barcode, $start, $end)) {
|
|
||||||
return $this->resolvedFromPackRow($barcode, '낱장', $row, 0, 0, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$fromScan = $this->resolveBarcodeFromScanTables($lgIdx, $barcode);
|
|
||||||
if ($fromScan !== null) {
|
|
||||||
return $fromScan;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['ok' => false, 'message' => '등록되지 않은 바코드입니다.'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 판매·반품 스캔에만 있는 낱장 코드(입고 팩 테이블 미등록) 조회
|
|
||||||
*
|
|
||||||
* @return array{ok: true, barcode: string, unit: string, bag_code: string, bag_name: string, lot_no: string, box_code: string, pack_code: string, pack_ids: list<int>, qty_box: int, qty_pack: int, qty_sheet: int}|null
|
|
||||||
*/
|
|
||||||
private function resolveBarcodeFromScanTables(int $lgIdx, string $barcode): ?array
|
|
||||||
{
|
|
||||||
$bagCode = '';
|
|
||||||
$bagName = '';
|
|
||||||
if ($this->db->tableExists('bag_sale_scan_code')) {
|
|
||||||
$sale = $this->db->table('bag_sale_scan_code')
|
|
||||||
->select('bssc_bag_code, bssc_bag_name')
|
|
||||||
->where('bssc_lg_idx', $lgIdx)
|
|
||||||
->where('bssc_code', $barcode)
|
|
||||||
->orderBy('bssc_regdate', 'DESC')
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if (is_array($sale) && $sale !== []) {
|
|
||||||
$bagCode = (string) ($sale['bssc_bag_code'] ?? '');
|
|
||||||
$bagName = (string) ($sale['bssc_bag_name'] ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($bagCode === '' && $this->db->tableExists('bag_return_scan_code')) {
|
|
||||||
$ret = $this->db->table('bag_return_scan_code')
|
|
||||||
->select('brsc_bag_code, brsc_bag_name')
|
|
||||||
->where('brsc_lg_idx', $lgIdx)
|
|
||||||
->where('brsc_code', $barcode)
|
|
||||||
->orderBy('brsc_regdate', 'DESC')
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if (is_array($ret) && $ret !== []) {
|
|
||||||
$bagCode = (string) ($ret['brsc_bag_code'] ?? '');
|
|
||||||
$bagName = (string) ($ret['brsc_bag_name'] ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($bagCode === '' && $bagName === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$packRow = $this->findPackRowContainingSheet($lgIdx, $barcode);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'ok' => true,
|
|
||||||
'barcode' => $barcode,
|
|
||||||
'unit' => '낱장',
|
|
||||||
'bag_code' => $bagCode,
|
|
||||||
'bag_name' => $bagName,
|
|
||||||
'lot_no' => (string) ($packRow['brpc_lot_no'] ?? ''),
|
|
||||||
'box_code' => (string) ($packRow['brpc_box_code'] ?? ''),
|
|
||||||
'pack_code' => (string) ($packRow['brpc_pack_code'] ?? ''),
|
|
||||||
'pack_ids' => isset($packRow['brpc_idx']) ? [(int) $packRow['brpc_idx']] : [],
|
|
||||||
'qty_box' => 0,
|
|
||||||
'qty_pack' => 0,
|
|
||||||
'qty_sheet' => 1,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function findPackRowContainingSheet(int $lgIdx, string $barcode): array
|
|
||||||
{
|
|
||||||
if (! $this->db->tableExists('bag_receiving_pack_code')) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$exact = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_sheet_start_code', $barcode)
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if (is_array($exact) && $exact !== []) {
|
|
||||||
return $exact;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($this->db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_sheet_start_code !=', '')
|
|
||||||
->where('brpc_sheet_end_code !=', '')
|
|
||||||
->limit(200)
|
|
||||||
->get()
|
|
||||||
->getResultArray() as $row) {
|
|
||||||
$start = (string) ($row['brpc_sheet_start_code'] ?? '');
|
|
||||||
$end = (string) ($row['brpc_sheet_end_code'] ?? '');
|
|
||||||
if ($this->barcodeInRange($barcode, $start, $end)) {
|
|
||||||
return $row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $pack
|
|
||||||
* @return array{ok: true, barcode: string, unit: string, bag_code: string, bag_name: string, lot_no: string, box_code: string, pack_code: string, pack_ids: list<int>, qty_box: int, qty_pack: int, qty_sheet: int}
|
|
||||||
*/
|
|
||||||
private function resolvedFromPackRow(string $barcode, string $unit, array $pack, int $qtyBox, int $qtyPack, int $qtySheet): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'ok' => true,
|
|
||||||
'barcode' => $barcode,
|
|
||||||
'unit' => $unit,
|
|
||||||
'bag_code' => (string) ($pack['brpc_bag_code'] ?? ''),
|
|
||||||
'bag_name' => (string) ($pack['brpc_bag_name'] ?? ''),
|
|
||||||
'lot_no' => (string) ($pack['brpc_lot_no'] ?? ''),
|
|
||||||
'box_code' => (string) ($pack['brpc_box_code'] ?? ''),
|
|
||||||
'pack_code' => (string) ($pack['brpc_pack_code'] ?? ''),
|
|
||||||
'pack_ids' => [(int) ($pack['brpc_idx'] ?? 0)],
|
|
||||||
'qty_box' => $qtyBox,
|
|
||||||
'qty_pack' => $qtyPack,
|
|
||||||
'qty_sheet' => $qtySheet,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $resolved
|
|
||||||
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
*/
|
|
||||||
private function collectFlowRows(int $lgIdx, array $resolved): array
|
|
||||||
{
|
|
||||||
$unit = (string) ($resolved['unit'] ?? '');
|
|
||||||
|
|
||||||
return match ($unit) {
|
|
||||||
'낱장' => $this->collectFlowRowsForSheet($lgIdx, $resolved),
|
|
||||||
'박스' => $this->collectFlowRowsForBox($lgIdx, $resolved),
|
|
||||||
default => $this->collectFlowRowsForPack($lgIdx, $resolved),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 낱장: 해당 바코드 판매·반품만 + 소속 팩 입고 1건(발주·동일 LOT 전체 이력 제외)
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $resolved
|
|
||||||
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
*/
|
|
||||||
private function collectFlowRowsForSheet(int $lgIdx, array $resolved): array
|
|
||||||
{
|
|
||||||
$rows = [];
|
|
||||||
$barcode = trim((string) ($resolved['barcode'] ?? ''));
|
|
||||||
if ($barcode === '') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$brIdx = $this->receivingBrIdxForPackCode($lgIdx, (string) ($resolved['pack_code'] ?? ''));
|
|
||||||
if ($brIdx > 0) {
|
|
||||||
foreach ($this->loadReceivingEventsByBrIdx($lgIdx, $brIdx) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($this->loadScanEventsForCodes($lgIdx, [$barcode]) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
foreach ($this->loadReturnEventsForCodes($lgIdx, [$barcode]) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 팩: 팩·낱장 바코드 스캔 + 입고 + LOT 발주
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $resolved
|
|
||||||
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
*/
|
|
||||||
private function collectFlowRowsForPack(int $lgIdx, array $resolved): array
|
|
||||||
{
|
|
||||||
$rows = [];
|
|
||||||
$codes = array_values(array_unique(array_filter([
|
|
||||||
(string) ($resolved['barcode'] ?? ''),
|
|
||||||
(string) ($resolved['pack_code'] ?? ''),
|
|
||||||
], static fn (string $c): bool => $c !== '')));
|
|
||||||
|
|
||||||
$brIdx = $this->receivingBrIdxForPackCode($lgIdx, (string) ($resolved['pack_code'] ?? ''));
|
|
||||||
if ($brIdx > 0) {
|
|
||||||
foreach ($this->loadReceivingEventsByBrIdx($lgIdx, $brIdx) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($this->loadScanEventsForCodes($lgIdx, $codes) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
foreach ($this->loadReturnEventsForCodes($lgIdx, $codes) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lotNo = (string) ($resolved['lot_no'] ?? '');
|
|
||||||
if ($lotNo !== '') {
|
|
||||||
foreach ($this->loadOrderEventsForLot($lgIdx, $lotNo) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 박스: 박스·소속 팩 코드 스캔 + 입고(박스 내 팩) + LOT 발주
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $resolved
|
|
||||||
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
*/
|
|
||||||
private function collectFlowRowsForBox(int $lgIdx, array $resolved): array
|
|
||||||
{
|
|
||||||
$rows = [];
|
|
||||||
$boxCode = (string) ($resolved['box_code'] ?? '');
|
|
||||||
$codes = array_values(array_unique(array_filter([
|
|
||||||
(string) ($resolved['barcode'] ?? ''),
|
|
||||||
$boxCode,
|
|
||||||
(string) ($resolved['pack_code'] ?? ''),
|
|
||||||
], static fn (string $c): bool => $c !== '')));
|
|
||||||
|
|
||||||
if ($boxCode !== '' && $this->db->tableExists('bag_receiving_pack_code')) {
|
|
||||||
$packCodes = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->select('brpc_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_box_code', $boxCode)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
foreach ($packCodes as $p) {
|
|
||||||
$pc = (string) ($p['brpc_pack_code'] ?? '');
|
|
||||||
if ($pc !== '' && ! in_array($pc, $codes, true)) {
|
|
||||||
$codes[] = $pc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$seenBr = [];
|
|
||||||
if ($boxCode !== '' && $this->db->tableExists('bag_receiving_pack_code')) {
|
|
||||||
$brRows = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->select('brpc_br_idx')
|
|
||||||
->distinct()
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_box_code', $boxCode)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
foreach ($brRows as $brRow) {
|
|
||||||
$brIdx = (int) ($brRow['brpc_br_idx'] ?? 0);
|
|
||||||
if ($brIdx <= 0 || isset($seenBr[$brIdx])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$seenBr[$brIdx] = true;
|
|
||||||
foreach ($this->loadReceivingEventsByBrIdx($lgIdx, $brIdx) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($this->loadScanEventsForCodes($lgIdx, $codes) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
foreach ($this->loadReturnEventsForCodes($lgIdx, $codes) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lotNo = (string) ($resolved['lot_no'] ?? '');
|
|
||||||
if ($lotNo !== '') {
|
|
||||||
foreach ($this->loadOrderEventsForLot($lgIdx, $lotNo) as $ev) {
|
|
||||||
$rows[] = $ev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function receivingBrIdxForPackCode(int $lgIdx, string $packCode): int
|
|
||||||
{
|
|
||||||
if ($packCode === '' || ! $this->db->tableExists('bag_receiving_pack_code')) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$p = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->select('brpc_br_idx')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_pack_code', $packCode)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
|
|
||||||
return (int) ($p['brpc_br_idx'] ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
*/
|
|
||||||
private function loadReceivingEventsByBrIdx(int $lgIdx, int $brIdx): array
|
|
||||||
{
|
|
||||||
$sql = "
|
|
||||||
SELECT r.br_receive_date, r.br_sender_name, r.br_regdate,
|
|
||||||
o.bo_order_date, c.cp_name, sa.sa_name
|
|
||||||
FROM bag_receiving r
|
|
||||||
LEFT JOIN bag_order o ON o.bo_idx = r.br_bo_idx
|
|
||||||
LEFT JOIN company c ON c.cp_idx = o.bo_company_idx
|
|
||||||
LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx
|
|
||||||
WHERE r.br_lg_idx = ? AND r.br_idx = ?
|
|
||||||
LIMIT 20
|
|
||||||
";
|
|
||||||
$rows = [];
|
|
||||||
foreach ($this->db->query($sql, [$lgIdx, $brIdx])->getResultArray() as $r) {
|
|
||||||
$rows[] = $this->makeEvent(
|
|
||||||
(string) ($r['br_receive_date'] ?? ''),
|
|
||||||
(string) ($r['br_regdate'] ?? ''),
|
|
||||||
$this->pickSource($r, '입고처', 'sa_name', 'cp_name', 'br_sender_name'),
|
|
||||||
'입고'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
*/
|
|
||||||
private function loadReceivingEventsForLot(int $lgIdx, string $lotNo): array
|
|
||||||
{
|
|
||||||
$sql = "
|
|
||||||
SELECT r.br_receive_date, r.br_sender_name, r.br_regdate, r.br_bag_name,
|
|
||||||
c.cp_name, sa.sa_name
|
|
||||||
FROM bag_receiving r
|
|
||||||
INNER JOIN bag_order o ON o.bo_idx = r.br_bo_idx AND o.bo_lot_no = ?
|
|
||||||
LEFT JOIN company c ON c.cp_idx = o.bo_company_idx
|
|
||||||
LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx
|
|
||||||
WHERE r.br_lg_idx = ?
|
|
||||||
ORDER BY r.br_receive_date ASC, r.br_idx ASC
|
|
||||||
LIMIT 200
|
|
||||||
";
|
|
||||||
$rows = [];
|
|
||||||
foreach ($this->db->query($sql, [$lotNo, $lgIdx])->getResultArray() as $r) {
|
|
||||||
$label = trim((string) ($r['br_bag_name'] ?? ''));
|
|
||||||
$rows[] = $this->makeEvent(
|
|
||||||
(string) ($r['br_receive_date'] ?? ''),
|
|
||||||
(string) ($r['br_regdate'] ?? ''),
|
|
||||||
$this->pickSource($r, '입고처', 'sa_name', 'cp_name', 'br_sender_name') . ($label !== '' ? ' · ' . $label : ''),
|
|
||||||
'입고'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<string> $codes
|
|
||||||
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
*/
|
|
||||||
private function loadScanEventsForCodes(int $lgIdx, array $codes): array
|
|
||||||
{
|
|
||||||
if ($codes === [] || ! $this->db->tableExists('bag_sale_scan_code')) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$placeholders = implode(',', array_fill(0, count($codes), '?'));
|
|
||||||
$params = array_merge([$lgIdx], $codes);
|
|
||||||
$sql = "
|
|
||||||
SELECT b.bssc_regdate, b.bssc_state, b.bssc_code, d.ds_name, d.ds_shop_no
|
|
||||||
FROM bag_sale_scan_code b
|
|
||||||
LEFT JOIN designated_shop d ON d.ds_idx = b.bssc_ds_idx
|
|
||||||
WHERE b.bssc_lg_idx = ? AND b.bssc_code IN ({$placeholders})
|
|
||||||
ORDER BY b.bssc_regdate ASC
|
|
||||||
LIMIT 200
|
|
||||||
";
|
|
||||||
$rows = [];
|
|
||||||
foreach ($this->db->query($sql, $params)->getResultArray() as $r) {
|
|
||||||
$state = strtolower((string) ($r['bssc_state'] ?? ''));
|
|
||||||
$type = $state === 'in_stock' ? '반품입고' : '출고';
|
|
||||||
$shop = trim((string) ($r['ds_name'] ?? ''));
|
|
||||||
if ($shop === '') {
|
|
||||||
$shop = trim((string) ($r['ds_shop_no'] ?? ''));
|
|
||||||
}
|
|
||||||
if ($shop === '') {
|
|
||||||
$shop = '지정판매소';
|
|
||||||
}
|
|
||||||
$rows[] = $this->makeEvent(
|
|
||||||
$this->dateOnly((string) ($r['bssc_regdate'] ?? '')),
|
|
||||||
(string) ($r['bssc_regdate'] ?? ''),
|
|
||||||
$shop,
|
|
||||||
$type
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<string> $codes
|
|
||||||
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
*/
|
|
||||||
private function loadReturnEventsForCodes(int $lgIdx, array $codes): array
|
|
||||||
{
|
|
||||||
if ($codes === [] || ! $this->db->tableExists('bag_return_scan_code')) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$placeholders = implode(',', array_fill(0, count($codes), '?'));
|
|
||||||
$params = array_merge([$lgIdx], $codes);
|
|
||||||
$sql = "
|
|
||||||
SELECT r.brsc_return_date, r.brsc_regdate, r.brsc_code, d.ds_name, d.ds_shop_no
|
|
||||||
FROM bag_return_scan_code r
|
|
||||||
LEFT JOIN designated_shop d ON d.ds_idx = r.brsc_ds_idx
|
|
||||||
WHERE r.brsc_lg_idx = ? AND r.brsc_code IN ({$placeholders})
|
|
||||||
ORDER BY r.brsc_return_date ASC
|
|
||||||
LIMIT 200
|
|
||||||
";
|
|
||||||
$rows = [];
|
|
||||||
foreach ($this->db->query($sql, $params)->getResultArray() as $r) {
|
|
||||||
$shop = trim((string) ($r['ds_name'] ?? ''));
|
|
||||||
if ($shop === '') {
|
|
||||||
$shop = trim((string) ($r['ds_shop_no'] ?? ''));
|
|
||||||
}
|
|
||||||
if ($shop === '') {
|
|
||||||
$shop = '지정판매소';
|
|
||||||
}
|
|
||||||
$rows[] = $this->makeEvent(
|
|
||||||
(string) ($r['brsc_return_date'] ?? ''),
|
|
||||||
(string) ($r['brsc_regdate'] ?? ''),
|
|
||||||
$shop,
|
|
||||||
'반품'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
|
|
||||||
*/
|
|
||||||
private function loadOrderEventsForLot(int $lgIdx, string $lotNo): array
|
|
||||||
{
|
|
||||||
$sql = "
|
|
||||||
SELECT o.bo_order_date, o.bo_regdate, c.cp_name, sa.sa_name
|
|
||||||
FROM bag_order o
|
|
||||||
LEFT JOIN company c ON c.cp_idx = o.bo_company_idx
|
|
||||||
LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx
|
|
||||||
WHERE o.bo_lg_idx = ? AND o.bo_lot_no = ? AND o.bo_status = 'normal'
|
|
||||||
ORDER BY o.bo_version DESC
|
|
||||||
LIMIT 1
|
|
||||||
";
|
|
||||||
$r = $this->db->query($sql, [$lgIdx, $lotNo])->getRowArray();
|
|
||||||
if (! $r) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
$this->makeEvent(
|
|
||||||
(string) ($r['bo_order_date'] ?? ''),
|
|
||||||
(string) ($r['bo_regdate'] ?? ''),
|
|
||||||
$this->pickSource($r, '제작·발주', 'cp_name', 'sa_name'),
|
|
||||||
'발주'
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $order
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function buildLotFromOrderOnly(int $lgIdx, string $lotNo, array $order): array
|
|
||||||
{
|
|
||||||
$rows = $this->loadOrderEventsForLot($lgIdx, $lotNo);
|
|
||||||
$boIdx = (int) ($order['bo_idx'] ?? 0);
|
|
||||||
if ($boIdx > 0) {
|
|
||||||
$sql = "
|
|
||||||
SELECT r.br_receive_date, r.br_sender_name, r.br_regdate, r.br_bag_name,
|
|
||||||
c.cp_name, sa.sa_name
|
|
||||||
FROM bag_receiving r
|
|
||||||
LEFT JOIN bag_order o ON o.bo_idx = r.br_bo_idx
|
|
||||||
LEFT JOIN company c ON c.cp_idx = o.bo_company_idx
|
|
||||||
LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx
|
|
||||||
WHERE r.br_bo_idx = ?
|
|
||||||
ORDER BY r.br_receive_date ASC
|
|
||||||
";
|
|
||||||
foreach ($this->db->query($sql, [$boIdx])->getResultArray() as $r) {
|
|
||||||
$rows[] = $this->makeEvent(
|
|
||||||
(string) ($r['br_receive_date'] ?? ''),
|
|
||||||
(string) ($r['br_regdate'] ?? ''),
|
|
||||||
$this->pickSource($r, '입고처', 'sa_name', 'cp_name', 'br_sender_name'),
|
|
||||||
'입고'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($rows, static fn (array $a, array $b): int => ($a['sort_ts'] ?? 0) <=> ($b['sort_ts'] ?? 0));
|
|
||||||
|
|
||||||
return array_merge($this->emptyResult($lotNo), [
|
|
||||||
'ok' => true,
|
|
||||||
'lot_no' => $lotNo,
|
|
||||||
'barcode' => $lotNo,
|
|
||||||
'rows' => $rows,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $row
|
|
||||||
*/
|
|
||||||
private function pickSource(array $row, string $default, string ...$keys): string
|
|
||||||
{
|
|
||||||
foreach ($keys as $key) {
|
|
||||||
$v = trim((string) ($row[$key] ?? ''));
|
|
||||||
if ($v !== '') {
|
|
||||||
return $v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}
|
|
||||||
*/
|
|
||||||
private function makeEvent(string $dateYmd, string $sortDatetime, string $counterparty, string $flowType): array
|
|
||||||
{
|
|
||||||
$ts = strtotime($sortDatetime !== '' ? $sortDatetime : $dateYmd);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'flow_date' => $dateYmd !== '' ? $dateYmd : ($ts ? date('Y-m-d', $ts) : ''),
|
|
||||||
'counterparty' => $counterparty,
|
|
||||||
'flow_type' => $flowType,
|
|
||||||
'sort_ts' => $ts !== false ? $ts : 0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function dateOnly(string $datetime): string
|
|
||||||
{
|
|
||||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $datetime, $m) === 1) {
|
|
||||||
return $m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
$ts = strtotime($datetime);
|
|
||||||
|
|
||||||
return $ts ? date('Y-m-d', $ts) : $datetime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LOT 수불 조회 화면 테스트용 — 등록된 바코드·LOT 샘플
|
|
||||||
*
|
|
||||||
* @return list<array{kind: string, code: string, bag_name: string, lot_no: string, state: string, hint: string}>
|
|
||||||
*/
|
|
||||||
public function loadTestSamples(int $lgIdx, int $limit = 80): array
|
|
||||||
{
|
|
||||||
$samples = [];
|
|
||||||
$seen = [];
|
|
||||||
|
|
||||||
$push = static function (array &$samples, array &$seen, string $kind, string $code, string $bagName, string $lotNo, string $state, string $hint) use ($limit): void {
|
|
||||||
$code = trim($code);
|
|
||||||
if ($code === '' || isset($seen[$code]) || count($samples) >= $limit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$seen[$code] = true;
|
|
||||||
$samples[] = [
|
|
||||||
'kind' => $kind,
|
|
||||||
'code' => $code,
|
|
||||||
'bag_name' => $bagName,
|
|
||||||
'lot_no' => $lotNo,
|
|
||||||
'state' => $state,
|
|
||||||
'hint' => $hint,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
if ($this->db->tableExists('bag_receiving_pack_code')) {
|
|
||||||
foreach ($this->db->table('bag_receiving_pack_code')
|
|
||||||
->select('brpc_pack_code, brpc_box_code, brpc_sheet_start_code, brpc_bag_name, brpc_lot_no, brpc_state')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->orderBy('brpc_idx', 'DESC')
|
|
||||||
->limit(40)
|
|
||||||
->get()
|
|
||||||
->getResultArray() as $row) {
|
|
||||||
$state = $this->packStateLabel((string) ($row['brpc_state'] ?? ''));
|
|
||||||
$bagName = (string) ($row['brpc_bag_name'] ?? '');
|
|
||||||
$lotNo = (string) ($row['brpc_lot_no'] ?? '');
|
|
||||||
$push($samples, $seen, '팩', (string) ($row['brpc_pack_code'] ?? ''), $bagName, $lotNo, $state, '입고 팩 코드');
|
|
||||||
}
|
|
||||||
|
|
||||||
$boxRows = $this->db->query("
|
|
||||||
SELECT brpc_box_code,
|
|
||||||
MAX(brpc_bag_name) AS brpc_bag_name,
|
|
||||||
MAX(brpc_lot_no) AS brpc_lot_no,
|
|
||||||
MAX(brpc_state) AS brpc_state
|
|
||||||
FROM bag_receiving_pack_code
|
|
||||||
WHERE brpc_lg_idx = ? AND brpc_box_code != ''
|
|
||||||
GROUP BY brpc_box_code
|
|
||||||
ORDER BY MAX(brpc_idx) DESC
|
|
||||||
LIMIT 15
|
|
||||||
", [$lgIdx])->getResultArray();
|
|
||||||
foreach ($boxRows as $row) {
|
|
||||||
$push($samples, $seen, '박스', (string) ($row['brpc_box_code'] ?? ''), (string) ($row['brpc_bag_name'] ?? ''), (string) ($row['brpc_lot_no'] ?? ''), $this->packStateLabel((string) ($row['brpc_state'] ?? '')), '박스 단위 조회');
|
|
||||||
}
|
|
||||||
|
|
||||||
$sheetRows = $this->db->table('bag_receiving_pack_code')
|
|
||||||
->select('brpc_sheet_start_code, brpc_bag_name, brpc_lot_no, brpc_state')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_sheet_start_code !=', '')
|
|
||||||
->orderBy('brpc_idx', 'DESC')
|
|
||||||
->limit(15)
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
foreach ($sheetRows as $row) {
|
|
||||||
$push($samples, $seen, '낱장', (string) ($row['brpc_sheet_start_code'] ?? ''), (string) ($row['brpc_bag_name'] ?? ''), (string) ($row['brpc_lot_no'] ?? ''), $this->packStateLabel((string) ($row['brpc_state'] ?? '')), '낱장 시작 코드');
|
|
||||||
}
|
|
||||||
|
|
||||||
$lotRows = $this->db->query("
|
|
||||||
SELECT DISTINCT brpc_lot_no
|
|
||||||
FROM bag_receiving_pack_code
|
|
||||||
WHERE brpc_lg_idx = ? AND brpc_lot_no != ''
|
|
||||||
ORDER BY brpc_lot_no DESC
|
|
||||||
LIMIT 10
|
|
||||||
", [$lgIdx])->getResultArray();
|
|
||||||
foreach ($lotRows as $row) {
|
|
||||||
$lot = (string) ($row['brpc_lot_no'] ?? '');
|
|
||||||
$push($samples, $seen, 'LOT', $lot, '(LOT 전체)', $lot, '—', 'lot_no 파라미터·입력 동일');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->db->tableExists('bag_sale_scan_code')) {
|
|
||||||
foreach ($this->db->table('bag_sale_scan_code b')
|
|
||||||
->select('b.bssc_code, b.bssc_bag_name, b.bssc_unit, b.bssc_state, b.bssc_regdate')
|
|
||||||
->where('b.bssc_lg_idx', $lgIdx)
|
|
||||||
->orderBy('b.bssc_regdate', 'DESC')
|
|
||||||
->limit(20)
|
|
||||||
->get()
|
|
||||||
->getResultArray() as $row) {
|
|
||||||
$state = strtolower((string) ($row['bssc_state'] ?? '')) === 'sold' ? '판매' : '반품재고';
|
|
||||||
$push($samples, $seen, '스캔', (string) ($row['bssc_code'] ?? ''), (string) ($row['bssc_bag_name'] ?? ''), '', $state, '판매·반품 스캔 이력');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $samples;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function packStateLabel(string $state): string
|
|
||||||
{
|
|
||||||
return match (strtolower($state)) {
|
|
||||||
'in_stock' => '재고',
|
|
||||||
'sold' => '판매',
|
|
||||||
default => $state !== '' ? $state : '—',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function barcodeInRange(string $code, string $start, string $end): bool
|
|
||||||
{
|
|
||||||
if ($start === '' || $end === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$extract = static function (string $v): array {
|
|
||||||
if (preg_match('/^(.*?)(\d+)$/', $v, $m) === 1) {
|
|
||||||
return [(string) $m[1], (int) $m[2], strlen((string) $m[2])];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['', -1, 0];
|
|
||||||
};
|
|
||||||
[$cp, $cn, $cl] = $extract($code);
|
|
||||||
[$sp, $sn, $sl] = $extract($start);
|
|
||||||
[$ep, $en, $el] = $extract($end);
|
|
||||||
if ($cn >= 0 && $sn >= 0 && $en >= 0 && $cp === $sp && $sp === $ep && $cl === $sl && $sl === $el) {
|
|
||||||
return $cn >= $sn && $cn <= $en;
|
|
||||||
}
|
|
||||||
|
|
||||||
return strcmp($code, $start) >= 0 && strcmp($code, $end) <= 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Libraries;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 봉투번호확인(번호알기) — 코드 입력 → 바코드·인쇄숫자·인식번호 분해.
|
|
||||||
* LOT-입고PK-팩/박스-낱장 형식 및 DB 등록 바코드 지원.
|
|
||||||
*/
|
|
||||||
class BagNumberLookup
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array{
|
|
||||||
* ok: bool,
|
|
||||||
* message: string,
|
|
||||||
* input: string,
|
|
||||||
* barcode_text: string,
|
|
||||||
* print_text: string,
|
|
||||||
* recognition_text: string,
|
|
||||||
* unit: string,
|
|
||||||
* bag_code: string,
|
|
||||||
* bag_name: string
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public function resolve(string $raw, ?int $lgIdx = null): array
|
|
||||||
{
|
|
||||||
$input = trim($raw);
|
|
||||||
if ($input === '') {
|
|
||||||
return $this->fail('코드를 입력해 주세요.', $input);
|
|
||||||
}
|
|
||||||
|
|
||||||
$normalized = strtoupper(preg_replace('/\s+/', '', $input) ?? $input);
|
|
||||||
$parsed = $this->parseStructuredCode($normalized);
|
|
||||||
|
|
||||||
if ($parsed === null && $lgIdx !== null) {
|
|
||||||
$parsed = $this->resolveFromDatabase($lgIdx, $normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($parsed === null) {
|
|
||||||
return $this->fail('인식할 수 없는 코드입니다. 봉투 바코드(LOT-입고번호-팩/박스-낱장) 형식을 확인해 주세요.', $input);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'ok' => true,
|
|
||||||
'message' => '',
|
|
||||||
'input' => $input,
|
|
||||||
'barcode_text' => $this->formatRow($parsed['barcode'], 4),
|
|
||||||
'print_text' => $this->formatRow($parsed['print'], 3),
|
|
||||||
'recognition_text' => $this->formatRow($parsed['recognition'], 2),
|
|
||||||
'unit' => (string) ($parsed['unit'] ?? ''),
|
|
||||||
'bag_code' => (string) ($parsed['bag_code'] ?? ''),
|
|
||||||
'bag_name' => (string) ($parsed['bag_name'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<string> $parts
|
|
||||||
*/
|
|
||||||
private function formatRow(array $parts, int $slots): string
|
|
||||||
{
|
|
||||||
$cells = array_pad($parts, $slots, '-');
|
|
||||||
foreach ($cells as $i => $cell) {
|
|
||||||
if ($cell === '' || $cell === null) {
|
|
||||||
$cells[$i] = '-';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode(' ', $cells);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{barcode:list<string>,print:list<string>,recognition:list<string>,unit:string,bag_code?:string,bag_name?:string}|null
|
|
||||||
*/
|
|
||||||
private function parseStructuredCode(string $code): ?array
|
|
||||||
{
|
|
||||||
if (preg_match('/^([A-Z0-9]+)-(\d{6})-P(\d{3})-S(\d+)$/i', $code, $m) === 1) {
|
|
||||||
$sheet = str_pad((string) $m[4], 5, '0', STR_PAD_LEFT);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'barcode' => [(string) $m[1], (string) $m[2], 'P' . $m[3], 'S' . $sheet],
|
|
||||||
'print' => [(string) (int) $m[2], (string) (int) $m[3], (string) (int) $sheet],
|
|
||||||
'recognition' => [(string) $m[2], 'P' . $m[3]],
|
|
||||||
'unit' => '낱장',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^([A-Z0-9]+)-(\d{6})-P(\d{3})$/i', $code, $m) === 1) {
|
|
||||||
return [
|
|
||||||
'barcode' => [(string) $m[1], (string) $m[2], 'P' . $m[3], '-'],
|
|
||||||
'print' => [(string) (int) $m[2], (string) (int) $m[3], '-'],
|
|
||||||
'recognition' => [(string) $m[2], 'P' . $m[3]],
|
|
||||||
'unit' => '팩',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^([A-Z0-9]+)-(\d{6})-B(\d{3})$/i', $code, $m) === 1) {
|
|
||||||
return [
|
|
||||||
'barcode' => [(string) $m[1], (string) $m[2], 'B' . $m[3], '-'],
|
|
||||||
'print' => [(string) (int) $m[2], (string) (int) $m[3], '-'],
|
|
||||||
'recognition' => [(string) $m[2], 'B' . $m[3]],
|
|
||||||
'unit' => '박스',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('/^([A-Z0-9]{4,8})$/i', $code) === 1) {
|
|
||||||
return [
|
|
||||||
'barcode' => [$code, '-', '-', '-'],
|
|
||||||
'print' => ['-', '-', '-'],
|
|
||||||
'recognition' => [$code, '-'],
|
|
||||||
'unit' => 'LOT',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{barcode:list<string>,print:list<string>,recognition:list<string>,unit:string,bag_code?:string,bag_name?:string}|null
|
|
||||||
*/
|
|
||||||
private function resolveFromDatabase(int $lgIdx, string $code): ?array
|
|
||||||
{
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
if (! $db->tableExists('bag_receiving_pack_code')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = $db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_pack_code', $code)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if (is_array($row) && $row !== []) {
|
|
||||||
return $this->parsedFromPackRow($code, $row, '팩');
|
|
||||||
}
|
|
||||||
|
|
||||||
$boxRows = $db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_box_code', $code)
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if (is_array($boxRows) && $boxRows !== []) {
|
|
||||||
return $this->parsedFromPackRow($code, $boxRows, '박스');
|
|
||||||
}
|
|
||||||
|
|
||||||
$sheetRow = $db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_sheet_start_code <=', $code)
|
|
||||||
->where('brpc_sheet_end_code >=', $code)
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if (is_array($sheetRow) && $sheetRow !== []) {
|
|
||||||
$parsed = $this->parseStructuredCode($code);
|
|
||||||
if ($parsed !== null) {
|
|
||||||
$parsed['bag_code'] = (string) ($sheetRow['brpc_bag_code'] ?? '');
|
|
||||||
$parsed['bag_name'] = (string) ($sheetRow['brpc_bag_name'] ?? '');
|
|
||||||
|
|
||||||
return $parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$exactSheet = $db->table('bag_receiving_pack_code')
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->groupStart()
|
|
||||||
->where('brpc_sheet_start_code', $code)
|
|
||||||
->orWhere('brpc_sheet_end_code', $code)
|
|
||||||
->groupEnd()
|
|
||||||
->limit(1)
|
|
||||||
->get()
|
|
||||||
->getRowArray();
|
|
||||||
if (is_array($exactSheet) && $exactSheet !== []) {
|
|
||||||
$parsed = $this->parseStructuredCode($code);
|
|
||||||
if ($parsed !== null) {
|
|
||||||
$parsed['bag_code'] = (string) ($exactSheet['brpc_bag_code'] ?? '');
|
|
||||||
$parsed['bag_name'] = (string) ($exactSheet['brpc_bag_name'] ?? '');
|
|
||||||
|
|
||||||
return $parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $row
|
|
||||||
* @return array{barcode:list<string>,print:list<string>,recognition:list<string>,unit:string,bag_code:string,bag_name:string}
|
|
||||||
*/
|
|
||||||
private function parsedFromPackRow(string $code, array $row, string $unit): array
|
|
||||||
{
|
|
||||||
$parsed = $this->parseStructuredCode($code);
|
|
||||||
if ($parsed === null) {
|
|
||||||
$parsed = [
|
|
||||||
'barcode' => [$code, '-', '-', '-'],
|
|
||||||
'print' => ['-', '-', '-'],
|
|
||||||
'recognition' => ['-', '-'],
|
|
||||||
'unit' => $unit,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
$parsed['unit'] = $unit;
|
|
||||||
$parsed['bag_code'] = (string) ($row['brpc_bag_code'] ?? '');
|
|
||||||
$parsed['bag_name'] = (string) ($row['brpc_bag_name'] ?? '');
|
|
||||||
|
|
||||||
return $parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{ok:bool,message:string,input:string,barcode_text:string,print_text:string,recognition_text:string,unit:string,bag_code:string,bag_name:string}
|
|
||||||
*/
|
|
||||||
private function fail(string $message, string $input): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'ok' => false,
|
|
||||||
'message' => $message,
|
|
||||||
'input' => $input,
|
|
||||||
'barcode_text' => $this->formatRow([], 4),
|
|
||||||
'print_text' => $this->formatRow([], 3),
|
|
||||||
'recognition_text' => $this->formatRow([], 2),
|
|
||||||
'unit' => '',
|
|
||||||
'bag_code' => '',
|
|
||||||
'bag_name' => '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,494 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Libraries;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 쓰레기봉투 수급 계획 (레거시 w_gm820r 유추)
|
|
||||||
*
|
|
||||||
* - 바코드 봉투: bag_receiving_pack_code 에 등록된 품목
|
|
||||||
* - 기존 봉투: 그 외 (수기·비바코드 재고)
|
|
||||||
* - 소진일수 ≈ (총재고 / 월판매량) × 30
|
|
||||||
* - 발주예정일 = 기준일 + 소진일수 − 적정재고보유일수(제작기일)
|
|
||||||
* - 긴급(발주예정일 ≤ 기준일): 발주수량 ≈ max(0, 월판매량×18 − 총재고)
|
|
||||||
*/
|
|
||||||
class BagSupplyPlanBuilder
|
|
||||||
{
|
|
||||||
private const AVG_SALES_MONTHS = 12;
|
|
||||||
|
|
||||||
/** 긴급 발주 시 목표 보유 개월 수 (레거시 화면 값 유추) */
|
|
||||||
private const URGENT_REPLENISH_MONTHS = 18;
|
|
||||||
|
|
||||||
private \CodeIgniter\Database\BaseConnection $db;
|
|
||||||
|
|
||||||
public function __construct(?\CodeIgniter\Database\BaseConnection $db = null)
|
|
||||||
{
|
|
||||||
$this->db = $db ?? \Config\Database::connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{
|
|
||||||
* rows: list<array<string, mixed>>,
|
|
||||||
* barcodeCodes: array<string, bool>,
|
|
||||||
* queried: bool
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public function build(
|
|
||||||
int $lgIdx,
|
|
||||||
string $refDate,
|
|
||||||
int $leadDays,
|
|
||||||
string $stockScope,
|
|
||||||
string $salesScope,
|
|
||||||
bool $queried
|
|
||||||
): array {
|
|
||||||
$barcodeCodes = $this->loadBarcodeCodes($lgIdx);
|
|
||||||
$products = $this->loadProducts($lgIdx);
|
|
||||||
if ($products === [] || ! $queried) {
|
|
||||||
return ['rows' => [], 'barcodeCodes' => $barcodeCodes, 'queried' => $queried];
|
|
||||||
}
|
|
||||||
|
|
||||||
$inventory = $this->loadInventoryMap($lgIdx);
|
|
||||||
$pendingIn = $this->loadPendingInbound($lgIdx);
|
|
||||||
$lastOrders = $this->loadLastOrders($lgIdx);
|
|
||||||
$monthlySales = $this->loadMonthlyAverageSales($lgIdx, $refDate, $salesScope, $barcodeCodes);
|
|
||||||
$movementsSince = $this->loadMovementsSinceOrders($lgIdx, $lastOrders, $refDate);
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
foreach ($products as $code => $name) {
|
|
||||||
$isBarcode = isset($barcodeCodes[$code]);
|
|
||||||
$rawStock = (int) ($inventory[$code] ?? 0);
|
|
||||||
$currentStock = $this->scopedStock($rawStock, $stockScope, $isBarcode);
|
|
||||||
$pendingQty = $this->scopedPending((int) ($pendingIn[$code] ?? 0), $stockScope, $isBarcode);
|
|
||||||
$totalStock = $currentStock + $pendingQty;
|
|
||||||
$monthlyFloat = (float) ($monthlySales[$code] ?? 0.0);
|
|
||||||
$monthlyAvg = (int) round($monthlyFloat);
|
|
||||||
|
|
||||||
$depletionDays = $this->calcDepletionDays($totalStock, $monthlyFloat);
|
|
||||||
$scheduleDate = $this->calcScheduleDate($refDate, $depletionDays, $leadDays);
|
|
||||||
$orderQty = $this->calcOrderQty($refDate, $scheduleDate, $depletionDays, $leadDays, $monthlyAvg, $totalStock);
|
|
||||||
|
|
||||||
$last = $lastOrders[$code] ?? null;
|
|
||||||
$orderDate = $last ? (string) ($last['order_date'] ?? '') : '';
|
|
||||||
$lastQty = $last ? (int) ($last['qty_sheet'] ?? 0) : 0;
|
|
||||||
$stockAtOrder = 0;
|
|
||||||
if ($orderDate !== '' && $lastQty > 0) {
|
|
||||||
$mv = $movementsSince[$code] ?? ['sale' => 0, 'recv' => 0, 'issue' => 0];
|
|
||||||
$stockAtOrder = max(0, $rawStock + $mv['sale'] - $mv['recv'] - $mv['issue']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows[] = [
|
|
||||||
'bag_code' => $code,
|
|
||||||
'bag_name' => $name,
|
|
||||||
'is_barcode' => $isBarcode,
|
|
||||||
'last_order_date' => $orderDate,
|
|
||||||
'last_order_qty' => $lastQty,
|
|
||||||
'stock_at_order' => $stockAtOrder,
|
|
||||||
'current_stock' => $currentStock,
|
|
||||||
'pending_inbound' => $pendingQty,
|
|
||||||
'total_stock' => $totalStock,
|
|
||||||
'monthly_avg_sales' => $monthlyAvg,
|
|
||||||
'depletion_days' => $depletionDays,
|
|
||||||
'schedule_date' => $scheduleDate,
|
|
||||||
'schedule_overdue' => $scheduleDate !== '' && $scheduleDate <= $refDate && $depletionDays < 99999,
|
|
||||||
'order_qty' => $orderQty,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($rows, static fn (array $a, array $b): int => strcmp((string) $a['bag_code'], (string) $b['bag_code']));
|
|
||||||
|
|
||||||
return ['rows' => $rows, 'barcodeCodes' => $barcodeCodes, 'queried' => true];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, bool>
|
|
||||||
*/
|
|
||||||
private function loadBarcodeCodes(int $lgIdx): array
|
|
||||||
{
|
|
||||||
if (! $this->db->tableExists('bag_receiving_pack_code')) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$set = [];
|
|
||||||
foreach ($this->db->table('bag_receiving_pack_code')
|
|
||||||
->select('brpc_bag_code')
|
|
||||||
->distinct()
|
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
|
||||||
->where('brpc_bag_code !=', '')
|
|
||||||
->get()
|
|
||||||
->getResultArray() as $row) {
|
|
||||||
$code = trim((string) ($row['brpc_bag_code'] ?? ''));
|
|
||||||
if ($code !== '') {
|
|
||||||
$set[$code] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string> code => name
|
|
||||||
*/
|
|
||||||
private function loadProducts(int $lgIdx): array
|
|
||||||
{
|
|
||||||
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
||||||
$products = [];
|
|
||||||
if ($kindO) {
|
|
||||||
foreach (model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx) as $d) {
|
|
||||||
$code = trim((string) ($d->cd_code ?? ''));
|
|
||||||
if ($code !== '') {
|
|
||||||
$products[$code] = trim((string) ($d->cd_name ?? $code));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($this->db->table('bag_inventory')
|
|
||||||
->select('bi_bag_code, bi_bag_name')
|
|
||||||
->where('bi_lg_idx', $lgIdx)
|
|
||||||
->get()
|
|
||||||
->getResultArray() as $row) {
|
|
||||||
$code = trim((string) ($row['bi_bag_code'] ?? ''));
|
|
||||||
if ($code === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (! isset($products[$code])) {
|
|
||||||
$products[$code] = trim((string) ($row['bi_bag_name'] ?? $code));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $products;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, int>
|
|
||||||
*/
|
|
||||||
private function loadInventoryMap(int $lgIdx): array
|
|
||||||
{
|
|
||||||
$map = [];
|
|
||||||
foreach (model(\App\Models\BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll() as $inv) {
|
|
||||||
$code = trim((string) ($inv->bi_bag_code ?? ''));
|
|
||||||
if ($code !== '') {
|
|
||||||
$map[$code] = (int) ($inv->bi_qty ?? 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, int>
|
|
||||||
*/
|
|
||||||
private function loadPendingInbound(int $lgIdx): array
|
|
||||||
{
|
|
||||||
$map = [];
|
|
||||||
$sql = "
|
|
||||||
SELECT boi.boi_bag_code AS bag_code,
|
|
||||||
SUM(GREATEST(0, CAST(boi.boi_qty_sheet AS SIGNED) - IFNULL(r.recv_qty, 0))) AS pending_qty
|
|
||||||
FROM bag_order_item boi
|
|
||||||
INNER JOIN bag_order bo ON bo.bo_idx = boi.boi_bo_idx
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT br_bo_idx, br_bag_code, SUM(br_qty_sheet) AS recv_qty
|
|
||||||
FROM bag_receiving
|
|
||||||
GROUP BY br_bo_idx, br_bag_code
|
|
||||||
) r ON r.br_bo_idx = bo.bo_idx AND r.br_bag_code = boi.boi_bag_code
|
|
||||||
WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
|
|
||||||
GROUP BY boi.boi_bag_code
|
|
||||||
";
|
|
||||||
foreach ($this->db->query($sql, [$lgIdx])->getResult() as $row) {
|
|
||||||
$qty = (int) ($row->pending_qty ?? 0);
|
|
||||||
if ($qty > 0) {
|
|
||||||
$map[(string) $row->bag_code] = $qty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array{order_date: string, qty_sheet: int, bag_name: string}>
|
|
||||||
*/
|
|
||||||
private function loadLastOrders(int $lgIdx): array
|
|
||||||
{
|
|
||||||
$map = [];
|
|
||||||
$supportsWindow = $this->db->DBDriver === 'MySQLi';
|
|
||||||
if ($supportsWindow) {
|
|
||||||
$sql = "
|
|
||||||
SELECT bag_code, order_date, qty_sheet, bag_name
|
|
||||||
FROM (
|
|
||||||
SELECT boi.boi_bag_code AS bag_code, bo.bo_order_date AS order_date,
|
|
||||||
boi.boi_qty_sheet AS qty_sheet, boi.boi_bag_name AS bag_name,
|
|
||||||
ROW_NUMBER() OVER (
|
|
||||||
PARTITION BY boi.boi_bag_code
|
|
||||||
ORDER BY bo.bo_order_date DESC, bo.bo_idx DESC
|
|
||||||
) AS rn
|
|
||||||
FROM bag_order_item boi
|
|
||||||
INNER JOIN bag_order bo ON bo.bo_idx = boi.boi_bo_idx
|
|
||||||
WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
|
|
||||||
) t
|
|
||||||
WHERE t.rn = 1
|
|
||||||
";
|
|
||||||
foreach ($this->db->query($sql, [$lgIdx])->getResultArray() as $row) {
|
|
||||||
$code = trim((string) ($row['bag_code'] ?? ''));
|
|
||||||
if ($code === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$map[$code] = [
|
|
||||||
'order_date' => (string) ($row['order_date'] ?? ''),
|
|
||||||
'qty_sheet' => (int) ($row['qty_sheet'] ?? 0),
|
|
||||||
'bag_name' => (string) ($row['bag_name'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = "
|
|
||||||
SELECT boi.boi_bag_code AS bag_code, MAX(bo.bo_order_date) AS order_date
|
|
||||||
FROM bag_order_item boi
|
|
||||||
INNER JOIN bag_order bo ON bo.bo_idx = boi.boi_bo_idx
|
|
||||||
WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
|
|
||||||
GROUP BY boi.boi_bag_code
|
|
||||||
";
|
|
||||||
foreach ($this->db->query($sql, [$lgIdx])->getResultArray() as $row) {
|
|
||||||
$code = trim((string) ($row['bag_code'] ?? ''));
|
|
||||||
$orderDate = (string) ($row['order_date'] ?? '');
|
|
||||||
if ($code === '' || $orderDate === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$item = $this->db->query("
|
|
||||||
SELECT boi.boi_qty_sheet AS qty_sheet, boi.boi_bag_name AS bag_name
|
|
||||||
FROM bag_order_item boi
|
|
||||||
INNER JOIN bag_order bo ON bo.bo_idx = boi.boi_bo_idx
|
|
||||||
WHERE bo.bo_lg_idx = ? AND bo.bo_status = 'normal'
|
|
||||||
AND boi.boi_bag_code = ? AND bo.bo_order_date = ?
|
|
||||||
ORDER BY bo.bo_idx DESC
|
|
||||||
LIMIT 1
|
|
||||||
", [$lgIdx, $code, $orderDate])->getRowArray();
|
|
||||||
if ($item) {
|
|
||||||
$map[$code] = [
|
|
||||||
'order_date' => $orderDate,
|
|
||||||
'qty_sheet' => (int) ($item['qty_sheet'] ?? 0),
|
|
||||||
'bag_name' => (string) ($item['bag_name'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, array{order_date: string, qty_sheet: int, bag_name: string}> $lastOrders
|
|
||||||
* @return array<string, array{sale: int, recv: int, issue: int}>
|
|
||||||
*/
|
|
||||||
private function loadMovementsSinceOrders(int $lgIdx, array $lastOrders, string $refDate): array
|
|
||||||
{
|
|
||||||
$byCode = [];
|
|
||||||
$minDate = $refDate;
|
|
||||||
foreach ($lastOrders as $code => $info) {
|
|
||||||
$d = (string) ($info['order_date'] ?? '');
|
|
||||||
if ($d === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$byCode[$code] = $d;
|
|
||||||
if ($d < $minDate) {
|
|
||||||
$minDate = $d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($byCode === []) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$codes = array_keys($byCode);
|
|
||||||
$placeholders = implode(',', array_fill(0, count($codes), '?'));
|
|
||||||
$params = array_merge([$lgIdx], $codes, [$minDate, $refDate]);
|
|
||||||
$out = [];
|
|
||||||
foreach ($codes as $code) {
|
|
||||||
$out[$code] = ['sale' => 0, 'recv' => 0, 'issue' => 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = "
|
|
||||||
SELECT bs_bag_code AS bag_code, bs_sale_date AS mv_date,
|
|
||||||
SUM(CASE WHEN bs_type IN ('sale','cancel') THEN ABS(bs_qty) ELSE 0 END) AS sale_qty
|
|
||||||
FROM bag_sale
|
|
||||||
WHERE bs_lg_idx = ? AND bs_bag_code IN ({$placeholders})
|
|
||||||
AND bs_sale_date >= ? AND bs_sale_date <= ?
|
|
||||||
GROUP BY bs_bag_code, bs_sale_date
|
|
||||||
";
|
|
||||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
|
||||||
$code = (string) $row->bag_code;
|
|
||||||
$orderDate = $byCode[$code] ?? '';
|
|
||||||
if ($orderDate === '' || (string) $row->mv_date < $orderDate) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$out[$code]['sale'] += (int) $row->sale_qty;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = "
|
|
||||||
SELECT br_bag_code AS bag_code, br_receive_date AS mv_date, SUM(br_qty_sheet) AS recv_qty
|
|
||||||
FROM bag_receiving
|
|
||||||
WHERE br_lg_idx = ? AND br_bag_code IN ({$placeholders})
|
|
||||||
AND br_receive_date >= ? AND br_receive_date <= ?
|
|
||||||
GROUP BY br_bag_code, br_receive_date
|
|
||||||
";
|
|
||||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
|
||||||
$code = (string) $row->bag_code;
|
|
||||||
$orderDate = $byCode[$code] ?? '';
|
|
||||||
if ($orderDate === '' || (string) $row->mv_date < $orderDate) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$out[$code]['recv'] += (int) $row->recv_qty;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = "
|
|
||||||
SELECT bi2_bag_code AS bag_code, bi2_issue_date AS mv_date, SUM(bi2_qty) AS issue_qty
|
|
||||||
FROM bag_issue
|
|
||||||
WHERE bi2_lg_idx = ? AND bi2_status = 'normal' AND bi2_bag_code IN ({$placeholders})
|
|
||||||
AND bi2_issue_date >= ? AND bi2_issue_date <= ?
|
|
||||||
GROUP BY bi2_bag_code, bi2_issue_date
|
|
||||||
";
|
|
||||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
|
||||||
$code = (string) $row->bag_code;
|
|
||||||
$orderDate = $byCode[$code] ?? '';
|
|
||||||
if ($orderDate === '' || (string) $row->mv_date < $orderDate) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$out[$code]['issue'] += (int) $row->issue_qty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, bool> $barcodeCodes
|
|
||||||
* @return array<string, float>
|
|
||||||
*/
|
|
||||||
private function loadMonthlyAverageSales(
|
|
||||||
int $lgIdx,
|
|
||||||
string $refDate,
|
|
||||||
string $salesScope,
|
|
||||||
array $barcodeCodes
|
|
||||||
): array {
|
|
||||||
$fromDate = date('Y-m-d', strtotime($refDate . ' -' . self::AVG_SALES_MONTHS . ' months'));
|
|
||||||
$legacyNet = [];
|
|
||||||
$barcodeNet = [];
|
|
||||||
|
|
||||||
foreach ($this->db->query("
|
|
||||||
SELECT bs_bag_code AS bag_code,
|
|
||||||
SUM(CASE WHEN bs_type = 'sale' THEN ABS(bs_qty)
|
|
||||||
WHEN bs_type IN ('return','cancel') THEN -ABS(bs_qty)
|
|
||||||
ELSE 0 END) AS net_qty
|
|
||||||
FROM bag_sale
|
|
||||||
WHERE bs_lg_idx = ? AND bs_sale_date > ? AND bs_sale_date <= ?
|
|
||||||
GROUP BY bs_bag_code
|
|
||||||
", [$lgIdx, $fromDate, $refDate])->getResult() as $row) {
|
|
||||||
$code = (string) $row->bag_code;
|
|
||||||
$legacyNet[$code] = (float) ($row->net_qty ?? 0) / self::AVG_SALES_MONTHS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->db->tableExists('bag_sale_scan_code')) {
|
|
||||||
foreach ($this->db->query("
|
|
||||||
SELECT bssc_bag_code AS bag_code, SUM(bssc_qty) AS net_qty
|
|
||||||
FROM bag_sale_scan_code
|
|
||||||
WHERE bssc_lg_idx = ? AND bssc_state = 'sold'
|
|
||||||
AND DATE(bssc_regdate) > ? AND DATE(bssc_regdate) <= ?
|
|
||||||
GROUP BY bssc_bag_code
|
|
||||||
", [$lgIdx, $fromDate, $refDate])->getResult() as $row) {
|
|
||||||
$code = (string) $row->bag_code;
|
|
||||||
$barcodeNet[$code] = (float) ($row->net_qty ?? 0) / self::AVG_SALES_MONTHS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$merged = [];
|
|
||||||
$allCodes = array_unique(array_merge(array_keys($legacyNet), array_keys($barcodeNet)));
|
|
||||||
foreach ($allCodes as $code) {
|
|
||||||
$isBarcode = isset($barcodeCodes[$code]);
|
|
||||||
$legacy = $legacyNet[$code] ?? 0.0;
|
|
||||||
$scan = $barcodeNet[$code] ?? 0.0;
|
|
||||||
$merged[$code] = match ($salesScope) {
|
|
||||||
'legacy' => $isBarcode ? 0.0 : $legacy,
|
|
||||||
'barcode' => $isBarcode ? ($scan > 0 ? $scan : $legacy) : 0.0,
|
|
||||||
default => $isBarcode && $scan > 0 ? $scan : $legacy,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return $merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function scopedStock(int $qty, string $scope, bool $isBarcode): int
|
|
||||||
{
|
|
||||||
return match ($scope) {
|
|
||||||
'legacy' => $isBarcode ? 0 : $qty,
|
|
||||||
'barcode' => $isBarcode ? $qty : 0,
|
|
||||||
default => $qty,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function scopedPending(int $qty, string $scope, bool $isBarcode): int
|
|
||||||
{
|
|
||||||
return $this->scopedStock($qty, $scope, $isBarcode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function calcDepletionDays(int $totalStock, float $monthlyAvg): int
|
|
||||||
{
|
|
||||||
if ($monthlyAvg <= 0.0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) round(($totalStock / $monthlyAvg) * 30);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function calcScheduleDate(string $refDate, int $depletionDays, int $leadDays): string
|
|
||||||
{
|
|
||||||
if ($depletionDays <= 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if ($depletionDays >= 99999) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$offset = $depletionDays - $leadDays;
|
|
||||||
if ($offset < 0) {
|
|
||||||
return $refDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
$base = \DateTimeImmutable::createFromFormat('Y-m-d', $refDate);
|
|
||||||
if ($base === false) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$scheduled = $base->modify('+' . $offset . ' days');
|
|
||||||
} catch (\Exception) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$year = (int) $scheduled->format('Y');
|
|
||||||
$refYear = (int) $base->format('Y');
|
|
||||||
if ($year < $refYear - 1 || $year > $refYear + 120) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $scheduled->format('Y-m-d');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function calcOrderQty(
|
|
||||||
string $refDate,
|
|
||||||
string $scheduleDate,
|
|
||||||
int $depletionDays,
|
|
||||||
int $leadDays,
|
|
||||||
int $monthlyAvg,
|
|
||||||
int $totalStock
|
|
||||||
): int {
|
|
||||||
if ($monthlyAvg <= 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
$urgent = $scheduleDate !== '' && $scheduleDate <= $refDate;
|
|
||||||
$lowStock = $depletionDays > 0 && $depletionDays <= $leadDays && $scheduleDate !== '';
|
|
||||||
if (! $urgent && ! $lowStock) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$target = (int) round($monthlyAvg * ($urgent ? self::URGENT_REPLENISH_MONTHS : max(2, (int) ceil($leadDays / 30.0))));
|
|
||||||
|
|
||||||
return max(0, $target - $totalStock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Libraries\Blockchain;
|
|
||||||
|
|
||||||
use App\Models\BlockchainLedgerModel;
|
|
||||||
|
|
||||||
class SqlLedger
|
|
||||||
{
|
|
||||||
private BlockchainLedgerModel $ledgerModel;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->ledgerModel = model(BlockchainLedgerModel::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string,mixed> $payload
|
|
||||||
* @return array{index:int,hash:string,previous_hash:string}
|
|
||||||
*/
|
|
||||||
public function appendBlock(string $txType, array $payload, ?string $entityUuid, int $entityVersion, ?int $actorIdx, ?int $lgIdx): array
|
|
||||||
{
|
|
||||||
$latest = $this->ledgerModel->orderBy('bl_idx', 'DESC')->first();
|
|
||||||
// 원장이 비어 있으면 $latest 가 null — $latest->bl_hash 는 PHP 8에서 치명 오류(? 는 ?? 와 달리 속성 접근 자체가 먼저 평가됨)
|
|
||||||
$previousHash = ($latest === null || ! isset($latest->bl_hash) || (string) $latest->bl_hash === '')
|
|
||||||
? str_repeat('0', 64)
|
|
||||||
: (string) $latest->bl_hash;
|
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
|
|
||||||
$normalizedPayload = $this->normalizeArray($payload);
|
|
||||||
$payloadJson = json_encode($normalizedPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}';
|
|
||||||
|
|
||||||
$hashInput = implode('|', [
|
|
||||||
$now,
|
|
||||||
$txType,
|
|
||||||
$entityUuid ?? '',
|
|
||||||
(string) $entityVersion,
|
|
||||||
$payloadJson,
|
|
||||||
$previousHash,
|
|
||||||
'0',
|
|
||||||
]);
|
|
||||||
$currentHash = hash('sha256', $hashInput);
|
|
||||||
|
|
||||||
$this->ledgerModel->insert([
|
|
||||||
'bl_created_at' => $now,
|
|
||||||
'bl_tx_type' => $txType,
|
|
||||||
'bl_entity_uuid' => $entityUuid,
|
|
||||||
'bl_entity_version' => $entityVersion,
|
|
||||||
'bl_payload' => $payloadJson,
|
|
||||||
'bl_previous_hash' => $previousHash,
|
|
||||||
'bl_hash' => $currentHash,
|
|
||||||
'bl_nonce' => 0,
|
|
||||||
'bl_actor_idx' => $actorIdx,
|
|
||||||
'bl_lg_idx' => $lgIdx,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'index' => (int) $this->ledgerModel->getInsertID(),
|
|
||||||
'hash' => $currentHash,
|
|
||||||
'previous_hash' => $previousHash,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string,mixed> $data
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
private function normalizeArray(array $data): array
|
|
||||||
{
|
|
||||||
ksort($data);
|
|
||||||
foreach ($data as $key => $value) {
|
|
||||||
if (is_array($value)) {
|
|
||||||
if ($this->isAssoc($value)) {
|
|
||||||
/** @var array<string,mixed> $assoc */
|
|
||||||
$assoc = $value;
|
|
||||||
$data[$key] = $this->normalizeArray($assoc);
|
|
||||||
} else {
|
|
||||||
$normalizedList = [];
|
|
||||||
foreach ($value as $item) {
|
|
||||||
if (is_array($item) && $this->isAssoc($item)) {
|
|
||||||
/** @var array<string,mixed> $assoc */
|
|
||||||
$assoc = $item;
|
|
||||||
$normalizedList[] = $this->normalizeArray($assoc);
|
|
||||||
} else {
|
|
||||||
$normalizedList[] = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$data[$key] = $normalizedList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<mixed> $array
|
|
||||||
*/
|
|
||||||
private function isAssoc(array $array): bool
|
|
||||||
{
|
|
||||||
if ($array === []) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_keys($array) !== range(0, count($array) - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Libraries;
|
|
||||||
|
|
||||||
use App\Models\CodeDetailModel;
|
|
||||||
use App\Models\CodeKindModel;
|
|
||||||
use Config\Roles;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 공공 포털형 기본 코드관리 UI 시안 전용 데이터 (/bag/code-kinds 와 별도 URL, 동일 집계)
|
|
||||||
*/
|
|
||||||
class GovPortalCodeKindsPage
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function buildPageData(?int $lgIdx, int $level, ?int $adminLgIdx, int $selectedCkIdx, array $filters): array
|
|
||||||
{
|
|
||||||
$kindModel = model(CodeKindModel::class);
|
|
||||||
$detailModel = model(CodeDetailModel::class);
|
|
||||||
$kinds = [];
|
|
||||||
$countMap = [];
|
|
||||||
$selectedKind = null;
|
|
||||||
$detailList = [];
|
|
||||||
$rowCanEdit = [];
|
|
||||||
|
|
||||||
$qCode = trim((string) ($filters['q_code'] ?? ''));
|
|
||||||
$qName = trim((string) ($filters['q_name'] ?? ''));
|
|
||||||
$qState = (string) ($filters['q_state'] ?? '');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$builder = $kindModel->orderBy('ck_code', 'ASC');
|
|
||||||
if ($qCode !== '') {
|
|
||||||
$builder->like('ck_code', $qCode);
|
|
||||||
}
|
|
||||||
if ($qName !== '') {
|
|
||||||
$builder->like('ck_name', $qName);
|
|
||||||
}
|
|
||||||
if ($qState === '1' || $qState === '0') {
|
|
||||||
$builder->where('ck_state', (int) $qState);
|
|
||||||
}
|
|
||||||
$kinds = $builder->findAll();
|
|
||||||
foreach ($kinds as $row) {
|
|
||||||
$countMap[$row->ck_idx] = (int) $detailModel->where('cd_ck_idx', $row->ck_idx)
|
|
||||||
->filterByTenantScope($lgIdx)
|
|
||||||
->countAllResults();
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
log_message('error', '[GovPortalCodeKinds] {type} {message}', [
|
|
||||||
'type' => $e::class,
|
|
||||||
'message' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$canManageKinds = Roles::canManageCodeKindMaster($level);
|
|
||||||
$canManageDetails = Roles::canManageCodeMaster($level);
|
|
||||||
|
|
||||||
if ($kinds !== []) {
|
|
||||||
foreach ($kinds as $row) {
|
|
||||||
if ((int) $row->ck_idx === $selectedCkIdx) {
|
|
||||||
$selectedKind = $row;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($selectedKind === null) {
|
|
||||||
$selectedKind = $kinds[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($selectedKind !== null) {
|
|
||||||
$detailList = $detailModel->where('cd_ck_idx', (int) $selectedKind->ck_idx)
|
|
||||||
->filterByTenantScope($lgIdx)
|
|
||||||
->orderBy('cd_sort', 'ASC')
|
|
||||||
->orderBy('cd_code', 'ASC')
|
|
||||||
->orderBy('cd_idx', 'ASC')
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
foreach ($detailList as $row) {
|
|
||||||
$rowCanEdit[$row->cd_idx] = Roles::canEditCodeDetailRow($level, $row, $adminLgIdx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'codeKinds' => $kinds,
|
|
||||||
'countMap' => $countMap,
|
|
||||||
'canManageKinds' => $canManageKinds,
|
|
||||||
'canManageDetails' => $canManageDetails,
|
|
||||||
'selectedKind' => $selectedKind,
|
|
||||||
'detailList' => $detailList,
|
|
||||||
'rowCanEdit' => $rowCanEdit,
|
|
||||||
'totalCount' => count($kinds),
|
|
||||||
'filters' => [
|
|
||||||
'q_code' => $qCode,
|
|
||||||
'q_name' => $qName,
|
|
||||||
'q_state' => $qState,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Libraries;
|
|
||||||
|
|
||||||
use Config\Manual as ManualConfig;
|
|
||||||
use League\CommonMark\Exception\CommonMarkException;
|
|
||||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 사용자 매뉴얼(설명서) 렌더러.
|
|
||||||
*
|
|
||||||
* - 목차(manifest)는 Config\Manual 에서 가져온다.
|
|
||||||
* - slug → 파일 매핑은 화이트리스트(manifest)로만 결정한다(사용자 입력으로 파일명 조합 금지).
|
|
||||||
* - 마크다운은 GFM(표·코드블록)로 변환하며, md 내 raw HTML 은 이스케이프한다.
|
|
||||||
*/
|
|
||||||
class ManualRenderer
|
|
||||||
{
|
|
||||||
private ManualConfig $config;
|
|
||||||
|
|
||||||
private GithubFlavoredMarkdownConverter $converter;
|
|
||||||
|
|
||||||
public function __construct(?ManualConfig $config = null)
|
|
||||||
{
|
|
||||||
$this->config = $config ?? config(ManualConfig::class);
|
|
||||||
$this->converter = new GithubFlavoredMarkdownConverter([
|
|
||||||
// 콘텐츠 저자는 신뢰되지만, 사고 방지를 위해 정책을 명시 고정한다.
|
|
||||||
'html_input' => 'escape',
|
|
||||||
'allow_unsafe_links' => false,
|
|
||||||
'max_nesting_level' => 50,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 목차(slug → title/file). 배열 순서가 노출 순서.
|
|
||||||
*
|
|
||||||
* @return array<string, array{title: string, file: string}>
|
|
||||||
*/
|
|
||||||
public function pages(): array
|
|
||||||
{
|
|
||||||
return $this->config->pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 목차의 첫 번째 slug (기본 진입 페이지). */
|
|
||||||
public function firstSlug(): string
|
|
||||||
{
|
|
||||||
return (string) (array_key_first($this->config->pages) ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* slug 메타 조회. 화이트리스트에 없으면 null.
|
|
||||||
*
|
|
||||||
* @return array{title: string, file: string}|null
|
|
||||||
*/
|
|
||||||
public function find(string $slug): ?array
|
|
||||||
{
|
|
||||||
return $this->config->pages[$slug] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* slug 페이지를 HTML 로 변환해 반환. 미등록 slug·파일 없음·변환 실패 시 null.
|
|
||||||
*/
|
|
||||||
public function render(string $slug): ?string
|
|
||||||
{
|
|
||||||
$page = $this->find($slug);
|
|
||||||
if ($page === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$path = $this->resolvePath($page['file']);
|
|
||||||
if ($path === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$markdown = @file_get_contents($path);
|
|
||||||
if ($markdown === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return (string) $this->converter->convert($markdown);
|
|
||||||
} catch (CommonMarkException) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 파일명을 디렉터리 경계 안으로 안전하게 해석한다.
|
|
||||||
* manifest 의 고정 파일명만 받으며, realpath 가 $dir 하위인지 재검증한다.
|
|
||||||
*/
|
|
||||||
private function resolvePath(string $file): ?string
|
|
||||||
{
|
|
||||||
$baseReal = realpath(rtrim($this->config->dir, '/\\'));
|
|
||||||
if ($baseReal === false) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$candidate = realpath($baseReal . DIRECTORY_SEPARATOR . $file);
|
|
||||||
if ($candidate === false || ! is_file($candidate)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$prefix = $baseReal . DIRECTORY_SEPARATOR;
|
|
||||||
if (! str_starts_with($candidate, $prefix)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use CodeIgniter\Model;
|
|
||||||
|
|
||||||
class BagIssueItemCodeModel extends Model
|
|
||||||
{
|
|
||||||
protected $table = 'bag_issue_item_code';
|
|
||||||
protected $primaryKey = 'bic_idx';
|
|
||||||
protected $returnType = 'object';
|
|
||||||
protected $useTimestamps = false;
|
|
||||||
protected $allowedFields = [
|
|
||||||
'bic_lg_idx',
|
|
||||||
'bic_bi2_idx',
|
|
||||||
'bic_bag_code',
|
|
||||||
'bic_issue_code',
|
|
||||||
'bic_qty',
|
|
||||||
'bic_cancel_qty',
|
|
||||||
'bic_state',
|
|
||||||
'bic_regdate',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -10,26 +10,9 @@ class BagOrderModel extends Model
|
|||||||
protected $primaryKey = 'bo_idx';
|
protected $primaryKey = 'bo_idx';
|
||||||
protected $returnType = 'object';
|
protected $returnType = 'object';
|
||||||
protected $useTimestamps = false;
|
protected $useTimestamps = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* 동일 발주 UUID에 대해 bo_version이 최대인 행만 (수정으로 생긴 이전 버전 행은 목록·이력에서 제외).
|
|
||||||
* DB에는 버전별 행이 그대로 남고, 조회 시에만 필터한다.
|
|
||||||
*/
|
|
||||||
public function whereLatestHead(int $lgIdx): self
|
|
||||||
{
|
|
||||||
$lg = (int) $lgIdx;
|
|
||||||
|
|
||||||
return $this->where(
|
|
||||||
"(bo_uuid, bo_version) IN (SELECT bo_uuid, MAX(bo_version) FROM {$this->table} WHERE bo_lg_idx = {$lg} GROUP BY bo_uuid)",
|
|
||||||
null,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'bo_uuid', 'bo_version', 'bo_lg_idx', 'bo_gugun_code', 'bo_dong_code',
|
'bo_uuid', 'bo_version', 'bo_lg_idx', 'bo_gugun_code', 'bo_dong_code',
|
||||||
'bo_company_idx', 'bo_agency_idx', 'bo_fee_rate', 'bo_order_date',
|
'bo_company_idx', 'bo_agency_idx', 'bo_fee_rate', 'bo_order_date',
|
||||||
'bo_bag_types', 'bo_unit_prices', 'bo_qty_boxes',
|
|
||||||
'bo_lot_no', 'bo_hash', 'bo_status', 'bo_orderer_idx',
|
'bo_lot_no', 'bo_hash', 'bo_status', 'bo_orderer_idx',
|
||||||
'bo_regdate', 'bo_moddate',
|
'bo_regdate', 'bo_moddate',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -16,45 +16,4 @@ class BagPriceModel extends Model
|
|||||||
'bp_start_date', 'bp_end_date', 'bp_state',
|
'bp_start_date', 'bp_end_date', 'bp_state',
|
||||||
'bp_regdate', 'bp_moddate', 'bp_reg_mb_idx',
|
'bp_regdate', 'bp_moddate', 'bp_reg_mb_idx',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* 같은 봉투코드에 단가 기간이 겹쳐도 "나중 등록 단가"가 우선되도록
|
|
||||||
* 활성 단가를 등록일/PK 역순으로 정렬해 봉투코드별 1건만 남긴다.
|
|
||||||
*
|
|
||||||
* @return array<string, object>
|
|
||||||
*/
|
|
||||||
public function latestActiveMapByBagCode(int $lgIdx): array
|
|
||||||
{
|
|
||||||
$rows = $this->where('bp_lg_idx', $lgIdx)
|
|
||||||
->where('bp_state', 1)
|
|
||||||
->orderBy('bp_regdate', 'DESC')
|
|
||||||
->orderBy('bp_idx', 'DESC')
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
$map = [];
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$code = (string) ($row->bp_bag_code ?? '');
|
|
||||||
if ($code === '' || isset($map[$code])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$map[$code] = $row;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function latestActiveByBagCode(int $lgIdx, string $bagCode): ?object
|
|
||||||
{
|
|
||||||
$bagCode = trim($bagCode);
|
|
||||||
if ($bagCode === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->where('bp_lg_idx', $lgIdx)
|
|
||||||
->where('bp_bag_code', $bagCode)
|
|
||||||
->where('bp_state', 1)
|
|
||||||
->orderBy('bp_regdate', 'DESC')
|
|
||||||
->orderBy('bp_idx', 'DESC')
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use CodeIgniter\Model;
|
|
||||||
|
|
||||||
class BlockchainLedgerModel extends Model
|
|
||||||
{
|
|
||||||
protected $table = 'blockchain_ledger';
|
|
||||||
protected $primaryKey = 'bl_idx';
|
|
||||||
protected $returnType = 'object';
|
|
||||||
protected $useTimestamps = false;
|
|
||||||
protected $allowedFields = [
|
|
||||||
'bl_created_at',
|
|
||||||
'bl_tx_type',
|
|
||||||
'bl_entity_uuid',
|
|
||||||
'bl_entity_version',
|
|
||||||
'bl_payload',
|
|
||||||
'bl_previous_hash',
|
|
||||||
'bl_hash',
|
|
||||||
'bl_nonce',
|
|
||||||
'bl_actor_idx',
|
|
||||||
'bl_lg_idx',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -53,11 +53,7 @@ class CodeDetailModel extends Model
|
|||||||
$this->where('cd_state', 1);
|
$this->where('cd_state', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 동일 정렬값일 때는 코드값 기준으로 안정적으로 정렬한다.
|
return $this->orderBy('cd_sort', 'ASC')->orderBy('cd_idx', 'ASC')->findAll();
|
||||||
return $this->orderBy('cd_sort', 'ASC')
|
|
||||||
->orderBy('cd_code', 'ASC')
|
|
||||||
->orderBy('cd_idx', 'ASC')
|
|
||||||
->findAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,31 +12,21 @@ class DesignatedShopModel extends Model
|
|||||||
protected $useTimestamps = false;
|
protected $useTimestamps = false;
|
||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'ds_lg_idx',
|
'ds_lg_idx',
|
||||||
'ds_sa_idx',
|
|
||||||
'ds_mb_idx',
|
'ds_mb_idx',
|
||||||
'ds_shop_no',
|
'ds_shop_no',
|
||||||
'ds_name',
|
'ds_name',
|
||||||
'ds_biz_no',
|
'ds_biz_no',
|
||||||
'ds_rep_name',
|
'ds_rep_name',
|
||||||
'ds_biz_type',
|
|
||||||
'ds_biz_kind',
|
|
||||||
'ds_va_number',
|
'ds_va_number',
|
||||||
'ds_va_bank',
|
|
||||||
'ds_va_account',
|
|
||||||
'ds_zip',
|
'ds_zip',
|
||||||
'ds_addr',
|
'ds_addr',
|
||||||
'ds_addr_jibun',
|
'ds_addr_jibun',
|
||||||
'ds_addr_detail',
|
|
||||||
'ds_tel',
|
'ds_tel',
|
||||||
'ds_rep_phone',
|
'ds_rep_phone',
|
||||||
'ds_email',
|
'ds_email',
|
||||||
'ds_gugun_code',
|
'ds_gugun_code',
|
||||||
'ds_zone_code',
|
|
||||||
'ds_branch_no',
|
|
||||||
'ds_designated_at',
|
'ds_designated_at',
|
||||||
'ds_state',
|
'ds_state',
|
||||||
'ds_state_changed_at',
|
|
||||||
'ds_change_reason',
|
|
||||||
'ds_regdate',
|
'ds_regdate',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,102 +189,4 @@ class MenuModel extends Model
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 특정 메뉴 타입(mt_idx)을 source 지자체 기준으로 모든 지자체에 재배포.
|
|
||||||
* 기존 대상 지자체의 해당 타입 메뉴는 삭제 후 source 구조로 재생성한다.
|
|
||||||
*/
|
|
||||||
public function syncTypeToAllLgs(int $mtIdx, int $sourceLg): void
|
|
||||||
{
|
|
||||||
if ($mtIdx <= 0 || $sourceLg <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$source = $this->where('mt_idx', $mtIdx)
|
|
||||||
->where('lg_idx', $sourceLg)
|
|
||||||
->orderBy('mm_dep', 'ASC')
|
|
||||||
->orderBy('mm_num', 'ASC')
|
|
||||||
->findAll();
|
|
||||||
if (empty($source)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lgRows = $this->db->table('local_government')
|
|
||||||
->select('lg_idx')
|
|
||||||
->orderBy('lg_idx', 'ASC')
|
|
||||||
->get()
|
|
||||||
->getResultArray();
|
|
||||||
|
|
||||||
foreach ($lgRows as $lgRow) {
|
|
||||||
$destLg = (int) ($lgRow['lg_idx'] ?? 0);
|
|
||||||
if ($destLg <= 0 || $destLg === $sourceLg) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->db->transStart();
|
|
||||||
$this->where('mt_idx', $mtIdx)
|
|
||||||
->where('lg_idx', $destLg)
|
|
||||||
->delete();
|
|
||||||
|
|
||||||
$idMap = [];
|
|
||||||
foreach ($source as $row) {
|
|
||||||
$oldId = (int) ($row->mm_idx ?? 0);
|
|
||||||
$oldP = (int) ($row->mm_pidx ?? 0);
|
|
||||||
$newPidx = 0;
|
|
||||||
if ($oldP > 0 && isset($idMap[$oldP])) {
|
|
||||||
$newPidx = (int) $idMap[$oldP];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->insert([
|
|
||||||
'mt_idx' => $mtIdx,
|
|
||||||
'lg_idx' => $destLg,
|
|
||||||
'mm_name' => (string) ($row->mm_name ?? ''),
|
|
||||||
'mm_link' => (string) ($row->mm_link ?? ''),
|
|
||||||
'mm_pidx' => $newPidx,
|
|
||||||
'mm_dep' => (int) ($row->mm_dep ?? 0),
|
|
||||||
'mm_num' => (int) ($row->mm_num ?? 0),
|
|
||||||
'mm_cnode' => (int) ($row->mm_cnode ?? 0),
|
|
||||||
'mm_level' => (string) ($row->mm_level ?? ''),
|
|
||||||
'mm_is_view' => (string) ($row->mm_is_view ?? 'Y'),
|
|
||||||
]);
|
|
||||||
$idMap[$oldId] = (int) $this->getInsertID();
|
|
||||||
}
|
|
||||||
$this->db->transComplete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 재고 관리 하위 메뉴는 "재고 현황", "실사 선별 조회"만 유지.
|
|
||||||
*/
|
|
||||||
public function pruneInventoryManagementMenus(int $mtIdx, int $lgIdx): void
|
|
||||||
{
|
|
||||||
if ($mtIdx <= 0 || $lgIdx <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$parentRows = $this->where('mt_idx', $mtIdx)
|
|
||||||
->where('lg_idx', $lgIdx)
|
|
||||||
->where('mm_pidx', 0)
|
|
||||||
->groupStart()
|
|
||||||
->where('mm_name', '재고 관리')
|
|
||||||
->orWhere('mm_name', '재고관리')
|
|
||||||
->groupEnd()
|
|
||||||
->findAll();
|
|
||||||
if ($parentRows === []) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$parentIds = array_values(array_filter(array_map(
|
|
||||||
static fn ($row): int => (int) ($row->mm_idx ?? 0),
|
|
||||||
$parentRows
|
|
||||||
)));
|
|
||||||
if ($parentIds === []) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->where('mt_idx', $mtIdx)
|
|
||||||
->where('lg_idx', $lgIdx)
|
|
||||||
->whereIn('mm_pidx', $parentIds)
|
|
||||||
->whereNotIn('mm_link', ['bag/inventory', 'bag/inventory/inspection-select'])
|
|
||||||
->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,29 +16,4 @@ class PackagingUnitModel extends Model
|
|||||||
'pu_start_date', 'pu_end_date', 'pu_state',
|
'pu_start_date', 'pu_end_date', 'pu_state',
|
||||||
'pu_regdate', 'pu_moddate', 'pu_reg_mb_idx',
|
'pu_regdate', 'pu_moddate', 'pu_reg_mb_idx',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* 동일 봉투코드에 행이 여러 개여도 최신 등록 1건만 사용.
|
|
||||||
*
|
|
||||||
* @return array<string, object>
|
|
||||||
*/
|
|
||||||
public function latestActiveMapByBagCode(int $lgIdx): array
|
|
||||||
{
|
|
||||||
$rows = $this->where('pu_lg_idx', $lgIdx)
|
|
||||||
->where('pu_state', 1)
|
|
||||||
->orderBy('pu_regdate', 'DESC')
|
|
||||||
->orderBy('pu_idx', 'DESC')
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
$map = [];
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$code = (string) ($row->pu_bag_code ?? '');
|
|
||||||
if ($code === '' || isset($map[$code])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$map[$code] = $row;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class ShopOrderModel extends Model
|
|||||||
protected $useTimestamps = false;
|
protected $useTimestamps = false;
|
||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'so_lg_idx', 'so_ds_idx', 'so_ds_name', 'so_order_date', 'so_delivery_date',
|
'so_lg_idx', 'so_ds_idx', 'so_ds_name', 'so_order_date', 'so_delivery_date',
|
||||||
'so_payment_type', 'so_channel', 'so_paid', 'so_received', 'so_total_qty', 'so_total_amount',
|
'so_payment_type', 'so_paid', 'so_received', 'so_total_qty', 'so_total_amount',
|
||||||
'so_status', 'so_orderer_idx', 'so_regdate',
|
'so_status', 'so_orderer_idx', 'so_regdate',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<th>봉투코드</th>
|
<th>봉투코드</th>
|
||||||
<th>봉투명</th>
|
<th>봉투명</th>
|
||||||
<th>수량</th>
|
<th>수량</th>
|
||||||
|
<th class="w-20">상태</th>
|
||||||
<th class="w-24">작업</th>
|
<th class="w-24">작업</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
<td class="text-center font-mono"><?= esc($row->bi2_bag_code) ?></td>
|
<td class="text-center font-mono"><?= esc($row->bi2_bag_code) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->bi2_bag_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->bi2_bag_name) ?></td>
|
||||||
<td><?= number_format((int) $row->bi2_qty) ?></td>
|
<td><?= number_format((int) $row->bi2_qty) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bi2_status) ?></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<form action="<?= mgmt_url('bag-issues/cancel/' . (int) $row->bi2_idx) ?>" method="POST" class="inline" onsubmit="return confirm('취소하시겠습니까?');">
|
<form action="<?= mgmt_url('bag-issues/cancel/' . (int) $row->bi2_idx) ?>" method="POST" class="inline" onsubmit="return confirm('취소하시겠습니까?');">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($list)): ?>
|
<?php if (empty($list)): ?>
|
||||||
<tr><td colspan="10" class="text-center text-gray-400 py-4">등록된 불출이 없습니다.</td></tr>
|
<tr><td colspan="11" class="text-center text-gray-400 py-4">등록된 불출이 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,443 +1,83 @@
|
|||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
<span class="text-sm font-bold text-gray-700">발주 등록</span>
|
<span class="text-sm font-bold text-gray-700">발주 등록</span>
|
||||||
</section>
|
</section>
|
||||||
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-4xl">
|
||||||
<?php
|
<form action="<?= mgmt_url('bag-orders/store') ?>" method="POST" class="space-y-4">
|
||||||
$oldBagCodes = old('item_bag_code');
|
|
||||||
$oldQtyBoxes = old('item_qty_box');
|
|
||||||
$oldQtySheets = old('item_qty_sheet');
|
|
||||||
$oldBagCodes = is_array($oldBagCodes) ? $oldBagCodes : [];
|
|
||||||
$oldQtyBoxes = is_array($oldQtyBoxes) ? $oldQtyBoxes : [];
|
|
||||||
$oldQtySheets = is_array($oldQtySheets) ? $oldQtySheets : [];
|
|
||||||
$defaultOrderDate = old('bo_order_date', date('Y-m-d'));
|
|
||||||
$defaultOrderMonth = old('bo_order_month_ui', substr($defaultOrderDate, 0, 7));
|
|
||||||
|
|
||||||
$bagMeta = [];
|
|
||||||
foreach (($bagReferenceRows ?? []) as $row) {
|
|
||||||
$bagMeta[$row['code']] = [
|
|
||||||
'name' => $row['name'],
|
|
||||||
'orderPrice' => (float) $row['orderPrice'],
|
|
||||||
'boxPerPack' => (int) $row['boxPerPack'],
|
|
||||||
'packPerSheet' => (int) $row['packPerSheet'],
|
|
||||||
'totalPerBox' => max(1, (int) $row['totalPerBox']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$initialSelectedItems = [];
|
|
||||||
$maxOldCount = max(count($oldBagCodes), count($oldQtySheets), count($oldQtyBoxes));
|
|
||||||
for ($i = 0; $i < $maxOldCount; $i++) {
|
|
||||||
$code = trim((string) ($oldBagCodes[$i] ?? ''));
|
|
||||||
if ($code === '' || ! isset($bagMeta[$code])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$fallbackQtyBox = (int) ($oldQtyBoxes[$i] ?? 0);
|
|
||||||
$rawQtySheet = (int) ($oldQtySheets[$i] ?? 0);
|
|
||||||
$fallbackTotalPerBox = (int) ($bagMeta[$code]['totalPerBox'] ?? 1);
|
|
||||||
if ($fallbackQtyBox <= 0 && $rawQtySheet > 0) {
|
|
||||||
$fallbackQtyBox = intdiv($rawQtySheet, max(1, $fallbackTotalPerBox));
|
|
||||||
}
|
|
||||||
$initialSelectedItems[] = [
|
|
||||||
'code' => $code,
|
|
||||||
'qtyBox' => max(0, $fallbackQtyBox),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
|
||||||
?>
|
|
||||||
|
|
||||||
<form action="<?= mgmt_url('bag-orders/store') ?>" method="POST" class="mt-2 space-y-2">
|
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<div class="border border-gray-300 bg-white p-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<div class="flex flex-wrap items-center gap-4 text-sm">
|
<label class="block text-sm font-bold text-gray-700 w-28">발주일 <span class="text-red-500">*</span></label>
|
||||||
<div class="flex items-center gap-2">
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="bo_order_date" type="date" value="<?= esc(old('bo_order_date', date('Y-m-d'))) ?>" required/>
|
||||||
<label for="bo_order_month_ui" class="font-bold text-gray-700">발주월</label>
|
|
||||||
<input id="bo_order_month_ui" name="bo_order_month_ui" type="month" value="<?= esc($defaultOrderMonth) ?>" class="border border-gray-300 rounded px-2 py-1" />
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<label for="bo_order_date" class="font-bold text-gray-700">발주일 <span class="text-red-500">*</span></label>
|
|
||||||
<input id="bo_order_date" name="bo_order_date" type="date" value="<?= esc($defaultOrderDate) ?>" required class="border border-gray-300 rounded px-2 py-1" />
|
|
||||||
</div>
|
|
||||||
<p class="text-blue-600 font-bold">※ 발주수량은 박스단위로 입력해 주세요. (발주일은 미래일도 선택 가능)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-12 gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<section class="xl:col-span-5 border border-gray-300 bg-white">
|
<label class="block text-sm font-bold text-gray-700 w-28">수수료율</label>
|
||||||
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 이력</div>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32 text-right" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', '0')) ?>"/>
|
||||||
<div class="overflow-auto max-h-[410px]">
|
<span class="text-sm text-gray-500">%</span>
|
||||||
<table class="w-full data-table text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="w-28">발주일</th>
|
|
||||||
<th>제작업체</th>
|
|
||||||
<th>입고처</th>
|
|
||||||
<th class="w-16">상태</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach (($recentOrders ?? []) as $history): ?>
|
|
||||||
<tr>
|
|
||||||
<td class="text-center"><?= esc((string) $history->bo_order_date) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc((string) ($companyMap[(int) $history->bo_company_idx] ?? '-')) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc((string) ($agencyMap[(int) $history->bo_agency_idx] ?? '-')) ?></td>
|
|
||||||
<td class="text-center"><?= esc((string) ($statusMap[(string) $history->bo_status] ?? $history->bo_status)) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($recentOrders)): ?>
|
|
||||||
<tr><td colspan="4" class="text-center text-gray-400 py-4">발주 이력이 없습니다.</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="xl:col-span-7 border border-gray-300 bg-white">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">발주 Form</div>
|
<label class="block text-sm font-bold text-gray-700 w-28">제작업체</label>
|
||||||
<div class="p-2 space-y-2">
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_company_idx">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
||||||
<div class="flex items-center gap-2 text-sm">
|
|
||||||
<label for="bo_fee_rate" class="w-20 font-bold text-gray-700">수수료</label>
|
|
||||||
<input id="bo_fee_rate" name="bo_fee_rate" type="number" step="0.01" value="<?= esc(old('bo_fee_rate', '0')) ?>" class="border border-gray-300 rounded px-2 py-1 w-24 text-right" />
|
|
||||||
<span>%</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 text-sm">
|
|
||||||
<label for="bo_association_idx" class="w-20 font-bold text-gray-700">협회</label>
|
|
||||||
<select id="bo_association_idx" name="bo_association_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
|
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach (($associations ?? []) as $association): ?>
|
<?php foreach ($companies as $cp): ?>
|
||||||
<option value="<?= esc((string) $association->cp_idx) ?>" <?= (int) old('bo_association_idx') === (int) $association->cp_idx ? 'selected' : '' ?>>
|
<option value="<?= esc($cp->cp_idx) ?>" <?= (int) old('bo_company_idx') === (int) $cp->cp_idx ? 'selected' : '' ?>>
|
||||||
<?= esc((string) $association->cp_name) ?>
|
<?= esc($cp->cp_name) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-sm">
|
|
||||||
<label for="bo_company_idx" class="w-20 font-bold text-gray-700">제작업체</label>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<select id="bo_company_idx" name="bo_company_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
|
<label class="block text-sm font-bold text-gray-700 w-28">입고처</label>
|
||||||
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="bo_agency_idx">
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach (($companies ?? []) as $company): ?>
|
<?php foreach ($agencies as $ag): ?>
|
||||||
<option value="<?= esc((string) $company->cp_idx) ?>" <?= (int) old('bo_company_idx') === (int) $company->cp_idx ? 'selected' : '' ?>>
|
<option value="<?= esc($ag->sa_idx) ?>" <?= (int) old('bo_agency_idx') === (int) $ag->sa_idx ? 'selected' : '' ?>>
|
||||||
<?= esc((string) $company->cp_name) ?>
|
[<?= esc($ag->sa_kind ?? '') ?>] <?= esc($ag->sa_code ?? '') ?> — <?= esc($ag->sa_name) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-sm">
|
|
||||||
<label for="bo_agency_idx" class="w-20 font-bold text-gray-700">입고처</label>
|
|
||||||
<select id="bo_agency_idx" name="bo_agency_idx" class="border border-gray-300 rounded px-2 py-1 w-full">
|
|
||||||
<option value="">선택</option>
|
|
||||||
<?php foreach (($agencies ?? []) as $agency): ?>
|
|
||||||
<option value="<?= esc((string) $agency->sa_idx) ?>" <?= (int) old('bo_agency_idx') === (int) $agency->sa_idx ? 'selected' : '' ?>>
|
|
||||||
[<?= esc((string) ($agency->sa_kind ?? '')) ?>] <?= esc((string) ($agency->sa_code ?? '')) ?> — <?= esc((string) $agency->sa_name) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 mb-2">발주 품목</label>
|
||||||
<div class="border border-gray-300 overflow-auto">
|
<div class="border border-gray-300 overflow-auto">
|
||||||
<table class="w-full data-table text-sm order-input-table" id="order-item-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-12">번호</th>
|
<th class="w-16">순번</th>
|
||||||
<th class="w-16">선택</th>
|
<th>봉투</th>
|
||||||
<th>품명</th>
|
<th class="w-32">박스수</th>
|
||||||
<th class="w-28">수량(BOX)</th>
|
|
||||||
<th class="w-24">단가</th>
|
|
||||||
<th class="w-24">환산수량</th>
|
|
||||||
<th class="w-28">금액</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="selected-order-items-body"></tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<th colspan="3" class="text-center">계</th>
|
|
||||||
<th class="text-right pr-2" id="sum-box-qty">0</th>
|
|
||||||
<th></th>
|
|
||||||
<th class="text-right pr-2" id="sum-sheet-qty">0</th>
|
|
||||||
<th class="text-right pr-2" id="sum-amount">0</th>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2 pt-1">
|
|
||||||
<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="<?= mgmt_url('bag-orders') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<p class="text-xs text-gray-600 px-2 py-1">아래 목록에서 봉투를 선택하면 발주 품목에 추가됩니다. (개수 제한 없음)</p>
|
|
||||||
<div class="overflow-auto">
|
|
||||||
<table class="w-full data-table text-sm order-reference-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="w-12">번호</th>
|
|
||||||
<th class="w-20">선택</th>
|
|
||||||
<th>봉투 종류</th>
|
|
||||||
<th class="w-24">발주단가</th>
|
|
||||||
<th class="w-24">Box당 팩</th>
|
|
||||||
<th class="w-24">팩당 낱장</th>
|
|
||||||
<th class="w-28">1박스 총 낱장</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach (($bagReferenceRows ?? []) as $idx => $row): ?>
|
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||||
<tr data-reference-row data-code="<?= esc((string) $row['code']) ?>" class="cursor-pointer">
|
<tr>
|
||||||
<td class="text-center"><?= $idx + 1 ?></td>
|
<td class="text-center"><?= $i + 1 ?></td>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<button type="button" class="js-toggle-bag border border-gray-300 rounded px-2 py-0.5 text-xs hover:bg-gray-100" data-code="<?= esc((string) $row['code']) ?>">선택</button>
|
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full" name="item_bag_code[]">
|
||||||
</td>
|
<option value="">선택</option>
|
||||||
<td class="text-left pl-2"><?= esc((string) $row['name']) ?></td>
|
<?php foreach ($bagCodes as $cd): ?>
|
||||||
<td class="text-right pr-2"><?= number_format((float) $row['orderPrice'], 2) ?></td>
|
<option value="<?= esc($cd->cd_code) ?>">
|
||||||
<td class="text-right pr-2"><?= number_format((int) $row['boxPerPack']) ?></td>
|
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||||
<td class="text-right pr-2"><?= number_format((int) $row['packPerSheet']) ?></td>
|
</option>
|
||||||
<td class="text-right pr-2"><?= number_format((int) $row['totalPerBox']) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($bagReferenceRows)): ?>
|
</select>
|
||||||
<tr><td colspan="7" class="text-center text-gray-400 py-4">표시할 봉투 기준 데이터가 없습니다.</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.order-input-table tbody tr,
|
|
||||||
.order-reference-table tbody tr {
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
.order-input-table tbody td,
|
|
||||||
.order-reference-table tbody td {
|
|
||||||
padding-top: 4px;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(() => {
|
|
||||||
const bagMeta = <?= json_encode($bagMeta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
||||||
const initialSelectedItems = <?= json_encode($initialSelectedItems, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
||||||
const selectedBody = document.getElementById('selected-order-items-body');
|
|
||||||
const referenceRows = Array.from(document.querySelectorAll('[data-reference-row]'));
|
|
||||||
const sumBoxQtyEl = document.getElementById('sum-box-qty');
|
|
||||||
const sumSheetQtyEl = document.getElementById('sum-sheet-qty');
|
|
||||||
const sumAmountEl = document.getElementById('sum-amount');
|
|
||||||
const monthInput = document.getElementById('bo_order_month_ui');
|
|
||||||
const orderDateInput = document.getElementById('bo_order_date');
|
|
||||||
const orderForm = document.querySelector('form[action*="bag-orders/store"]');
|
|
||||||
const selectedItems = new Map();
|
|
||||||
let activeCode = null;
|
|
||||||
|
|
||||||
const formatNumber = (value) => new Intl.NumberFormat('ko-KR').format(Number.isFinite(value) ? value : 0);
|
|
||||||
const escapeHtml = (value) => String(value ?? '')
|
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('"', '"')
|
|
||||||
.replaceAll("'", ''');
|
|
||||||
|
|
||||||
const syncMonthFromDate = () => {
|
|
||||||
if (!orderDateInput || !monthInput || !orderDateInput.value) return;
|
|
||||||
monthInput.value = orderDateInput.value.substring(0, 7);
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncDateFromMonth = () => {
|
|
||||||
if (!orderDateInput || !monthInput || !monthInput.value) return;
|
|
||||||
const parts = monthInput.value.split('-');
|
|
||||||
if (parts.length !== 2) return;
|
|
||||||
const year = Number(parts[0]);
|
|
||||||
const month = Number(parts[1]);
|
|
||||||
if (!Number.isFinite(year) || !Number.isFinite(month)) return;
|
|
||||||
|
|
||||||
const currentDay = orderDateInput.value ? Number(orderDateInput.value.split('-')[2]) : 1;
|
|
||||||
const lastDay = new Date(year, month, 0).getDate();
|
|
||||||
const day = String(Math.min(Math.max(currentDay, 1), lastDay)).padStart(2, '0');
|
|
||||||
orderDateInput.value = `${String(year)}-${String(month).padStart(2, '0')}-${day}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateReferenceSelectionUi = () => {
|
|
||||||
referenceRows.forEach((row) => {
|
|
||||||
const code = row.dataset.code || '';
|
|
||||||
const button = row.querySelector('.js-toggle-bag');
|
|
||||||
const isSelected = selectedItems.has(code);
|
|
||||||
row.classList.toggle('bg-blue-50', isSelected);
|
|
||||||
if (button) {
|
|
||||||
button.textContent = isSelected ? '선택됨' : '선택';
|
|
||||||
button.classList.toggle('bg-blue-600', isSelected);
|
|
||||||
button.classList.toggle('text-white', isSelected);
|
|
||||||
button.classList.toggle('border-blue-600', isSelected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTotals = () => {
|
|
||||||
let sumBoxQty = 0;
|
|
||||||
let sumSheetQty = 0;
|
|
||||||
let sumAmount = 0;
|
|
||||||
|
|
||||||
selectedBody.querySelectorAll('tr[data-item-row]').forEach((row) => {
|
|
||||||
const code = row.dataset.code || '';
|
|
||||||
const qtyInput = row.querySelector('.item-qty-box');
|
|
||||||
const qtyBox = Math.max(0, parseInt(qtyInput?.value || '0', 10));
|
|
||||||
const meta = bagMeta[code] || { orderPrice: 0, totalPerBox: 1 };
|
|
||||||
const unitPrice = Number(meta.orderPrice || 0);
|
|
||||||
const totalPerBox = Math.max(1, Number(meta.totalPerBox || 1));
|
|
||||||
const qtySheet = qtyBox * totalPerBox;
|
|
||||||
const amount = qtySheet * unitPrice;
|
|
||||||
|
|
||||||
const unitPriceEl = row.querySelector('.item-unit-price');
|
|
||||||
const qtySheetEl = row.querySelector('.item-qty-sheet');
|
|
||||||
const sheetHelpEl = row.querySelector('.item-sheet-help');
|
|
||||||
const amountEl = row.querySelector('.item-amount');
|
|
||||||
if (unitPriceEl) unitPriceEl.textContent = formatNumber(unitPrice);
|
|
||||||
if (qtySheetEl) qtySheetEl.textContent = formatNumber(qtySheet);
|
|
||||||
if (sheetHelpEl) sheetHelpEl.textContent = `낱장 ${formatNumber(qtySheet)}장`;
|
|
||||||
if (amountEl) amountEl.textContent = formatNumber(amount);
|
|
||||||
|
|
||||||
selectedItems.set(code, { qtyBox });
|
|
||||||
sumBoxQty += qtyBox;
|
|
||||||
sumSheetQty += qtySheet;
|
|
||||||
sumAmount += amount;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sumBoxQtyEl) sumBoxQtyEl.textContent = formatNumber(sumBoxQty);
|
|
||||||
if (sumSheetQtyEl) sumSheetQtyEl.textContent = formatNumber(sumSheetQty);
|
|
||||||
if (sumAmountEl) sumAmountEl.textContent = formatNumber(sumAmount);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setActiveRow = (code) => {
|
|
||||||
activeCode = code || null;
|
|
||||||
selectedBody.querySelectorAll('tr[data-item-row]').forEach((row) => {
|
|
||||||
row.classList.toggle('bg-amber-50', row.dataset.code === activeCode);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSelectedRows = () => {
|
|
||||||
const codes = Object.keys(bagMeta).filter((code) => selectedItems.has(code));
|
|
||||||
if (codes.length === 0) {
|
|
||||||
selectedBody.innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-4">아래 "발주 등록 종류"에서 봉투를 선택해 주세요.</td></tr>';
|
|
||||||
setActiveRow(null);
|
|
||||||
updateTotals();
|
|
||||||
updateReferenceSelectionUi();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedBody.innerHTML = codes.map((code, idx) => {
|
|
||||||
const meta = bagMeta[code];
|
|
||||||
const qtyBox = Math.max(0, parseInt(String(selectedItems.get(code)?.qtyBox ?? 0), 10));
|
|
||||||
const name = meta?.name || code;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<tr data-item-row data-code="${escapeHtml(code)}" class="cursor-pointer">
|
|
||||||
<td class="text-center">${idx + 1}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<button type="button" class="js-remove-selected text-xs text-red-600 hover:underline" data-code="${escapeHtml(code)}">해제</button>
|
|
||||||
</td>
|
|
||||||
<td class="text-left pl-2">
|
|
||||||
${escapeHtml(name)}
|
|
||||||
<input type="hidden" name="item_bag_code[]" value="${escapeHtml(code)}" />
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input name="item_qty_box[]" type="number" min="0" step="1" value="${qtyBox}" class="item-qty-box border border-gray-300 rounded px-2 py-1 text-sm w-full text-right leading-tight" />
|
<input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right" name="item_qty_box[]" type="number" min="0" value="0"/>
|
||||||
<p class="text-[11px] text-gray-500 mt-1 item-sheet-help">낱장 0장</p>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right pr-2 item-unit-price">0</td>
|
|
||||||
<td class="text-right pr-2 item-qty-sheet">0</td>
|
|
||||||
<td class="text-right pr-2 item-amount">0</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
<?php endfor; ?>
|
||||||
}).join('');
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
if (!activeCode || !selectedItems.has(activeCode)) {
|
<div class="flex gap-2 pt-2">
|
||||||
activeCode = codes[0];
|
<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="<?= mgmt_url('bag-orders') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||||
setActiveRow(activeCode);
|
</div>
|
||||||
updateTotals();
|
</form>
|
||||||
updateReferenceSelectionUi();
|
</div>
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSelection = (code) => {
|
|
||||||
if (!code || !bagMeta[code]) return;
|
|
||||||
if (selectedItems.has(code)) {
|
|
||||||
selectedItems.delete(code);
|
|
||||||
if (activeCode === code) activeCode = null;
|
|
||||||
} else {
|
|
||||||
selectedItems.set(code, { qtyBox: 0 });
|
|
||||||
activeCode = code;
|
|
||||||
}
|
|
||||||
renderSelectedRows();
|
|
||||||
};
|
|
||||||
|
|
||||||
initialSelectedItems.forEach((item) => {
|
|
||||||
if (!item || !item.code || !bagMeta[item.code]) return;
|
|
||||||
selectedItems.set(item.code, { qtyBox: Math.max(0, parseInt(String(item.qtyBox ?? 0), 10)) });
|
|
||||||
activeCode = item.code;
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedBody.addEventListener('click', (event) => {
|
|
||||||
const removeButton = event.target.closest('.js-remove-selected');
|
|
||||||
if (removeButton) {
|
|
||||||
toggleSelection(removeButton.dataset.code || '');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = event.target.closest('tr[data-item-row]');
|
|
||||||
if (!row) return;
|
|
||||||
const code = row.dataset.code || '';
|
|
||||||
setActiveRow(code);
|
|
||||||
const qtyInput = row.querySelector('.item-qty-box');
|
|
||||||
if (qtyInput) qtyInput.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedBody.addEventListener('input', (event) => {
|
|
||||||
const qtyInput = event.target.closest('.item-qty-box');
|
|
||||||
if (!qtyInput) return;
|
|
||||||
const row = qtyInput.closest('tr[data-item-row]');
|
|
||||||
if (!row) return;
|
|
||||||
const code = row.dataset.code || '';
|
|
||||||
selectedItems.set(code, { qtyBox: Math.max(0, parseInt(qtyInput.value || '0', 10)) });
|
|
||||||
updateTotals();
|
|
||||||
});
|
|
||||||
|
|
||||||
referenceRows.forEach((row) => {
|
|
||||||
row.addEventListener('click', (event) => {
|
|
||||||
const button = event.target.closest('.js-toggle-bag');
|
|
||||||
if (button) {
|
|
||||||
toggleSelection(button.dataset.code || '');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.target.closest('td')) {
|
|
||||||
toggleSelection(row.dataset.code || '');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (monthInput) monthInput.addEventListener('change', () => { syncDateFromMonth(); updateTotals(); });
|
|
||||||
if (orderDateInput) orderDateInput.addEventListener('change', syncMonthFromDate);
|
|
||||||
|
|
||||||
if (orderForm) {
|
|
||||||
orderForm.addEventListener('submit', (event) => {
|
|
||||||
const hasValidItem = Array.from(selectedBody.querySelectorAll('tr[data-item-row]')).some((row) => {
|
|
||||||
const qtyInput = row.querySelector('.item-qty-box');
|
|
||||||
return Math.max(0, parseInt(qtyInput?.value || '0', 10)) > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasValidItem) {
|
|
||||||
event.preventDefault();
|
|
||||||
alert('봉투를 선택하고 수량을 1 이상 입력해 주세요.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
syncMonthFromDate();
|
|
||||||
renderSelectedRows();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,200 +1,81 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '발주 현황']) ?>
|
||||||
// 발주기간: native month 입력은 로케일에 따라 Jan 등 영문 표기될 수 있어 YYYY-MM select + 한글 라벨 사용
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
$bagOrderYmChoices = [];
|
|
||||||
$bagOrderYmCenterY = (int) date('Y');
|
|
||||||
for ($by = $bagOrderYmCenterY - 4; $by <= $bagOrderYmCenterY + 2; $by++) {
|
|
||||||
for ($bm = 1; $bm <= 12; $bm++) {
|
|
||||||
$bagOrderYmChoices[] = sprintf('%04d-%02d', $by, $bm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ([(string) ($startMonth ?? ''), (string) ($endMonth ?? '')] as $ymExtra) {
|
|
||||||
if (preg_match('/^\d{4}-\d{2}$/', $ymExtra) && ! in_array($ymExtra, $bagOrderYmChoices, true)) {
|
|
||||||
$bagOrderYmChoices[] = $ymExtra;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort($bagOrderYmChoices);
|
|
||||||
$bagOrderYmLabel = static function (string $ym): string {
|
|
||||||
if (preg_match('/^(\d{4})-(\d{2})$/', $ym, $m)) {
|
|
||||||
return $m[1] . '년 ' . (int) $m[2] . '월';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $ym;
|
|
||||||
};
|
|
||||||
?>
|
|
||||||
<?= view('components/print_header', ['printTitle' => '봉투 발주 현황']) ?>
|
|
||||||
<section class="no-print border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
<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 items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a href="<?= mgmt_url('bag-orders/export') . '?' . http_build_query(array_filter(['start_month' => $startMonth ?? '', 'end_month' => $endMonth ?? '', 'company_idx' => $companyIdx ?? 0, 'bag_code' => $bagCode ?? '', 'receive_type' => $receiveType ?? ''])) ?>" 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>
|
<a href="<?= mgmt_url('bag-orders/export') . '?' . http_build_query(array_filter(['start_date' => $startDate ?? '', 'end_date' => $endDate ?? '', 'status' => $status ?? ''])) ?>" 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 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>
|
<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="<?= mgmt_url('bag-orders/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">발주 등록</a>
|
<a href="<?= mgmt_url('bag-orders/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">발주 등록</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
<section class="no-print p-2 bg-white border-b border-gray-200">
|
<form method="GET" action="<?= mgmt_url('bag-orders') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<!-- GBMS 발주현황: 발주기간은 [시작] ~ [끝] 한 줄 고정, 필터 블록은 가로 나열 후 좁으면 블록 단위로만 줄바꿈 -->
|
<label class="text-sm text-gray-600">발주일</label>
|
||||||
<form method="GET" action="<?= mgmt_url('bag-orders') ?>" class="flex flex-wrap items-end gap-x-5 gap-y-3 w-full">
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
<div class="flex flex-nowrap items-center gap-2 shrink-0">
|
<label class="text-sm text-gray-600">~</label>
|
||||||
<label class="text-sm text-gray-600 whitespace-nowrap">발주 기간</label>
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
<select name="start_month" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11.5rem] max-w-[13rem] shrink-0">
|
<label class="text-sm text-gray-600">상태</label>
|
||||||
<?php foreach ($bagOrderYmChoices as $ym): ?>
|
<select name="status" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||||
<option value="<?= esc($ym) ?>" <?= ($startMonth ?? date('Y-m')) === $ym ? 'selected' : '' ?>><?= esc($bagOrderYmLabel($ym)) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
<span class="text-sm text-gray-500 select-none">~</span>
|
|
||||||
<select name="end_month" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11.5rem] max-w-[13rem] shrink-0">
|
|
||||||
<?php foreach ($bagOrderYmChoices as $ym): ?>
|
|
||||||
<option value="<?= esc($ym) ?>" <?= ($endMonth ?? date('Y-m')) === $ym ? 'selected' : '' ?>><?= esc($bagOrderYmLabel($ym)) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-nowrap items-center gap-2 shrink-0">
|
|
||||||
<label class="text-sm text-gray-600 whitespace-nowrap">제작 업체</label>
|
|
||||||
<select name="company_idx" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11rem] max-w-[14rem]">
|
|
||||||
<option value="0">전 체</option>
|
|
||||||
<?php foreach (($companyOptions ?? []) as $company): ?>
|
|
||||||
<option value="<?= (int) $company->cp_idx ?>" <?= (int) ($companyIdx ?? 0) === (int) $company->cp_idx ? 'selected' : '' ?>>
|
|
||||||
<?= esc((string) $company->cp_name) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-nowrap items-center gap-2 shrink-0">
|
|
||||||
<label class="text-sm text-gray-600 whitespace-nowrap">품 명</label>
|
|
||||||
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 text-sm w-[11rem] max-w-[16rem]">
|
|
||||||
<option value="">전체</option>
|
<option value="">전체</option>
|
||||||
<?php foreach (($bagCodeOptions ?? []) as $bag): ?>
|
<option value="normal" <?= ($status ?? '') === 'normal' ? 'selected' : '' ?>>정상</option>
|
||||||
<option value="<?= esc((string) $bag->cd_code) ?>" <?= (string) ($bagCode ?? '') === (string) $bag->cd_code ? 'selected' : '' ?>>
|
<option value="cancelled" <?= ($status ?? '') === 'cancelled' ? 'selected' : '' ?>>취소</option>
|
||||||
<?= esc((string) $bag->cd_name) ?>
|
<option value="deleted" <?= ($status ?? '') === 'deleted' ? 'selected' : '' ?>>삭제</option>
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-nowrap items-center gap-2 shrink-0">
|
|
||||||
<label class="text-sm text-gray-600 whitespace-nowrap">입고 구분</label>
|
|
||||||
<select name="receive_type" class="border border-gray-300 rounded px-2 py-1 text-sm w-[8.5rem]">
|
|
||||||
<option value="all" <?= ($receiveType ?? 'all') === 'all' ? 'selected' : '' ?>>전 체</option>
|
|
||||||
<option value="received" <?= ($receiveType ?? 'all') === 'received' ? 'selected' : '' ?>>입고완료</option>
|
|
||||||
<option value="pending" <?= ($receiveType ?? 'all') === 'pending' ? 'selected' : '' ?>>미입고</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-nowrap items-center gap-2 shrink-0 sm:ml-auto">
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<a href="<?= mgmt_url('bag-orders') ?>" class="text-sm text-gray-500 hover:underline whitespace-nowrap">초기화</a>
|
<a href="<?= mgmt_url('bag-orders') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<div class="bag-order-print-wrap border border-gray-300 overflow-auto mt-2">
|
<table class="w-full data-table">
|
||||||
<table class="bag-order-print-table w-full data-table">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-32">발주일자</th>
|
<th class="w-16">번호</th>
|
||||||
<th class="min-w-[10rem]">제작 업체</th>
|
<th>LOT번호</th>
|
||||||
<th class="min-w-[12rem]">품 명</th>
|
<th>발주일</th>
|
||||||
<th class="w-28">발주 수량</th>
|
<th>제작업체</th>
|
||||||
<th class="w-28">입고 수량</th>
|
<th>입고처</th>
|
||||||
<th class="w-28">미입고수량</th>
|
<th>품목수</th>
|
||||||
<th class="w-32">발주 금액</th>
|
<th>총수량</th>
|
||||||
<th class="min-w-[9rem]">입고처</th>
|
<th>총금액</th>
|
||||||
<th class="min-w-[8rem]">비 고</th>
|
<th class="w-20">상태</th>
|
||||||
|
<th class="w-44">작업</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php $printedGroup = []; ?>
|
<?php foreach ($list as $row): ?>
|
||||||
<?php foreach (($rows ?? []) as $row): ?>
|
|
||||||
<?php if (! empty($row['is_subtotal'])): ?>
|
|
||||||
<tr class="bg-gray-50 font-semibold">
|
|
||||||
<td colspan="3" class="text-center"><?= esc((string) ($row['label'] ?? '소계')) ?></td>
|
|
||||||
<td><?= number_format((int) ($row['order_qty'] ?? 0)) ?></td>
|
|
||||||
<td><?= number_format((int) ($row['received_qty'] ?? 0)) ?></td>
|
|
||||||
<td><?= number_format((int) ($row['pending_qty'] ?? 0)) ?></td>
|
|
||||||
<td><?= number_format((float) ($row['amount'] ?? 0)) ?></td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
<?php continue; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$boIdx = (int) ($row['bo_idx'] ?? 0);
|
|
||||||
$showGroup = ! isset($printedGroup[$boIdx]);
|
|
||||||
$rowspan = (int) (($groupRows[$boIdx] ?? 1));
|
|
||||||
if ($showGroup) {
|
|
||||||
$printedGroup[$boIdx] = true;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<?php if ($showGroup): ?>
|
<td class="text-center"><?= esc($row->bo_idx) ?></td>
|
||||||
<td class="text-center align-top" rowspan="<?= $rowspan ?>"><?= esc((string) ($row['order_date'] ?? '')) ?></td>
|
<td class="text-center font-mono"><?= esc($row->bo_lot_no) ?></td>
|
||||||
<td class="text-left pl-2 align-top" rowspan="<?= $rowspan ?>"><?= esc((string) ($row['company_name'] ?? '')) ?></td>
|
<td class="text-center"><?= esc($row->bo_order_date) ?></td>
|
||||||
<?php endif; ?>
|
<td class="text-left pl-2"><?= esc($companyMap[$row->bo_company_idx] ?? '') ?></td>
|
||||||
<td class="text-left pl-2"><?= esc((string) ($row['bag_name'] ?? '')) ?></td>
|
<td class="text-left pl-2"><?= esc($agencyMap[$row->bo_agency_idx] ?? '') ?></td>
|
||||||
<td><?= number_format((int) ($row['order_qty'] ?? 0)) ?></td>
|
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['count'] ?? 0)) ?></td>
|
||||||
<td><?= number_format((int) ($row['received_qty'] ?? 0)) ?></td>
|
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['qty'] ?? 0)) ?></td>
|
||||||
<td><?= number_format((int) ($row['pending_qty'] ?? 0)) ?></td>
|
<td><?= number_format((int) ($itemSummary[$row->bo_idx]['amount'] ?? 0)) ?></td>
|
||||||
<td><?= number_format((float) ($row['amount'] ?? 0)) ?></td>
|
<td class="text-center">
|
||||||
<td class="text-left pl-2"><?= esc((string) ($row['agency_name'] ?? '')) ?></td>
|
<?php
|
||||||
<td></td>
|
$statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제'];
|
||||||
|
echo esc($statusMap[$row->bo_status] ?? $row->bo_status);
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="<?= mgmt_url('bag-orders/detail/' . (int) $row->bo_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">상세</a>
|
||||||
|
<form action="<?= mgmt_url('bag-orders/cancel/' . (int) $row->bo_idx) ?>" method="POST" class="inline" onsubmit="return confirm('취소하시겠습니까?');">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button type="submit" class="text-orange-600 hover:underline text-sm mr-1">취소</button>
|
||||||
|
</form>
|
||||||
|
<form action="<?= mgmt_url('bag-orders/delete/' . (int) $row->bo_idx) ?>" method="POST" class="inline" onsubmit="return confirm('삭제하시겠습니까?');">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($list)): ?>
|
||||||
<?php if (empty($rows ?? [])): ?>
|
<tr><td colspan="10" class="text-center text-gray-400 py-4">등록된 발주가 없습니다.</td></tr>
|
||||||
<tr><td colspan="9" class="text-center text-gray-400 py-6">조회 조건에 해당하는 발주 내역이 없습니다.</td></tr>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
|
||||||
<tr class="bg-gray-100 font-bold">
|
|
||||||
<td colspan="3" class="text-center">총계</td>
|
|
||||||
<td class="text-right"><?= number_format((int) ($grandTotals['order_qty'] ?? 0)) ?></td>
|
|
||||||
<td class="text-right"><?= number_format((int) ($grandTotals['received_qty'] ?? 0)) ?></td>
|
|
||||||
<td class="text-right"><?= number_format((int) ($grandTotals['pending_qty'] ?? 0)) ?></td>
|
|
||||||
<td class="text-right"><?= number_format((float) ($grandTotals['amount'] ?? 0)) ?></td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
<style>
|
|
||||||
@media print {
|
|
||||||
#debug-icon,
|
|
||||||
#debug-bar,
|
|
||||||
#debug-bar-contents,
|
|
||||||
#debug-toolbar,
|
|
||||||
.debug-toolbar,
|
|
||||||
.ci-debug-toolbar,
|
|
||||||
[id^='debug-bar-'],
|
|
||||||
[id^='debug-icon'],
|
|
||||||
[class*='debug-toolbar'] {
|
|
||||||
display: none !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bag-order-print-wrap {
|
|
||||||
overflow: visible !important;
|
|
||||||
border: none !important;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bag-order-print-table {
|
|
||||||
width: 100% !important;
|
|
||||||
table-layout: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bag-order-print-table th,
|
|
||||||
.bag-order-print-table td {
|
|
||||||
white-space: nowrap !important;
|
|
||||||
word-break: keep-all !important;
|
|
||||||
overflow-wrap: normal !important;
|
|
||||||
font-size: 10px !important;
|
|
||||||
padding: 2px 3px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -9,91 +9,12 @@
|
|||||||
<span class="text-sm font-bold text-gray-700">봉투 단가 관리</span>
|
<span class="text-sm font-bold text-gray-700">봉투 단가 관리</span>
|
||||||
<div class="flex items-center gap-2 no-print">
|
<div class="flex items-center gap-2 no-print">
|
||||||
<button onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
<button onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||||
|
<a href="<?= base_url('bag/prices') ?>" class="text-blue-600 hover:underline text-sm">단가 조회·검색</a>
|
||||||
<a href="<?= mgmt_url('bag-prices/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">단가 등록</a>
|
<a href="<?= mgmt_url('bag-prices/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">단가 등록</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="no-print border border-gray-200 rounded-lg bg-white p-3 mt-2">
|
<p class="text-xs text-gray-500 mt-2 no-print">목록·등록·수정·삭제는 이 화면에서, <strong>기간·봉투별 조회·인쇄</strong>는 <a href="<?= base_url('bag/prices') ?>" class="text-blue-600 hover:underline">봉투 단가(조회)</a>에서 이용하세요.</p>
|
||||||
<form method="get" action="<?= mgmt_url('bag-prices') ?>" class="flex flex-wrap items-end gap-3" autocomplete="off">
|
|
||||||
<div class="flex flex-col gap-0.5">
|
|
||||||
<label class="text-xs text-gray-500">봉투구분</label>
|
|
||||||
<select name="bag_kind_e" class="border border-gray-300 rounded px-2 py-1.5 text-sm min-w-[9rem]">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<?php foreach ($bag_kind_options ?? [] as $cd): ?>
|
|
||||||
<option value="<?= esc($cd->cd_code) ?>" <?= (string) ($cd->cd_code ?? '') === (string) ($bag_kind_e ?? '') ? 'selected' : '' ?>>
|
|
||||||
<?= esc($cd->cd_name) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-0.5">
|
|
||||||
<label class="text-xs text-gray-500">봉투코드</label>
|
|
||||||
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1.5 text-sm min-w-[11rem]">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<?php foreach ($bag_codes ?? [] as $cd): ?>
|
|
||||||
<option value="<?= esc($cd->cd_code) ?>" <?= (string) ($cd->cd_code ?? '') === (string) ($bag_code ?? '') ? 'selected' : '' ?>>
|
|
||||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-0.5">
|
|
||||||
<label class="text-xs text-gray-500">조회 기간 (적용기간 겹침)</label>
|
|
||||||
<?php
|
|
||||||
$sp = $startParts ?? ['y' => '', 'm' => '', 'd' => ''];
|
|
||||||
$ep = $endParts ?? ['y' => '', 'm' => '', 'd' => ''];
|
|
||||||
$ymin = (int) ($dateYearMin ?? ((int) date('Y') - 12));
|
|
||||||
$ymax = (int) ($dateYearMax ?? ((int) date('Y') + 2));
|
|
||||||
?>
|
|
||||||
<div class="flex flex-wrap items-center gap-1">
|
|
||||||
<span class="text-xs text-gray-500 mr-0.5">시작</span>
|
|
||||||
<select name="start_y" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[4.5rem]">
|
|
||||||
<option value="">연도</option>
|
|
||||||
<?php for ($yy = $ymin; $yy <= $ymax; $yy++): ?>
|
|
||||||
<option value="<?= $yy ?>" <?= (string) ($sp['y'] ?? '') === (string) $yy ? 'selected' : '' ?>><?= $yy ?></option>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</select>
|
|
||||||
<select name="start_m" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
|
|
||||||
<option value="">월</option>
|
|
||||||
<?php for ($mi = 1; $mi <= 12; $mi++): ?>
|
|
||||||
<option value="<?= $mi ?>" <?= isset($sp['m']) && (int) $sp['m'] === $mi ? 'selected' : '' ?>><?= $mi ?>월</option>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</select>
|
|
||||||
<select name="start_d" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
|
|
||||||
<option value="">일</option>
|
|
||||||
<?php for ($di = 1; $di <= 31; $di++): ?>
|
|
||||||
<option value="<?= $di ?>" <?= isset($sp['d']) && (int) $sp['d'] === $di ? 'selected' : '' ?>><?= $di ?>일</option>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</select>
|
|
||||||
<span class="text-sm text-gray-500 mx-0.5">~</span>
|
|
||||||
<span class="text-xs text-gray-500 mr-0.5">종료</span>
|
|
||||||
<select name="end_y" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[4.5rem]">
|
|
||||||
<option value="">연도</option>
|
|
||||||
<?php for ($yy = $ymin; $yy <= $ymax; $yy++): ?>
|
|
||||||
<option value="<?= $yy ?>" <?= (string) ($ep['y'] ?? '') === (string) $yy ? 'selected' : '' ?>><?= $yy ?></option>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</select>
|
|
||||||
<select name="end_m" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
|
|
||||||
<option value="">월</option>
|
|
||||||
<?php for ($mi = 1; $mi <= 12; $mi++): ?>
|
|
||||||
<option value="<?= $mi ?>" <?= isset($ep['m']) && (int) $ep['m'] === $mi ? 'selected' : '' ?>><?= $mi ?>월</option>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</select>
|
|
||||||
<select name="end_d" class="border border-gray-300 rounded px-1.5 py-1.5 text-sm min-w-[3.75rem]">
|
|
||||||
<option value="">일</option>
|
|
||||||
<?php for ($di = 1; $di <= 31; $di++): ?>
|
|
||||||
<option value="<?= $di ?>" <?= isset($ep['d']) && (int) $ep['d'] === $di ? 'selected' : '' ?>><?= $di ?>일</option>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 pb-0.5">
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
|
||||||
<a href="<?= mgmt_url('bag-prices') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
|
||||||
<button type="button" onclick="window.print()" class="border border-gray-300 text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50">인쇄</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -111,17 +32,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php
|
<?php foreach ($list as $row): ?>
|
||||||
$startNo = 1;
|
|
||||||
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
|
|
||||||
$currentPage = (int) $pager->getCurrentPage();
|
|
||||||
$perPage = (int) $pager->getPerPage();
|
|
||||||
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<?php foreach (($list ?? []) as $idx => $row): ?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
|
<td class="text-center"><?= esc($row->bp_idx) ?></td>
|
||||||
<td class="text-center font-mono"><?= esc($row->bp_bag_code) ?></td>
|
<td class="text-center font-mono"><?= esc($row->bp_bag_code) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->bp_bag_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->bp_bag_name) ?></td>
|
||||||
<td><?= number_format((float) $row->bp_order_price) ?></td>
|
<td><?= number_format((float) $row->bp_order_price) ?></td>
|
||||||
|
|||||||
@@ -8,19 +8,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
|
||||||
<form method="GET" action="<?= mgmt_url('companies') ?>" class="flex flex-wrap items-center gap-2">
|
|
||||||
<label class="text-sm text-gray-600">업체유형</label>
|
|
||||||
<select name="cp_type" class="border border-gray-300 rounded px-2 py-1 text-sm w-44">
|
|
||||||
<option value="">전 체</option>
|
|
||||||
<?php foreach (($typeOptions ?? []) as $type): ?>
|
|
||||||
<option value="<?= esc($type) ?>" <?= (string) ($cpType ?? '') === (string) $type ? 'selected' : '' ?>><?= esc($type) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
|
||||||
<a href="<?= mgmt_url('companies') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -37,17 +24,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php
|
<?php foreach ($list as $row): ?>
|
||||||
$startNo = 1;
|
|
||||||
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
|
|
||||||
$currentPage = (int) $pager->getCurrentPage();
|
|
||||||
$perPage = (int) $pager->getPerPage();
|
|
||||||
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<?php foreach (($list ?? []) as $idx => $row): ?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
|
<td class="text-center"><?= esc($row->cp_idx) ?></td>
|
||||||
<td class="text-center"><?= esc($row->cp_type) ?></td>
|
<td class="text-center"><?= esc($row->cp_type) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->cp_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->cp_name) ?></td>
|
||||||
<td class="text-center"><?= esc($row->cp_biz_no) ?></td>
|
<td class="text-center"><?= esc($row->cp_biz_no) ?></td>
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
<?= view('components/print_header', ['printTitle' => '지정판매소 바코드 출력']) ?>
|
|
||||||
<style>
|
|
||||||
.ds-bc-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
||||||
.ds-bc-table th, .ds-bc-table td { border: 1px solid #ccc; padding: 4px 6px; }
|
|
||||||
.ds-bc-table th { background: #e9ecef; color: #2d3748; }
|
|
||||||
.ds-bc-table td { background: #fff; }
|
|
||||||
.ds-bc-table td.name-cell { max-width: 14rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
.ds-bc-table td.addr-cell { max-width: 24rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
.ds-bc-check { width: 14px; height: 14px; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
|
||||||
<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 items-center gap-2">
|
|
||||||
<button type="button" id="ds-bc-print-btn" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">인쇄</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
|
||||||
<form id="ds-bc-filter-form" method="get" action="<?= mgmt_url('designated-shops/barcode') ?>" class="flex flex-wrap items-end gap-3">
|
|
||||||
<div class="min-w-[12rem]">
|
|
||||||
<label class="block text-xs text-gray-600 mb-0.5">군·구</label>
|
|
||||||
<div class="border border-gray-300 rounded px-3 py-1 text-sm bg-gray-50 text-gray-800 font-medium pointer-events-none select-none">
|
|
||||||
<?= esc($fixedGugunLabel ?? '현재 지자체') ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs text-gray-600 mb-0.5">읍·면·동</label>
|
|
||||||
<select name="ds_zone_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem]">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<?php foreach (($zones ?? []) as $z): ?>
|
|
||||||
<?php $zc = trim((string) ($z->zone_code ?? '')); ?>
|
|
||||||
<option value="<?= esc($zc) ?>" <?= ($zoneFilter ?? '') === $zc ? 'selected' : '' ?>><?= esc($zc) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs text-gray-600 mb-0.5">조회순서</label>
|
|
||||||
<select name="order_by" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[8rem]">
|
|
||||||
<option value="shop_no" <?= ($orderBy ?? 'shop_no') === 'shop_no' ? 'selected' : '' ?>>판매소 코드</option>
|
|
||||||
<option value="name" <?= ($orderBy ?? '') === 'name' ? 'selected' : '' ?>>판매소명</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mx-2 mt-2 mb-2">
|
|
||||||
<form id="ds-bc-print-form" method="post" action="<?= mgmt_url('designated-shops/barcode/print') ?>" target="ds-bc-print-frame">
|
|
||||||
<?= csrf_field() ?>
|
|
||||||
<input type="hidden" name="zone_label" value="<?= esc(($zoneFilter ?? '') !== '' ? (string) $zoneFilter : '전체') ?>">
|
|
||||||
<div class="mb-1 text-xs text-gray-600">
|
|
||||||
<label class="inline-flex items-center gap-1 cursor-pointer"><input type="checkbox" id="ds-bc-check-all" class="ds-bc-check"> 전체선택</label>
|
|
||||||
<span class="ml-3">선택 건수: <strong id="ds-bc-selected-count">0</strong></span>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-auto border border-gray-300 bg-white">
|
|
||||||
<table class="ds-bc-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="w-14">출력</th>
|
|
||||||
<th class="w-36">판매소 코드</th>
|
|
||||||
<th>판매소명</th>
|
|
||||||
<th class="w-24">대표자명</th>
|
|
||||||
<th class="w-32">사업자번호</th>
|
|
||||||
<th>사업장 주소</th>
|
|
||||||
<th class="w-16">상태</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach (($list ?? []) as $row): ?>
|
|
||||||
<?php
|
|
||||||
$st = (int) ($row->ds_state ?? 1);
|
|
||||||
$stLabel = $st === 1 ? '사용' : '정지';
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td class="text-center"><input class="ds-bc-row-check ds-bc-check" type="checkbox" name="ds_idx[]" value="<?= (int) $row->ds_idx ?>"></td>
|
|
||||||
<td class="text-center text-blue-700"><?= esc((string) ($row->ds_shop_no ?? '')) ?></td>
|
|
||||||
<td class="name-cell text-blue-700" title="<?= esc((string) ($row->ds_name ?? '')) ?>"><?= esc((string) ($row->ds_name ?? '')) ?></td>
|
|
||||||
<td><?= esc((string) ($row->ds_rep_name ?? '')) ?></td>
|
|
||||||
<td><?= esc((string) ($row->ds_biz_no ?? '')) ?></td>
|
|
||||||
<td class="addr-cell" title="<?= esc((string) ($row->ds_addr ?? '')) ?>"><?= esc((string) ($row->ds_addr ?? '')) ?></td>
|
|
||||||
<td class="<?= $st === 1 ? 'text-blue-700' : 'text-red-600' ?>"><?= esc($stLabel) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($list)): ?>
|
|
||||||
<tr><td colspan="7" class="text-center text-gray-400 py-8">조회된 지정판매소가 없습니다.</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<iframe name="ds-bc-print-frame" class="hidden" style="display:none;width:0;height:0;border:0;" aria-hidden="true"></iframe>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<?php if (isset($pager)): ?>
|
|
||||||
<div class="mt-2 mb-2 mx-2 no-print"><?= $pager->links() ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var all = document.getElementById('ds-bc-check-all');
|
|
||||||
var countEl = document.getElementById('ds-bc-selected-count');
|
|
||||||
var printBtn = document.getElementById('ds-bc-print-btn');
|
|
||||||
var printForm = document.getElementById('ds-bc-print-form');
|
|
||||||
var rows = Array.prototype.slice.call(document.querySelectorAll('.ds-bc-row-check'));
|
|
||||||
if (!all || !countEl || !rows.length) return;
|
|
||||||
|
|
||||||
function refreshCount() {
|
|
||||||
var n = rows.filter(function (el) { return el.checked; }).length;
|
|
||||||
countEl.textContent = String(n);
|
|
||||||
all.checked = n > 0 && n === rows.length;
|
|
||||||
all.indeterminate = n > 0 && n < rows.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
all.addEventListener('change', function () {
|
|
||||||
rows.forEach(function (el) { el.checked = all.checked; });
|
|
||||||
refreshCount();
|
|
||||||
});
|
|
||||||
rows.forEach(function (el) { el.addEventListener('change', refreshCount); });
|
|
||||||
|
|
||||||
if (printBtn && printForm) {
|
|
||||||
printBtn.addEventListener('click', function () {
|
|
||||||
var selected = rows.filter(function (el) { return el.checked; }).length;
|
|
||||||
if (selected < 1) {
|
|
||||||
window.alert('출력할 지정판매소를 1개 이상 선택해 주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
printForm.action = "<?= esc(mgmt_url('designated-shops/barcode/print')) ?>?autoprint=1";
|
|
||||||
printForm.submit();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshCount();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<?php
|
|
||||||
$rows = $rows ?? [];
|
|
||||||
$zoneLabel = trim((string) ($zoneLabel ?? '전체'));
|
|
||||||
$printedAt = trim((string) ($printedAt ?? date('Y.m.d')));
|
|
||||||
$chunks = array_chunk($rows, 12);
|
|
||||||
$totalPages = count($chunks);
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>지정판매소 바코드</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; font-family: Arial, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; color: #222; background: #fff; }
|
|
||||||
.page { width: 210mm; min-height: 297mm; margin: 0 auto; padding: 14mm 12mm 12mm; box-sizing: border-box; }
|
|
||||||
.title { text-align: center; font-size: 42px; letter-spacing: 1px; font-weight: 500; margin: 0 0 14px; }
|
|
||||||
.meta { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #555; padding-bottom: 4px; font-size: 13px; margin-bottom: 8px; }
|
|
||||||
.meta .center { font-weight: 700; }
|
|
||||||
.cards { display: flex; flex-wrap: wrap; align-content: flex-start; }
|
|
||||||
.card { width: 33.3333%; padding: 0 8px 12px; box-sizing: border-box; }
|
|
||||||
.barcode-wrap { min-height: 40px; }
|
|
||||||
.barcode-svg { width: 100%; max-width: 270px; height: 22px; }
|
|
||||||
.code-text { text-align: center; margin-top: 1px; font-size: 16px; letter-spacing: 0.35px; }
|
|
||||||
.name-text { text-align: center; margin-top: 5px; font-size: 14px; line-height: 1.2; word-break: keep-all; }
|
|
||||||
@media print {
|
|
||||||
@page { size: A4; margin: 0; }
|
|
||||||
.page { page-break-after: always; }
|
|
||||||
.page:last-child { page-break-after: auto; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<?= view('components/print_header', [
|
|
||||||
'printTitle' => '지정판매소 바코드',
|
|
||||||
'printExtraLines' => [
|
|
||||||
'구역: ' . $zoneLabel,
|
|
||||||
'출력일: ' . $printedAt,
|
|
||||||
],
|
|
||||||
]) ?>
|
|
||||||
<?php if ($rows === []): ?>
|
|
||||||
<div class="page">
|
|
||||||
<h1 class="title">지정판매소 바코드</h1>
|
|
||||||
<p style="text-align:center; margin-top:30px; color:#666;">출력할 지정판매소가 없습니다.</p>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($chunks as $pageIndex => $pageRows): ?>
|
|
||||||
<section class="page">
|
|
||||||
<h1 class="title">지정판매소 바코드</h1>
|
|
||||||
<div class="meta">
|
|
||||||
<span>출 력 일 자: <?= esc($printedAt) ?></span>
|
|
||||||
<span class="center"><?= esc($zoneLabel) ?></span>
|
|
||||||
<span>페 이 지: <?= (int) ($pageIndex + 1) ?> / <?= (int) $totalPages ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="cards">
|
|
||||||
<?php foreach ($pageRows as $row): ?>
|
|
||||||
<?php
|
|
||||||
$code = trim((string) ($row->ds_shop_no ?? ''));
|
|
||||||
$nm = trim((string) ($row->ds_name ?? ''));
|
|
||||||
$rep = trim((string) ($row->ds_rep_name ?? ''));
|
|
||||||
$label = trim($nm . ($rep !== '' ? ('-' . $rep) : ''));
|
|
||||||
?>
|
|
||||||
<div class="card">
|
|
||||||
<div class="barcode-wrap">
|
|
||||||
<svg class="barcode-svg" data-barcode="<?= esc($code, 'attr') ?>"></svg>
|
|
||||||
</div>
|
|
||||||
<div class="code-text"><?= esc($code) ?></div>
|
|
||||||
<div class="name-text"><?= esc($label) ?></div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
var svgs = document.querySelectorAll('svg[data-barcode]');
|
|
||||||
svgs.forEach(function (svg) {
|
|
||||||
var code = (svg.getAttribute('data-barcode') || '').trim();
|
|
||||||
if (!code) return;
|
|
||||||
try {
|
|
||||||
JsBarcode(svg, code, {
|
|
||||||
format: 'CODE128',
|
|
||||||
displayValue: false,
|
|
||||||
margin: 0,
|
|
||||||
height: 16,
|
|
||||||
width: 1.28
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
svg.outerHTML = '<div style="font-size:12px;color:#b91c1c;">바코드 생성 실패: ' + code + '</div>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (window.location.search.indexOf('autoprint=1') >= 0) {
|
|
||||||
setTimeout(function () { window.print(); }, 200);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<span class="text-sm font-bold text-gray-700">지정판매소 등록</span>
|
<span class="text-sm font-bold text-gray-700">지정판매소 등록</span>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
<form id="designated-shop-create-form" action="<?= mgmt_url('designated-shops/store') ?>" method="POST" class="space-y-4">
|
<form action="<?= mgmt_url('designated-shops/store') ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<?php if (! empty($localGovs)): ?>
|
<?php if (! empty($localGovs)): ?>
|
||||||
@@ -23,18 +23,14 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
|
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
|
||||||
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
|
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
|
||||||
<span class="text-gray-500 ml-1"><?= esc(trim((string) ($currentLg->lg_sido ?? '') . ' ' . (string) ($currentLg->lg_gugun ?? ''))) ?></span>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="ds_lg_idx" value="<?= esc($currentLg->lg_idx) ?>"/>
|
<input type="hidden" name="ds_lg_idx" value="<?= esc($currentLg->lg_idx) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<input type="hidden" name="addr_search_sido" id="addr_search_sido" value="<?= esc((string) old('addr_search_sido', '')) ?>"/>
|
|
||||||
<input type="hidden" name="addr_search_sigungu" id="addr_search_sigungu" value="<?= esc((string) old('addr_search_sigungu', '')) ?>"/>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">판매소번호</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">판매소번호</label>
|
||||||
<div class="text-sm text-gray-600">등록 시 자동 부여 (주소 기준 기본코드 B·C·D 조합 + 일련번호 3자리)</div>
|
<div class="text-sm text-gray-600">등록 시 자동 부여 (지자체코드 + 일련번호 3자리)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -53,50 +49,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">업태</label>
|
<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-48" name="ds_biz_type" type="text" value="<?= esc(old('ds_biz_type')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_number" type="text" value="<?= esc(old('ds_va_number')) ?>"/>
|
||||||
</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-48" name="ds_biz_kind" type="text" value="<?= esc(old('ds_biz_kind')) ?>"/>
|
|
||||||
</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-48" name="ds_va_bank" type="text" value="<?= esc(old('ds_va_bank')) ?>" placeholder="예: 농협"/>
|
|
||||||
</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-60" name="ds_va_account" type="text" value="<?= esc(old('ds_va_account')) ?>"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-start gap-2">
|
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">주소</label>
|
|
||||||
<p class="text-xs text-gray-600 max-w-lg leading-relaxed">우편번호 옆 <strong>주소 검색</strong>으로만 지정합니다(직접 입력 불가). <strong>지도</strong>는 카카오맵에서 현재 입력된 주소를 검색해 엽니다. 관할이 아니면 검색 직후 안내 후 반영되지 않습니다. 동·호 등은 아래 <strong>상세주소</strong>에 입력하세요.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">우편번호</label>
|
<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-32 bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_zip" id="ds_zip" type="text" value="<?= esc(old('ds_zip')) ?>" maxlength="10" autocomplete="postal-code" readonly tabindex="-1"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="ds_zip" type="text" value="<?= esc(old('ds_zip')) ?>"/>
|
||||||
<button type="button" id="btn-ds-kakao-postcode" class="no-print border border-btn-print-border text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition shrink-0">주소 검색</button>
|
|
||||||
<?= view('components/kakao_map_link_button', ['buttonId' => 'btn-ds-kakao-map-create']) ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label>
|
<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-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr" id="ds_addr" type="text" value="<?= esc(old('ds_addr')) ?>" readonly tabindex="-1"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr" type="text" value="<?= esc(old('ds_addr')) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지번주소</label>
|
<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-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr_jibun" id="ds_addr_jibun" type="text" value="<?= esc(old('ds_addr_jibun')) ?>" readonly tabindex="-1"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr_jibun" type="text" value="<?= esc(old('ds_addr_jibun')) ?>"/>
|
||||||
</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-96 max-w-full" name="ds_addr_detail" id="ds_addr_detail" type="text" value="<?= esc(old('ds_addr_detail')) ?>" maxlength="200" placeholder="동·호수·층 등"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -119,31 +88,11 @@
|
|||||||
<div class="text-sm text-gray-600">해당 지자체(구·군) 코드로 등록 시 자동 설정</div>
|
<div class="text-sm text-gray-600">해당 지자체(구·군) 코드로 등록 시 자동 설정</div>
|
||||||
</div>
|
</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-72" name="ds_zone_code" type="text" value="<?= esc(old('ds_zone_code')) ?>"/>
|
|
||||||
</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-48" name="ds_branch_no" type="text" value="<?= esc(old('ds_branch_no')) ?>"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지정일자</label>
|
<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-40" name="ds_designated_at" type="date" value="<?= esc(old('ds_designated_at')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc(old('ds_designated_at')) ?>"/>
|
||||||
</div>
|
</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-40" name="ds_state_changed_at" type="date" value="<?= esc(old('ds_state_changed_at')) ?>"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-start gap-2">
|
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">변경사유</label>
|
|
||||||
<textarea class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 min-h-[4rem]" name="ds_change_reason" rows="3"><?= esc(old('ds_change_reason')) ?></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-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>
|
<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="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
|
<a href="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
|
||||||
@@ -151,17 +100,3 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?= view('components/kakao_map_modal', ['kakaoJavascriptKey' => $kakaoJavascriptKey ?? '']) ?>
|
|
||||||
|
|
||||||
<?= view('components/kakao_address_search', [
|
|
||||||
'buttonId' => 'btn-ds-kakao-postcode',
|
|
||||||
'zipName' => 'ds_zip',
|
|
||||||
'roadName' => 'ds_addr',
|
|
||||||
'jibunName' => 'ds_addr_jibun',
|
|
||||||
'sidoFieldName' => 'addr_search_sido',
|
|
||||||
'sigunguFieldName' => 'addr_search_sigungu',
|
|
||||||
'tenantScope' => $addrTenantScope ?? ['lg_sido' => '', 'lg_gugun' => ''],
|
|
||||||
'roadBaseOnly' => true,
|
|
||||||
'detailFieldName' => 'ds_addr_detail',
|
|
||||||
]) ?>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
<?php
|
|
||||||
$ry = (int) ($reportYear ?? (int) date('Y'));
|
|
||||||
$lg = $currentLg ?? null;
|
|
||||||
$lgSido = $lg !== null ? trim((string) ($lg->lg_sido ?? '')) : '';
|
|
||||||
$lgGugun = $lg !== null ? trim((string) ($lg->lg_gugun ?? '')) : '';
|
|
||||||
$lgName = $lg !== null ? trim((string) ($lg->lg_name ?? '')) : '';
|
|
||||||
$scopeLabel = $lgSido !== '' && $lgGugun !== ''
|
|
||||||
? $lgSido . ' ' . $lgGugun
|
|
||||||
: ($lgName !== '' ? $lgName : '—');
|
|
||||||
$exportUrl = mgmt_url('designated-shops/district-new-cancel/export') . '?' . http_build_query(['year' => $ry]);
|
|
||||||
?>
|
|
||||||
<?= view('components/print_header', ['printTitle' => '지정 판매소 신규/취소 현황 (' . $ry . '년)']) ?>
|
|
||||||
<style>
|
|
||||||
.gbms-dnc-wrap { max-width: 100%; }
|
|
||||||
.gbms-dnc-table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
||||||
.gbms-dnc-table th,
|
|
||||||
.gbms-dnc-table td {
|
|
||||||
border: 1px solid #7a8aa0;
|
|
||||||
padding: 6px 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.gbms-dnc-table thead th {
|
|
||||||
background: linear-gradient(180deg, #e8eef6 0%, #d4dee9 100%);
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1a2a3a;
|
|
||||||
}
|
|
||||||
.gbms-dnc-table thead th.gbms-sub {
|
|
||||||
background: #dce6f0;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.gbms-dnc-table tbody td.text-left { text-align: left; }
|
|
||||||
.gbms-dnc-table tbody tr.gbms-total td {
|
|
||||||
font-weight: 700;
|
|
||||||
border: 2px solid #c62828;
|
|
||||||
background: #fff8f8;
|
|
||||||
}
|
|
||||||
.gbms-dnc-caption {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 8px 0 6px;
|
|
||||||
color: #1a2a3a;
|
|
||||||
}
|
|
||||||
.gbms-unit-pill {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 8px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0d47a1;
|
|
||||||
background: #e3f2fd;
|
|
||||||
border: 1px solid #90caf9;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.gbms-tip {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.gbms-help {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
padding: 0 2px;
|
|
||||||
border: 1px solid #5c6f85;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #fef9c3;
|
|
||||||
color: #111827;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
user-select: none;
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
.gbms-help::after {
|
|
||||||
content: attr(data-tip);
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
display: none;
|
|
||||||
min-width: 12rem;
|
|
||||||
max-width: 14rem;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border: 1px solid #8fa0b3;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #1f2937;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.35;
|
|
||||||
text-align: left;
|
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
|
||||||
z-index: 30;
|
|
||||||
}
|
|
||||||
.gbms-help:hover::after,
|
|
||||||
.gbms-help:focus::after {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
@media print {
|
|
||||||
.gbms-dnc-table { font-size: 11px; }
|
|
||||||
.gbms-help { display: none !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<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-800">[지정 판매소 신규/취소 현황]</span>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<a href="<?= esc($exportUrl) ?>" class="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="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
|
||||||
<a href="<?= mgmt_url('designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="p-3 bg-white border-b border-gray-200 no-print">
|
|
||||||
<form method="get" action="<?= mgmt_url('designated-shops/district-new-cancel') ?>" class="flex flex-wrap items-end gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs text-gray-600 mb-0.5">조회년도</label>
|
|
||||||
<select name="year" class="border border-gray-400 rounded px-2 py-1.5 text-sm min-w-[7rem] bg-white">
|
|
||||||
<?php foreach (($yearChoices ?? []) as $y): ?>
|
|
||||||
<option value="<?= (int) $y ?>" <?= $ry === (int) $y ? 'selected' : '' ?>><?= (int) $y ?>년</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 min-w-[12rem]">
|
|
||||||
<span class="block text-xs text-gray-600 mb-0.5">군·구 (소속 지자체)</span>
|
|
||||||
<div class="border border-gray-300 rounded px-3 py-1.5 text-sm bg-gray-50 text-gray-800 font-medium">
|
|
||||||
<?= esc($scopeLabel) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="gbms-unit-pill self-end mb-0.5">단위: 판매소</span>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-5 py-1.5 rounded-sm text-sm font-medium shadow-sm hover:opacity-90">조회</button>
|
|
||||||
</form>
|
|
||||||
<p class="text-xs text-gray-500 mt-2">
|
|
||||||
종전·현행은 각각 <?= $ry - 1 ?>-12-31·<?= $ry ?>-12-31 기준 정상(지정일 이전·비정상 전환일 이후) 건수이며, 지정·취소는 <?= $ry ?>년 달력연도 기준입니다. 구·군 행은 효과 지자체의 기본코드(구 코드) 순서로 표시됩니다.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="mx-2 mt-3 mb-4 gbms-dnc-wrap">
|
|
||||||
<div class="gbms-dnc-caption">지정 판매소 신규/취소 현황 조회 내역</div>
|
|
||||||
<div class="overflow-x-auto border border-gray-400 bg-white">
|
|
||||||
<table class="gbms-dnc-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th rowspan="2" class="min-w-[6rem]">군·구</th>
|
|
||||||
<th rowspan="2">
|
|
||||||
<span class="gbms-tip">
|
|
||||||
종전
|
|
||||||
<span class="gbms-help" tabindex="0" aria-label="종전 설명" data-tip="전년도 12월 31일 기준 정상 상태 판매소 수">?</span>
|
|
||||||
</span>
|
|
||||||
<br><span class="text-xs font-normal">(전년도말)</span>
|
|
||||||
</th>
|
|
||||||
<th colspan="2">사용</th>
|
|
||||||
<th rowspan="2">
|
|
||||||
<span class="gbms-tip">
|
|
||||||
현행
|
|
||||||
<span class="gbms-help" tabindex="0" aria-label="현행 설명" data-tip="조회년도 12월 31일 기준 정상 상태 판매소 수">?</span>
|
|
||||||
</span>
|
|
||||||
<br><span class="text-xs font-normal">(금년도말)</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="gbms-sub">
|
|
||||||
<span class="gbms-tip">
|
|
||||||
지정
|
|
||||||
<span class="gbms-help" tabindex="0" aria-label="지정 설명" data-tip="조회년도 내 지정일이 속한 신규 지정 건수">?</span>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
<th class="gbms-sub">
|
|
||||||
<span class="gbms-tip">
|
|
||||||
취소
|
|
||||||
<span class="gbms-help" tabindex="0" aria-label="취소 설명" data-tip="조회년도 내 폐업/해지 전환일이 속한 건수">?</span>
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach (($districtRows ?? []) as $row): ?>
|
|
||||||
<tr>
|
|
||||||
<td class="text-left font-medium"><?= esc($row->region_label) ?></td>
|
|
||||||
<td><?= number_format((int) $row->prev_end) ?></td>
|
|
||||||
<td><?= number_format((int) $row->designated_y) ?></td>
|
|
||||||
<td><?= number_format((int) $row->cancelled_y) ?></td>
|
|
||||||
<td><?= number_format((int) $row->curr_end) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($districtRows)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" class="text-center text-gray-500 py-8">표시할 구·군 또는 지정판매소 데이터가 없습니다.</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (! empty($districtRows) && isset($districtTotal)): ?>
|
|
||||||
<tr class="gbms-total">
|
|
||||||
<td class="text-left"><?= esc($districtTotal->region_label) ?></td>
|
|
||||||
<td><?= number_format((int) $districtTotal->prev_end) ?></td>
|
|
||||||
<td><?= number_format((int) $districtTotal->designated_y) ?></td>
|
|
||||||
<td><?= number_format((int) $districtTotal->cancelled_y) ?></td>
|
|
||||||
<td><?= number_format((int) $districtTotal->curr_end) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -5,35 +5,20 @@ if ($shop === null) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($key) : ($shop->{$key} ?? $default);
|
$v = fn ($key, $default = '') => old($key) !== null && old($key) !== '' ? old($key) : ($shop->{$key} ?? $default);
|
||||||
$vaAccountDefault = (isset($shop->ds_va_account) && (string) $shop->ds_va_account !== '')
|
|
||||||
? (string) $shop->ds_va_account
|
|
||||||
: (string) ($shop->ds_va_number ?? '');
|
|
||||||
$dateField = static function (string $key) use ($shop, $v): string {
|
|
||||||
$s = (string) $v($key);
|
|
||||||
if ($s === '' || str_starts_with($s, '0000')) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $s;
|
|
||||||
};
|
|
||||||
?>
|
?>
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
<span class="text-sm font-bold text-gray-700">지정판매소 수정</span>
|
<span class="text-sm font-bold text-gray-700">지정판매소 수정</span>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
<form id="designated-shop-edit-form" action="<?= mgmt_url('designated-shops/update/' . (int) $shop->ds_idx) ?>" method="POST" class="space-y-4">
|
<form action="<?= mgmt_url('designated-shops/update/' . (int) $shop->ds_idx) ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
<input type="hidden" name="addr_search_sido" id="addr_search_sido" value="<?= esc((string) old('addr_search_sido', $currentLg !== null ? (string) ($currentLg->lg_sido ?? '') : '')) ?>"/>
|
|
||||||
<input type="hidden" name="addr_search_sigungu" id="addr_search_sigungu" value="<?= esc((string) old('addr_search_sigungu', $currentLg !== null ? (string) ($currentLg->lg_gugun ?? '') : '')) ?>"/>
|
|
||||||
|
|
||||||
<?php if ($currentLg !== null): ?>
|
<?php if ($currentLg !== null): ?>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지자체</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">지자체</label>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
|
<span class="font-semibold"><?= esc($currentLg->lg_name) ?></span>
|
||||||
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
|
<span class="text-gray-500 ml-2">(<?= esc($currentLg->lg_code) ?>)</span>
|
||||||
<span class="text-gray-500 ml-1"><?= esc(trim((string) ($currentLg->lg_sido ?? '') . ' ' . (string) ($currentLg->lg_gugun ?? ''))) ?></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -64,50 +49,23 @@ $dateField = static function (string $key) use ($shop, $v): string {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">업태</label>
|
<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-48" name="ds_biz_type" type="text" value="<?= esc($v('ds_biz_type')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_va_number" type="text" value="<?= esc($v('ds_va_number')) ?>"/>
|
||||||
</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-48" name="ds_biz_kind" type="text" value="<?= esc($v('ds_biz_kind')) ?>"/>
|
|
||||||
</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-48" name="ds_va_bank" type="text" value="<?= esc($v('ds_va_bank')) ?>"/>
|
|
||||||
</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-60" name="ds_va_account" type="text" value="<?= esc((old('ds_va_account') !== null && old('ds_va_account') !== '') ? old('ds_va_account') : $vaAccountDefault) ?>"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-start gap-2">
|
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">주소</label>
|
|
||||||
<p class="text-xs text-gray-600 max-w-lg leading-relaxed">우편·도로명·지번은 <strong>주소 검색</strong>으로만 바꿀 수 있습니다(직접 입력 불가). <strong>지도</strong>는 카카오맵에서 현재 입력된 주소를 검색해 엽니다. 관할이 아니면 검색 직후 안내 후 반영되지 않습니다. 동·호 등은 <strong>상세주소</strong>에 입력하세요.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">우편번호</label>
|
<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-32 bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_zip" id="ds_zip" type="text" value="<?= esc($v('ds_zip')) ?>" maxlength="10" readonly tabindex="-1"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-32" name="ds_zip" type="text" value="<?= esc($v('ds_zip')) ?>"/>
|
||||||
<button type="button" id="btn-ds-kakao-postcode-edit" class="no-print border border-btn-print-border text-gray-700 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition shrink-0">주소 검색</button>
|
|
||||||
<?= view('components/kakao_map_link_button', ['buttonId' => 'btn-ds-kakao-map-edit']) ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">도로명주소</label>
|
<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-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr" id="ds_addr" type="text" value="<?= esc($v('ds_addr')) ?>" readonly tabindex="-1"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr" type="text" value="<?= esc($v('ds_addr')) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지번주소</label>
|
<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-96 max-w-full bg-gray-100 text-gray-800 cursor-not-allowed" name="ds_addr_jibun" id="ds_addr_jibun" type="text" value="<?= esc($v('ds_addr_jibun')) ?>" readonly tabindex="-1"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96" name="ds_addr_jibun" type="text" value="<?= esc($v('ds_addr_jibun')) ?>"/>
|
||||||
</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-96 max-w-full" name="ds_addr_detail" id="ds_addr_detail" type="text" value="<?= esc($v('ds_addr_detail')) ?>" maxlength="200" placeholder="동·호수·층 등"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -125,16 +83,6 @@ $dateField = static function (string $key) use ($shop, $v): string {
|
|||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_email" type="email" value="<?= esc($v('ds_email')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="ds_email" type="email" value="<?= esc($v('ds_email')) ?>"/>
|
||||||
</div>
|
</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-72" name="ds_zone_code" type="text" value="<?= esc($v('ds_zone_code')) ?>"/>
|
|
||||||
</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-48" name="ds_branch_no" type="text" value="<?= esc($v('ds_branch_no')) ?>"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">지정일자</label>
|
<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-40" name="ds_designated_at" type="date" value="<?= esc($v('ds_designated_at')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-40" name="ds_designated_at" type="date" value="<?= esc($v('ds_designated_at')) ?>"/>
|
||||||
@@ -149,33 +97,9 @@ $dateField = static function (string $key) use ($shop, $v): string {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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-40" name="ds_state_changed_at" type="date" value="<?= esc($dateField('ds_state_changed_at')) ?>"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-start gap-2">
|
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28 pt-1.5">변경사유</label>
|
|
||||||
<textarea class="border border-gray-300 rounded px-3 py-1.5 text-sm w-96 min-h-[4rem]" name="ds_change_reason" rows="3"><?= esc($v('ds_change_reason')) ?></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-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>
|
<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="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
|
<a href="<?= mgmt_url('designated-shops') ?>" class="bg-white text-black border border-btn-print-border px-4 py-1.5 rounded-sm text-sm shadow hover:bg-gray-50 transition">목록</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?= view('components/kakao_map_modal', ['kakaoJavascriptKey' => $kakaoJavascriptKey ?? '']) ?>
|
|
||||||
|
|
||||||
<?= view('components/kakao_address_search', [
|
|
||||||
'buttonId' => 'btn-ds-kakao-postcode-edit',
|
|
||||||
'zipName' => 'ds_zip',
|
|
||||||
'roadName' => 'ds_addr',
|
|
||||||
'jibunName' => 'ds_addr_jibun',
|
|
||||||
'sidoFieldName' => 'addr_search_sido',
|
|
||||||
'sigunguFieldName' => 'addr_search_sigungu',
|
|
||||||
'tenantScope' => $addrTenantScope ?? ['lg_sido' => '', 'lg_gugun' => ''],
|
|
||||||
'roadBaseOnly' => true,
|
|
||||||
'detailFieldName' => 'ds_addr_detail',
|
|
||||||
]) ?>
|
|
||||||
|
|||||||
@@ -1,202 +1,24 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '지정판매소 목록']) ?>
|
||||||
helper('admin');
|
|
||||||
$currentPath = current_nav_request_path();
|
|
||||||
if ($currentPath === 'bag/designated-shops') {
|
|
||||||
$readOnly = false;
|
|
||||||
} elseif ($currentPath === 'bag/designated-shops/browse') {
|
|
||||||
$readOnly = true;
|
|
||||||
} else {
|
|
||||||
$readOnly = ! empty($readOnly);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<?= view('components/print_header', ['printTitle' => $readOnly ? '지정판매소 조회 목록' : '지정판매소 목록']) ?>
|
|
||||||
<style>
|
|
||||||
/* 목록 위 → 지정판매소 정보 아래 (가로 2열 없음) */
|
|
||||||
.ds-split {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
.ds-list-panel {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 42vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.ds-detail-panel {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 12rem;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background: #f5f5f5;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.ds-panel-title {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: linear-gradient(180deg, #fafafa 0%, #e9ecef 100%);
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.ds-summary-bar {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: #fff3cd;
|
|
||||||
border: 1px solid #ffc107;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.ds-row-selected td { background-color: #cce5ff !important; }
|
|
||||||
.ds-detail-inner { padding: 10px; overflow: auto; flex: 1; }
|
|
||||||
/* 원본 지정판매소 정보: 라벨 고정폭 + 2열 값(우측 값이 더 넓음), 주소·개인전화는 전폭 */
|
|
||||||
.ds-detail-form {
|
|
||||||
font-size: 12px;
|
|
||||||
border: 1px solid #bbb;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.ds-row {
|
|
||||||
display: grid;
|
|
||||||
gap: 0;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
.ds-detail-form > .ds-row:last-child { border-bottom: none; }
|
|
||||||
/* 그 외 2+2 동일 비율 (상호명 | 우편번호 등) */
|
|
||||||
.ds-row-4-even {
|
|
||||||
grid-template-columns: 5.5rem minmax(0, 1fr) 5.5rem minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
/* 판매소번호 전폭 행 — 값을 우편·주소 필드처럼 넓게 */
|
|
||||||
.ds-value-shop-wide {
|
|
||||||
font-weight: 600;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
font-size: 13px;
|
|
||||||
padding-top: 8px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
/* 라벨 | 값(나머지 전체) — 도로명·지번·개인전화·이메일 */
|
|
||||||
.ds-row-wide {
|
|
||||||
grid-template-columns: 5.5rem minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
.ds-row-wide-tall .ds-field-value {
|
|
||||||
min-height: 3.25rem;
|
|
||||||
align-content: start;
|
|
||||||
}
|
|
||||||
/* 도로명주소 + 카카오맵 버튼 */
|
|
||||||
.ds-field-value-with-map {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.ds-field-value-with-map .ds-addr-text {
|
|
||||||
flex: 1 1 12rem;
|
|
||||||
min-width: 0;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
.ds-field-label {
|
|
||||||
background: #eef2f5;
|
|
||||||
border-right: 1px solid #ccc;
|
|
||||||
padding: 5px 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.ds-field-value {
|
|
||||||
padding: 5px 8px;
|
|
||||||
background: #fff;
|
|
||||||
word-break: break-word;
|
|
||||||
border-right: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
.ds-row-4-even > *:nth-child(4n) { border-right: none; }
|
|
||||||
.ds-row-wide > .ds-field-value { border-right: none; }
|
|
||||||
.ds-field-hint {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #b91c1c;
|
|
||||||
margin-top: 4px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
@media (max-width: 720px) {
|
|
||||||
.ds-row-4-even { grid-template-columns: 5rem 1fr; }
|
|
||||||
}
|
|
||||||
.ds-detail-actions { padding: 10px; border-top: 1px solid #ccc; background: #eee; }
|
|
||||||
.ds-detail-info-wrap { overflow-x: auto; }
|
|
||||||
.ds-detail-info-wrap .data-table th { white-space: nowrap; }
|
|
||||||
.ds-detail-info-wrap th.ds-col-tight-head,
|
|
||||||
.ds-detail-info-wrap td.ds-col-tight-cell {
|
|
||||||
max-width: 6.5rem;
|
|
||||||
width: 6.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.ds-list-panel .ds-col-tight {
|
|
||||||
max-width: 6rem;
|
|
||||||
width: 6rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.ds-list-panel .ds-col-zip {
|
|
||||||
width: 4.5rem;
|
|
||||||
max-width: 5rem;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.ds-list-panel .ds-col-addr-list {
|
|
||||||
max-width: 11rem;
|
|
||||||
min-width: 6rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.ds-list-panel .ds-col-detail-list {
|
|
||||||
max-width: 8rem;
|
|
||||||
min-width: 4rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.ds-ro-road-btn { margin-left: 6px; vertical-align: middle; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$listBasePath = $readOnly ? 'designated-shops/browse' : 'designated-shops';
|
|
||||||
?>
|
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700"><?= $readOnly ? '지정판매소 조회' : '지정판매소 관리' ?></span>
|
<span class="text-sm font-bold text-gray-700">지정판매소 목록</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<?php if ($readOnly): ?>
|
|
||||||
<a href="<?= mgmt_url('designated-shops/export') ?>" 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>
|
<a href="<?= mgmt_url('designated-shops/export') ?>" 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>
|
<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>
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (! $readOnly): ?>
|
|
||||||
<a href="<?= mgmt_url('designated-shops/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">지정판매소 등록</a>
|
<a href="<?= mgmt_url('designated-shops/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">지정판매소 등록</a>
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<!-- P2-15: 다조건 검색 -->
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||||
<form method="GET" action="<?= mgmt_url($listBasePath) ?>" class="flex flex-wrap items-center gap-2">
|
<form method="GET" action="<?= mgmt_url('designated-shops') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<span class="text-sm font-semibold text-gray-700 mr-1">지정판매소 검색</span>
|
|
||||||
<label class="text-sm text-gray-600">상호명</label>
|
<label class="text-sm text-gray-600">상호명</label>
|
||||||
<input type="text" name="ds_name" value="<?= esc($dsName ?? '') ?>" placeholder="상호명" class="border border-gray-300 rounded px-2 py-1 text-sm w-36"/>
|
<input type="text" name="ds_name" value="<?= esc($dsName ?? '') ?>" placeholder="상호명 검색" class="border border-gray-300 rounded px-2 py-1 text-sm w-40"/>
|
||||||
<label class="text-sm text-gray-600">구·군 코드</label>
|
<label class="text-sm text-gray-600">구군코드</label>
|
||||||
<select name="ds_gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[14rem]">
|
<select name="ds_gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||||
<option value="">전체</option>
|
<option value="">전체</option>
|
||||||
<?php foreach (($gugunCodes ?? []) as $gc): ?>
|
<?php foreach (($gugunCodes ?? []) as $gc): ?>
|
||||||
<?php $gCode = (string) ($gc->ds_gugun_code ?? ''); ?>
|
<option value="<?= esc($gc->ds_gugun_code) ?>" <?= ($dsGugunCode ?? '') === $gc->ds_gugun_code ? 'selected' : '' ?>><?= esc($gc->ds_gugun_code) ?></option>
|
||||||
<option value="<?= esc($gCode) ?>" <?= ($dsGugunCode ?? '') === $gCode ? 'selected' : '' ?>><?= esc((string) (($gugunNameMap[$gCode] ?? '') !== '' ? $gugunNameMap[$gCode] : $gCode)) ?></option>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<label class="text-sm text-gray-600">상태</label>
|
<label class="text-sm text-gray-600">상태</label>
|
||||||
@@ -207,357 +29,48 @@ $listBasePath = $readOnly ? 'designated-shops/browse' : 'designated-shops';
|
|||||||
<option value="3" <?= ($dsState ?? '') === '3' ? 'selected' : '' ?>>직권해지</option>
|
<option value="3" <?= ($dsState ?? '') === '3' ? 'selected' : '' ?>>직권해지</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<a href="<?= mgmt_url($listBasePath) ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">초기화</a>
|
<a href="<?= mgmt_url('designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">초기화</a>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<?php
|
|
||||||
$sc = $stateCounts ?? ['total' => 0, 1 => 0, 2 => 0, 3 => 0];
|
|
||||||
?>
|
|
||||||
<div class="ds-summary-bar no-print mx-2 mt-2 rounded-sm">
|
|
||||||
건수 : <?= (int) ($sc['total'] ?? 0) ?>
|
|
||||||
(정상 : <?= (int) ($sc[1] ?? 0) ?> / 폐업 : <?= (int) ($sc[2] ?? 0) ?> / 해지 : <?= (int) ($sc[3] ?? 0) ?>)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ds-split no-print mx-2 mb-2 mt-2 flex-1 min-h-0">
|
|
||||||
<div class="ds-list-panel">
|
|
||||||
<div class="ds-panel-title shrink-0">지정판매소 리스트</div>
|
|
||||||
<div class="overflow-auto flex-1 min-h-0">
|
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-14">번호</th>
|
<th class="w-16">번호</th>
|
||||||
<th class="w-24">구·군</th>
|
<th>지자체</th>
|
||||||
<th class="w-24">지정일</th>
|
|
||||||
<th class="w-24">구역</th>
|
|
||||||
<th class="ds-col-tight">대표자명</th>
|
|
||||||
<th class="ds-col-tight">상호명</th>
|
|
||||||
<th class="ds-col-zip">우편번호</th>
|
|
||||||
<th class="text-left">주소</th>
|
|
||||||
<th class="w-28">사업자번호</th>
|
|
||||||
<th class="w-28">전화</th>
|
|
||||||
<th class="w-16">상태</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="ds-list-body" class="text-right">
|
|
||||||
<?php foreach ($list as $i => $row): ?>
|
|
||||||
<?php
|
|
||||||
$sn = (string) ($row->ds_shop_no ?? '');
|
|
||||||
if (preg_match('/(\d{3})$/', $sn, $m)) {
|
|
||||||
$shortNo = $m[1];
|
|
||||||
} elseif ($sn !== '' && strlen($sn) >= 3) {
|
|
||||||
$shortNo = substr($sn, -3);
|
|
||||||
} else {
|
|
||||||
$shortNo = $sn;
|
|
||||||
}
|
|
||||||
$st = (int) ($row->ds_state ?? 1);
|
|
||||||
$stLabel = $st === 1 ? '' : ($st === 2 ? '폐업' : '해지');
|
|
||||||
$ggCode = (string) ($row->ds_gugun_code ?? '');
|
|
||||||
$ggLabel = (string) (($gugunNameMap[$ggCode] ?? '') !== '' ? $gugunNameMap[$ggCode] : $ggCode);
|
|
||||||
$da = $row->ds_designated_at ?? null;
|
|
||||||
$daDisp = ($da !== null && $da !== '' && (string) $da !== '0000-00-00') ? substr((string) $da, 0, 10) : '';
|
|
||||||
$zone = (string) ($row->ds_zone_code ?? '');
|
|
||||||
$zipList = trim((string) ($row->ds_zip ?? ''));
|
|
||||||
$roadL = trim((string) ($row->ds_addr ?? ''));
|
|
||||||
$jibunL = trim((string) ($row->ds_addr_jibun ?? ''));
|
|
||||||
$addrMainList = $roadL !== '' ? $roadL : $jibunL;
|
|
||||||
$addrDetailList = trim((string) ($row->ds_addr_detail ?? ''));
|
|
||||||
$addrCombinedList = trim($addrMainList . ' ' . $addrDetailList);
|
|
||||||
if ($addrCombinedList === '') {
|
|
||||||
$addrCombinedList = $addrMainList;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<tr class="ds-list-row cursor-pointer" data-row-index="<?= (int) $i ?>" role="button" tabindex="0">
|
|
||||||
<td class="text-center"><?= esc($shortNo) ?></td>
|
|
||||||
<td class="text-left pl-1 text-xs"><?= esc($ggLabel) ?></td>
|
|
||||||
<td class="text-center text-xs"><?= esc($daDisp) ?></td>
|
|
||||||
<td class="text-left pl-1 text-xs"><?= esc($zone) ?></td>
|
|
||||||
<td class="text-left pl-1 text-xs ds-col-tight" title="<?= esc($row->ds_rep_name ?? '') ?>"><?= esc($row->ds_rep_name ?? '') ?></td>
|
|
||||||
<td class="text-left pl-1 text-xs ds-col-tight" title="<?= esc($row->ds_name ?? '') ?>"><?= esc($row->ds_name ?? '') ?></td>
|
|
||||||
<td class="text-center text-xs ds-col-zip" title="<?= esc($zipList) ?>"><?= esc($zipList) ?></td>
|
|
||||||
<td class="text-left pl-1 text-xs ds-col-addr-list" title="<?= esc($addrCombinedList) ?>"><?= esc($addrCombinedList) ?></td>
|
|
||||||
<td class="text-left pl-1 text-xs"><?= esc($row->ds_biz_no ?? '') ?></td>
|
|
||||||
<td class="text-left pl-1 text-xs"><?= esc($row->ds_tel ?? '') ?></td>
|
|
||||||
<td class="text-center <?= $st === 2 ? 'text-pink-600 font-medium' : ($st === 3 ? 'text-orange-700' : '') ?>"><?= esc($stLabel) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ds-detail-panel">
|
|
||||||
<div class="ds-panel-title shrink-0">지정판매소 정보</div>
|
|
||||||
<div class="ds-detail-inner" id="ds-detail-box">
|
|
||||||
<p id="ds-detail-placeholder" class="text-sm text-gray-500 py-6 text-center">위 목록에서 행을 선택하세요.</p>
|
|
||||||
<div id="ds-detail-fields" class="hidden">
|
|
||||||
<div class="ds-detail-info-wrap">
|
|
||||||
<table class="w-full data-table text-sm" id="ds-detail-info-table" aria-label="지정판매소 상세">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>판매소번호</th>
|
<th>판매소번호</th>
|
||||||
<th class="ds-col-tight-head">상호명</th>
|
|
||||||
<th>우편번호</th>
|
|
||||||
<th>사업자번호</th>
|
|
||||||
<th>일반전화</th>
|
|
||||||
<th class="ds-col-tight-head">대표자명</th>
|
|
||||||
<th>이메일</th>
|
|
||||||
<th>업태</th>
|
|
||||||
<th>업종</th>
|
|
||||||
<th>지정일자</th>
|
|
||||||
<th>지자체</th>
|
|
||||||
<th>도로명주소</th>
|
|
||||||
<th>지번주소</th>
|
|
||||||
<th>상세주소</th>
|
|
||||||
<th>개인전화</th>
|
|
||||||
<th>구·군</th>
|
|
||||||
<th>구역</th>
|
|
||||||
<th>가상계좌(은행)</th>
|
|
||||||
<th>계좌번호</th>
|
|
||||||
<th>종사업장번호</th>
|
|
||||||
<th>변경일자</th>
|
|
||||||
<th>영업상태</th>
|
|
||||||
<th>등록일시</th>
|
|
||||||
<th>변경사유</th>
|
|
||||||
<th class="no-print w-14">지도</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="text-left" data-ro="ds_shop_no">—</td>
|
|
||||||
<td class="text-left ds-col-tight-cell" data-ro="ds_name">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_zip">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_biz_no">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_tel">—</td>
|
|
||||||
<td class="text-left ds-col-tight-cell" data-ro="ds_rep_name">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_email">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_biz_type">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_biz_kind">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_designated_at">—</td>
|
|
||||||
<td class="text-left" data-ro="lg_name">—</td>
|
|
||||||
<td class="text-left min-w-[10rem]"><span data-ro="ds_addr">—</span></td>
|
|
||||||
<td class="text-left" data-ro="ds_addr_jibun">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_addr_detail">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_rep_phone">—</td>
|
|
||||||
<td class="text-left" data-ro="gugun_name">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_zone_code">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_va_bank">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_va_account">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_branch_no">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_state_changed_at">—</td>
|
|
||||||
<td class="text-left" data-ro="state_label">—</td>
|
|
||||||
<td class="text-left" data-ro="ds_regdate">—</td>
|
|
||||||
<td class="text-left min-w-[8rem]" data-ro="ds_change_reason">—</td>
|
|
||||||
<td class="text-center no-print">
|
|
||||||
<button type="button" class="border border-btn-print-border text-gray-700 px-2 py-0.5 rounded-sm text-xs hover:bg-gray-50" id="ds-ro-map-btn">지도</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php if (! $readOnly): ?>
|
|
||||||
<div class="ds-detail-actions no-print flex flex-wrap items-center gap-3 shrink-0">
|
|
||||||
<a id="ds-edit-link" href="#" class="text-blue-700 hover:underline text-sm font-medium pointer-events-none opacity-40">수정</a>
|
|
||||||
<form id="ds-delete-form" method="POST" action="" class="inline" onsubmit="return confirm('이 지정판매소를 삭제하시겠습니까?');">
|
|
||||||
<?= csrf_field() ?>
|
|
||||||
<button type="submit" id="ds-delete-btn" class="text-red-600 hover:underline text-sm pointer-events-none opacity-40" disabled>삭제</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (isset($pager)): ?>
|
|
||||||
<div class="mt-2 mb-2 mx-2 no-print"><?= $pager->links() ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?= view('components/kakao_map_modal', ['kakaoJavascriptKey' => $kakaoJavascriptKey ?? '']) ?>
|
|
||||||
|
|
||||||
<script type="application/json" id="ds-detail-json"><?= $detailRowsJson ?? '[]' ?></script>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var raw = document.getElementById('ds-detail-json');
|
|
||||||
var rows = [];
|
|
||||||
try {
|
|
||||||
rows = JSON.parse(raw.textContent || '[]');
|
|
||||||
} catch (e) {
|
|
||||||
rows = [];
|
|
||||||
}
|
|
||||||
var readOnly = <?= json_encode($readOnly) ?>;
|
|
||||||
var body = document.getElementById('ds-list-body');
|
|
||||||
var placeholder = document.getElementById('ds-detail-placeholder');
|
|
||||||
var fieldsWrap = document.getElementById('ds-detail-fields');
|
|
||||||
var infoTable = document.getElementById('ds-detail-info-table');
|
|
||||||
var editLink = readOnly ? null : document.getElementById('ds-edit-link');
|
|
||||||
var delForm = readOnly ? null : document.getElementById('ds-delete-form');
|
|
||||||
var delBtn = readOnly ? null : document.getElementById('ds-delete-btn');
|
|
||||||
// mgmt_url() 이 path 를 trim 하므로 'edit/33' 이 아니라 'edit33' 로 붙지 않게 슬래시를 넣음
|
|
||||||
var editBase = <?= json_encode(mgmt_url('designated-shops/edit')) ?>;
|
|
||||||
var delBase = <?= json_encode(mgmt_url('designated-shops/delete')) ?>;
|
|
||||||
|
|
||||||
function textVal(v) {
|
|
||||||
return (v === '' || v == null) ? '—' : String(v);
|
|
||||||
}
|
|
||||||
function buildKakaoMapSearchQuery(d) {
|
|
||||||
var road = String(d.ds_addr || '').trim();
|
|
||||||
var jibun = String(d.ds_addr_jibun || '').trim();
|
|
||||||
var detail = String(d.ds_addr_detail || '').trim();
|
|
||||||
var q = road || jibun;
|
|
||||||
if (detail) {
|
|
||||||
q = q ? (q + ' ' + detail) : detail;
|
|
||||||
}
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
function fillDetailInfoTable(d) {
|
|
||||||
if (!infoTable) return;
|
|
||||||
infoTable.querySelectorAll('[data-ro]').forEach(function (el) {
|
|
||||||
var k = el.getAttribute('data-ro');
|
|
||||||
var v = d[k];
|
|
||||||
if (k === 'ds_va_account') {
|
|
||||||
v = d.ds_va_account || d.ds_va_number || '';
|
|
||||||
}
|
|
||||||
el.textContent = textVal(v);
|
|
||||||
});
|
|
||||||
window.__dsDetailForMap = d;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectIndex(idx) {
|
|
||||||
if (!rows.length || idx < 0 || idx >= rows.length) return;
|
|
||||||
var d = rows[idx];
|
|
||||||
Array.prototype.forEach.call(body.querySelectorAll('tr.ds-list-row'), function (tr) {
|
|
||||||
tr.classList.remove('ds-row-selected');
|
|
||||||
});
|
|
||||||
var tr = body.querySelector('tr[data-row-index="' + idx + '"]');
|
|
||||||
if (tr) tr.classList.add('ds-row-selected');
|
|
||||||
|
|
||||||
placeholder.classList.add('hidden');
|
|
||||||
fieldsWrap.classList.remove('hidden');
|
|
||||||
fillDetailInfoTable(d);
|
|
||||||
|
|
||||||
if (!readOnly && editLink && delForm && delBtn) {
|
|
||||||
var id = d.ds_idx;
|
|
||||||
editLink.href = editBase + '/' + id;
|
|
||||||
editLink.classList.remove('pointer-events-none', 'opacity-40');
|
|
||||||
delForm.action = delBase + '/' + id;
|
|
||||||
delBtn.disabled = false;
|
|
||||||
delBtn.classList.remove('pointer-events-none', 'opacity-40');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body) {
|
|
||||||
body.addEventListener('click', function (e) {
|
|
||||||
var tr = e.target.closest('tr.ds-list-row');
|
|
||||||
if (!tr) return;
|
|
||||||
var idx = parseInt(tr.getAttribute('data-row-index'), 10);
|
|
||||||
if (!isNaN(idx)) selectIndex(idx);
|
|
||||||
});
|
|
||||||
body.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
||||||
var tr = e.target.closest('tr.ds-list-row');
|
|
||||||
if (!tr) return;
|
|
||||||
e.preventDefault();
|
|
||||||
var idx = parseInt(tr.getAttribute('data-row-index'), 10);
|
|
||||||
if (!isNaN(idx)) selectIndex(idx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var mapBtnRo = document.getElementById('ds-ro-map-btn');
|
|
||||||
if (mapBtnRo) {
|
|
||||||
mapBtnRo.addEventListener('click', function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
var d = window.__dsDetailForMap;
|
|
||||||
if (!d) return;
|
|
||||||
var q = buildKakaoMapSearchQuery(d);
|
|
||||||
if (!q) {
|
|
||||||
window.alert('표시할 주소 정보가 없습니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof window.openDesignatedShopKakaoMap === 'function') {
|
|
||||||
window.openDesignatedShopKakaoMap(q);
|
|
||||||
} else {
|
|
||||||
window.open('https://map.kakao.com/link/search/' + encodeURIComponent(q), '_blank', 'noopener,noreferrer');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rows.length > 0) {
|
|
||||||
selectIndex(0);
|
|
||||||
} else if (!readOnly && editLink && delBtn) {
|
|
||||||
editLink.classList.add('pointer-events-none', 'opacity-40');
|
|
||||||
delBtn.disabled = true;
|
|
||||||
delBtn.classList.add('pointer-events-none', 'opacity-40');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- 인쇄용: 전체 테이블 -->
|
|
||||||
<div class="hidden print:block print:p-4">
|
|
||||||
<table class="w-full data-table text-xs">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>번호</th>
|
|
||||||
<th>지자체</th>
|
|
||||||
<th>구·군</th>
|
|
||||||
<th>지정일</th>
|
|
||||||
<th>구역</th>
|
|
||||||
<th>대표자명</th>
|
|
||||||
<th>상호명</th>
|
<th>상호명</th>
|
||||||
<th>우편번호</th>
|
<th>대표자</th>
|
||||||
<th>주소</th>
|
|
||||||
<th>사업자번호</th>
|
<th>사업자번호</th>
|
||||||
<th>전화</th>
|
|
||||||
<th>판매소번호</th>
|
|
||||||
<th>가상계좌</th>
|
<th>가상계좌</th>
|
||||||
<th>상태</th>
|
<th>상태</th>
|
||||||
<th>등록일</th>
|
<th>등록일</th>
|
||||||
|
<th class="w-28">작업</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="text-right">
|
||||||
<?php foreach ($list as $row): ?>
|
<?php foreach ($list as $row): ?>
|
||||||
<?php
|
|
||||||
$snP = (string) ($row->ds_shop_no ?? '');
|
|
||||||
if (preg_match('/(\d{3})$/', $snP, $mP)) {
|
|
||||||
$shortNoP = $mP[1];
|
|
||||||
} elseif ($snP !== '' && strlen($snP) >= 3) {
|
|
||||||
$shortNoP = substr($snP, -3);
|
|
||||||
} else {
|
|
||||||
$shortNoP = $snP;
|
|
||||||
}
|
|
||||||
$daP = $row->ds_designated_at ?? null;
|
|
||||||
$daDispP = ($daP !== null && $daP !== '' && (string) $daP !== '0000-00-00') ? substr((string) $daP, 0, 10) : '';
|
|
||||||
$stP = (int) ($row->ds_state ?? 1);
|
|
||||||
$stLabP = $stP === 1 ? '정상' : ($stP === 2 ? '폐업' : '직권해지');
|
|
||||||
$zipP = trim((string) ($row->ds_zip ?? ''));
|
|
||||||
$roadP = trim((string) ($row->ds_addr ?? ''));
|
|
||||||
$jibP = trim((string) ($row->ds_addr_jibun ?? ''));
|
|
||||||
$addrP = $roadP !== '' ? $roadP : $jibP;
|
|
||||||
$detP = trim((string) ($row->ds_addr_detail ?? ''));
|
|
||||||
$addrCombinedP = trim($addrP . ' ' . $detP);
|
|
||||||
if ($addrCombinedP === '') {
|
|
||||||
$addrCombinedP = $addrP;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($shortNoP) ?></td>
|
<td class="text-center"><?= esc($row->ds_idx) ?></td>
|
||||||
<td class="text-left"><?= esc($lgMap[$row->ds_lg_idx] ?? '') ?></td>
|
<td class="text-left pl-2"><?= esc($lgMap[$row->ds_lg_idx] ?? '') ?></td>
|
||||||
<?php $gCodeP = (string) ($row->ds_gugun_code ?? ''); ?>
|
<td class="text-left pl-2"><?= esc($row->ds_shop_no) ?></td>
|
||||||
<td class="text-left"><?= esc((string) (($gugunNameMap[$gCodeP] ?? '') !== '' ? $gugunNameMap[$gCodeP] : $gCodeP)) ?></td>
|
<td class="text-left pl-2"><?= esc($row->ds_name) ?></td>
|
||||||
<td class="text-center"><?= esc($daDispP) ?></td>
|
<td class="text-left pl-2"><?= esc($row->ds_rep_name) ?></td>
|
||||||
<td class="text-left"><?= esc($row->ds_zone_code ?? '') ?></td>
|
<td class="text-left pl-2"><?= esc($row->ds_biz_no) ?></td>
|
||||||
<td class="text-left"><?= esc($row->ds_rep_name ?? '') ?></td>
|
<td class="text-left pl-2"><?= esc($row->ds_va_number) ?></td>
|
||||||
<td class="text-left"><?= esc($row->ds_name ?? '') ?></td>
|
<td class="text-center"><?= (int) $row->ds_state === 1 ? '정상' : ((int) $row->ds_state === 2 ? '폐업' : '직권해지') ?></td>
|
||||||
<td class="text-left"><?= esc($zipP) ?></td>
|
<td class="text-left pl-2"><?= esc($row->ds_regdate ?? '') ?></td>
|
||||||
<td class="text-left"><?= esc($addrCombinedP) ?></td>
|
<td class="text-center">
|
||||||
<td class="text-left"><?= esc($row->ds_biz_no ?? '') ?></td>
|
<a href="<?= mgmt_url('designated-shops/edit/' . (int) $row->ds_idx) ?>" class="text-blue-600 hover:underline text-sm">수정</a>
|
||||||
<td class="text-left"><?= esc($row->ds_tel ?? '') ?></td>
|
<form action="<?= mgmt_url('designated-shops/delete/' . (int) $row->ds_idx) ?>" method="POST" class="inline ml-1" onsubmit="return confirm('이 지정판매소를 삭제하시겠습니까?');">
|
||||||
<td class="text-left"><?= esc($row->ds_shop_no) ?></td>
|
<?= csrf_field() ?>
|
||||||
<td class="text-left"><?= esc($row->ds_va_number) ?></td>
|
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
|
||||||
<td class="text-center"><?= esc($stLabP) ?></td>
|
</form>
|
||||||
<td class="text-left"><?= esc($row->ds_regdate ?? '') ?></td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
||||||
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
$readOnly = ! empty($readOnly);
|
|
||||||
$listBasePath = $readOnly ? 'designated-shops/browse' : 'designated-shops';
|
|
||||||
?>
|
|
||||||
<?= view('components/print_header', ['printTitle' => $readOnly ? '지정판매소 조회 목록' : '지정판매소 목록']) ?>
|
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
|
||||||
<span class="text-sm font-bold text-gray-700"><?= $readOnly ? '지정판매소 조회' : '지정판매소 관리' ?></span>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<?php if ($readOnly): ?>
|
|
||||||
<a href="<?= mgmt_url('designated-shops/export') ?>" 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 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>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (! $readOnly): ?>
|
|
||||||
<a href="<?= mgmt_url('designated-shops/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">지정판매소 등록</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
|
||||||
<form method="GET" action="<?= mgmt_url($listBasePath) ?>" class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="text-sm font-semibold text-gray-700 mr-1">지정판매소 검색</span>
|
|
||||||
<label class="text-sm text-gray-600">상호명</label>
|
|
||||||
<input type="text" name="ds_name" value="<?= esc($dsName ?? '') ?>" placeholder="상호명" class="border border-gray-300 rounded px-2 py-1 text-sm w-36"/>
|
|
||||||
<label class="text-sm text-gray-600">구군코드</label>
|
|
||||||
<select name="ds_gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<?php foreach (($gugunCodes ?? []) as $gc): ?>
|
|
||||||
<option value="<?= esc($gc->ds_gugun_code) ?>" <?= ($dsGugunCode ?? '') === $gc->ds_gugun_code ? 'selected' : '' ?>><?= esc($gc->ds_gugun_code) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
<label class="text-sm text-gray-600">상태</label>
|
|
||||||
<select name="ds_state" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<option value="1" <?= ($dsState ?? '') === '1' ? 'selected' : '' ?>>정상</option>
|
|
||||||
<option value="2" <?= ($dsState ?? '') === '2' ? 'selected' : '' ?>>폐업</option>
|
|
||||||
<option value="3" <?= ($dsState ?? '') === '3' ? 'selected' : '' ?>>직권해지</option>
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
|
||||||
<a href="<?= mgmt_url($listBasePath) ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">초기화</a>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
|
||||||
<table class="w-full 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>
|
|
||||||
<?php if (! $readOnly): ?>
|
|
||||||
<th class="w-28">작업</th>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="text-right">
|
|
||||||
<?php foreach ($list as $row): ?>
|
|
||||||
<tr>
|
|
||||||
<td class="text-center"><?= esc($row->ds_idx) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($lgMap[$row->ds_lg_idx] ?? '') ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_shop_no) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_name) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_rep_name) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_biz_no) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_va_number) ?></td>
|
|
||||||
<td class="text-center"><?= (int) $row->ds_state === 1 ? '정상' : ((int) $row->ds_state === 2 ? '폐업' : '직권해지') ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($row->ds_regdate ?? '') ?></td>
|
|
||||||
<?php if (! $readOnly): ?>
|
|
||||||
<td class="text-center">
|
|
||||||
<a href="<?= mgmt_url('designated-shops/edit/' . (int) $row->ds_idx) ?>" class="text-blue-600 hover:underline text-sm">수정</a>
|
|
||||||
<form action="<?= mgmt_url('designated-shops/delete/' . (int) $row->ds_idx) ?>" method="POST" class="inline ml-1" onsubmit="return confirm('이 지정판매소를 삭제하시겠습니까?');">
|
|
||||||
<?= csrf_field() ?>
|
|
||||||
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>
|
|
||||||
@@ -8,12 +8,12 @@
|
|||||||
<div id="kakao-map" class="w-full border border-gray-300 mt-2" style="height:600px;"></div>
|
<div id="kakao-map" class="w-full border border-gray-300 mt-2" style="height:600px;"></div>
|
||||||
<div class="mt-2 text-sm text-gray-500">총 <?= count($shops) ?>개 판매소 표시</div>
|
<div class="mt-2 text-sm text-gray-500">총 <?= count($shops) ?>개 판매소 표시</div>
|
||||||
|
|
||||||
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=<?= esc($kakaoJavascriptKey ?? '', 'attr') ?>&libraries=services"></script>
|
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=KAKAO_APP_KEY&libraries=services"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var mapContainer = document.getElementById('kakao-map');
|
var mapContainer = document.getElementById('kakao-map');
|
||||||
if (typeof kakao === 'undefined' || typeof kakao.maps === 'undefined') {
|
if (typeof kakao === 'undefined' || typeof kakao.maps === 'undefined') {
|
||||||
mapContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500 text-sm px-4 text-center">카카오맵을 불러올 수 없습니다. Kakao Developers → 제품 설정에서 「Kakao Map」을 켜고, 플랫폼(Web)에 이 사이트 URL을 등록했는지 확인하세요.</div>';
|
mapContainer.innerHTML = '<div class="flex items-center justify-center h-full text-gray-400">카카오맵 API 키를 설정해 주세요.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,387 +1,80 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '지정판매소 현황']) ?>
|
||||||
$ry = (int) ($reportYear ?? (int) date('Y'));
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
$exportUrl = mgmt_url('designated-shops/status/export') . '?' . http_build_query([
|
|
||||||
'year' => $ry,
|
|
||||||
]);
|
|
||||||
$fixedGugunLabel = trim((string) ($fixedGugunLabel ?? ''));
|
|
||||||
$regionColLabel = '군·구';
|
|
||||||
$sumCurrForPct = (int) ($districtTotal->curr_end ?? 0);
|
|
||||||
?>
|
|
||||||
<style>
|
|
||||||
.ds-status-x-scroll {
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: visible;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
@media print {
|
|
||||||
.ds-status-x-scroll { overflow: visible !important; border: none; }
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll .ds-status-table {
|
|
||||||
width: max-content;
|
|
||||||
min-width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll .ds-status-table th,
|
|
||||||
.ds-status-x-scroll .ds-status-table td {
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll .ds-status-table thead th {
|
|
||||||
background: #e9ecef;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll .ds-status-table tbody td {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll th.sticky-num,
|
|
||||||
.ds-status-x-scroll td.sticky-num {
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 3;
|
|
||||||
min-width: 3rem;
|
|
||||||
max-width: 3rem;
|
|
||||||
width: 3rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #e9ecef;
|
|
||||||
border-right: 1px solid #bbb;
|
|
||||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll td.sticky-num {
|
|
||||||
background: #fff;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll tr.sum-row td.sticky-num {
|
|
||||||
background: #f3f4f6;
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll th.sticky-region,
|
|
||||||
.ds-status-x-scroll td.sticky-region {
|
|
||||||
position: sticky;
|
|
||||||
left: 3rem;
|
|
||||||
z-index: 2;
|
|
||||||
background: #e9ecef;
|
|
||||||
border-right: 1px solid #bbb;
|
|
||||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.06);
|
|
||||||
max-width: 16rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll td.sticky-region {
|
|
||||||
background: #fff;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.ds-status-x-scroll tr.sum-row td.sticky-region {
|
|
||||||
background: #f3f4f6;
|
|
||||||
}
|
|
||||||
.ds-help {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.ds-help-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
padding: 0 2px;
|
|
||||||
border: 1px solid #64748b;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #fef9c3;
|
|
||||||
color: #111827;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
cursor: help;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.ds-floating-tip {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
display: none;
|
|
||||||
max-width: min(22rem, calc(100vw - 16px));
|
|
||||||
padding: 6px 8px;
|
|
||||||
border: 1px solid #8fa0b3;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #1f2937;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.35;
|
|
||||||
text-align: left;
|
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
|
||||||
z-index: 9999;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<?= view('components/print_header', ['printTitle' => '지정판매소 신규·취소 현황 (' . $ry . '년)']) ?>
|
|
||||||
<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">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">지정판매소 현황 (신규/취소)</span>
|
<span class="text-sm font-bold text-gray-700">지정판매소 현황 (신규/취소)</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a href="<?= esc($exportUrl) ?>" class="border border-btn-excel-border text-btn-excel-text px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
<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>
|
||||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
|
||||||
<a href="<?= mgmt_url('designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록으로</a>
|
<a href="<?= mgmt_url('designated-shops') ?>" class="border border-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">목록으로</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
<!-- 전체 현황 요약 -->
|
||||||
<form method="get" action="<?= mgmt_url('designated-shops/status') ?>" class="flex flex-wrap items-end gap-3">
|
<div class="flex gap-4 mt-2 mb-2">
|
||||||
<div>
|
<div class="border border-gray-300 p-3 flex-1 text-center">
|
||||||
<label class="block text-xs text-gray-600 mb-0.5">연도</label>
|
<div class="text-sm text-gray-500">활성 판매소</div>
|
||||||
<select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[6rem]">
|
<div class="text-2xl font-bold text-green-600"><?= number_format($totalActive) ?></div>
|
||||||
<?php foreach (($yearChoices ?? []) as $y): ?>
|
|
||||||
<option value="<?= (int) $y ?>" <?= $ry === (int) $y ? 'selected' : '' ?>><?= (int) $y ?>년</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-[12rem]">
|
<div class="border border-gray-300 p-3 flex-1 text-center">
|
||||||
<label class="block text-xs text-gray-600 mb-0.5">군·구</label>
|
<div class="text-sm text-gray-500">비활성/취소 판매소</div>
|
||||||
<div class="border border-gray-300 rounded px-3 py-1 text-sm bg-gray-50 text-gray-800 font-medium pointer-events-none select-none">
|
<div class="text-2xl font-bold text-red-600"><?= number_format($totalInactive) ?></div>
|
||||||
<?= esc($fixedGugunLabel !== '' ? $fixedGugunLabel : '현재 지자체 기준') ?>
|
</div>
|
||||||
|
<div class="border border-gray-300 p-3 flex-1 text-center">
|
||||||
|
<div class="text-sm text-gray-500">전체</div>
|
||||||
|
<div class="text-2xl font-bold text-gray-700"><?= number_format($totalActive + $totalInactive) ?></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
|
||||||
</form>
|
|
||||||
<p class="text-xs text-gray-500 mt-2">
|
|
||||||
종전·현행은 각각 <?= $ry - 1 ?>-12-31·<?= $ry ?>-12-31 기준 정상(지정일 이전·비정상 전환일 이후) 건수이며, 지정·취소는 <?= $ry ?>년 달력연도 기준입니다. 군·구는 현재 로그인 사용자의 지자체 기준으로 고정 표시됩니다.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 인쇄 시에도 보이는 본표 -->
|
|
||||||
<div class="mx-2 mt-2 mb-2 ds-status-x-scroll">
|
|
||||||
<table class="ds-status-table data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="sticky-num text-center w-12">순번</th>
|
|
||||||
<th class="sticky-region"><?= esc($regionColLabel) ?></th>
|
|
||||||
<th class="text-left">
|
|
||||||
<span class="ds-help">구코드 <span class="ds-help-badge" tabindex="0" data-tip="지정판매소에 저장된 구·군 코드 값">?</span></span>
|
|
||||||
</th>
|
|
||||||
<th class="text-right">
|
|
||||||
<span class="ds-help">종전 <span class="ds-help-badge" tabindex="0" data-tip="전년도 12월 31일 기준 정상 상태 판매소 수">?</span></span>(전년도말)
|
|
||||||
</th>
|
|
||||||
<th class="text-right">
|
|
||||||
<span class="ds-help">지정 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 내 지정일이 속한 신규 지정 건수">?</span></span>(<?= $ry ?>년)
|
|
||||||
</th>
|
|
||||||
<th class="text-right">
|
|
||||||
<span class="ds-help">취소 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 내 폐업/해지 전환일이 속한 건수">?</span></span>(<?= $ry ?>년)
|
|
||||||
</th>
|
|
||||||
<th class="text-right">
|
|
||||||
<span class="ds-help">현행 <span class="ds-help-badge" tabindex="0" data-tip="조회년도 12월 31일 기준 정상 상태 판매소 수">?</span></span>(금년도말)
|
|
||||||
</th>
|
|
||||||
<th class="text-right">
|
|
||||||
<span class="ds-help">증감 <span class="ds-help-badge" tabindex="0" data-tip="현행에서 종전을 뺀 값 (현행−종전)">?</span></span>
|
|
||||||
<br/><span class="font-normal text-xs">(현행−종전)</span>
|
|
||||||
</th>
|
|
||||||
<th class="text-right">
|
|
||||||
<span class="ds-help">지정−취소 <span class="ds-help-badge" tabindex="0" data-tip="<?= esc($ry) ?>년 지정 건수에서 취소 건수를 뺀 값">?</span></span>
|
|
||||||
<br/><span class="font-normal text-xs">(<?= $ry ?>년)</span>
|
|
||||||
</th>
|
|
||||||
<th class="text-right">
|
|
||||||
<span class="ds-help">현행비중 <span class="ds-help-badge" tabindex="0" data-tip="전체 현행 합계 대비 해당 행 현행 건수의 비율(%)">?</span></span>
|
|
||||||
<br/><span class="font-normal text-xs">(%)</span>
|
|
||||||
</th>
|
|
||||||
<th class="text-right">
|
|
||||||
<span class="ds-help">전년대비 <span class="ds-help-badge ds-help-right" tabindex="0" data-tip="((현행−종전) / 종전) × 100, 종전이 0이면 표시 안함">?</span></span>
|
|
||||||
<br/><span class="font-normal text-xs">증감률(%)</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="text-right">
|
|
||||||
<?php $rowNo = 0; ?>
|
|
||||||
<?php foreach (($districtRows ?? []) as $row): ?>
|
|
||||||
<?php
|
|
||||||
$rowNo++;
|
|
||||||
$curr = (int) $row->curr_end;
|
|
||||||
$prev = (int) $row->prev_end;
|
|
||||||
$pctShare = $sumCurrForPct > 0 ? round(($curr / $sumCurrForPct) * 100, 1) : 0.0;
|
|
||||||
$yoyPct = $prev > 0 ? round((($curr - $prev) / $prev) * 100, 1) : null;
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td class="sticky-num"><?= $rowNo ?></td>
|
|
||||||
<td class="sticky-region" title="<?= esc($row->region_label) ?>"><?= esc($row->region_label) ?></td>
|
|
||||||
<td class="text-left text-xs"><?= esc((string) ($row->gugun_code ?? '')) ?></td>
|
|
||||||
<td><?= number_format($prev) ?></td>
|
|
||||||
<td><?= number_format((int) $row->designated_y) ?></td>
|
|
||||||
<td><?= number_format((int) $row->cancelled_y) ?></td>
|
|
||||||
<td><?= number_format($curr) ?></td>
|
|
||||||
<td><?= number_format((int) ($row->delta_curr_prev ?? 0)) ?></td>
|
|
||||||
<td><?= number_format((int) ($row->delta_des_cancel ?? 0)) ?></td>
|
|
||||||
<td><?= $pctShare ?></td>
|
|
||||||
<td><?= $yoyPct !== null ? $yoyPct : '—' ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($districtRows)): ?>
|
|
||||||
<tr><td colspan="11" class="text-center text-gray-400 py-6">조건에 맞는 데이터가 없습니다.</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (! empty($districtRows) && isset($districtTotal)): ?>
|
|
||||||
<tr class="font-bold bg-gray-50 sum-row">
|
|
||||||
<td class="sticky-num">—</td>
|
|
||||||
<td class="sticky-region"><?= esc($districtTotal->region_label) ?></td>
|
|
||||||
<td class="text-left">—</td>
|
|
||||||
<td><?= number_format((int) $districtTotal->prev_end) ?></td>
|
|
||||||
<td><?= number_format((int) $districtTotal->designated_y) ?></td>
|
|
||||||
<td><?= number_format((int) $districtTotal->cancelled_y) ?></td>
|
|
||||||
<td><?= number_format((int) $districtTotal->curr_end) ?></td>
|
|
||||||
<td><?= number_format((int) ($districtTotal->delta_curr_prev ?? 0)) ?></td>
|
|
||||||
<td><?= number_format((int) ($districtTotal->delta_des_cancel ?? 0)) ?></td>
|
|
||||||
<td>100</td>
|
|
||||||
<td>
|
|
||||||
<?php
|
|
||||||
$tPrev = (int) $districtTotal->prev_end;
|
|
||||||
$tCurr = (int) $districtTotal->curr_end;
|
|
||||||
echo $tPrev > 0 ? round((($tCurr - $tPrev) / $tPrev) * 100, 1) : '—';
|
|
||||||
?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php $zoneRows = $zoneSummaryRows ?? []; ?>
|
|
||||||
<section class="mx-2 mb-3 no-print">
|
|
||||||
<div class="text-xs font-semibold text-gray-700 mb-1">동별 현행 요약 (<?= esc($fixedGugunLabel ?? '군·구') ?>)</div>
|
|
||||||
<?php if (! empty($zoneRows)): ?>
|
|
||||||
<div class="flex flex-wrap gap-1 mb-2">
|
|
||||||
<?php foreach ($zoneRows as $z): ?>
|
|
||||||
<span class="inline-flex items-center px-2 py-0.5 text-xs rounded border border-gray-300 bg-gray-50 text-gray-700">
|
|
||||||
<?= esc((string) $z->zone_label) ?> <?= number_format((int) $z->curr_end) ?>
|
|
||||||
</span>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<div class="border border-gray-300 bg-white overflow-auto max-h-56">
|
|
||||||
<table class="w-full data-table text-xs">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="text-left">동</th>
|
|
||||||
<th class="text-right">종전</th>
|
|
||||||
<th class="text-right">지정</th>
|
|
||||||
<th class="text-right">취소</th>
|
|
||||||
<th class="text-right">현행</th>
|
|
||||||
<th class="text-right">증감</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="text-right">
|
|
||||||
<?php foreach ($zoneRows as $z): ?>
|
|
||||||
<tr>
|
|
||||||
<td class="text-left"><?= esc((string) $z->zone_label) ?></td>
|
|
||||||
<td><?= number_format((int) $z->prev_end) ?></td>
|
|
||||||
<td><?= number_format((int) $z->designated_y) ?></td>
|
|
||||||
<td><?= number_format((int) $z->cancelled_y) ?></td>
|
|
||||||
<td><?= number_format((int) $z->curr_end) ?></td>
|
|
||||||
<td><?= number_format((int) $z->delta_curr_prev) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<p class="text-xs text-gray-500">동별 집계 데이터가 없습니다.</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div id="ds-floating-tip" class="ds-floating-tip no-print" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var tipEl = document.getElementById('ds-floating-tip');
|
|
||||||
if (!tipEl) return;
|
|
||||||
var badges = Array.prototype.slice.call(document.querySelectorAll('.ds-help-badge'));
|
|
||||||
if (!badges.length) return;
|
|
||||||
|
|
||||||
function placeTip(target) {
|
|
||||||
var text = String(target.getAttribute('data-tip') || '').trim();
|
|
||||||
if (!text) return;
|
|
||||||
tipEl.textContent = text;
|
|
||||||
tipEl.style.display = 'block';
|
|
||||||
tipEl.setAttribute('aria-hidden', 'false');
|
|
||||||
|
|
||||||
var rect = target.getBoundingClientRect();
|
|
||||||
var tipRect = tipEl.getBoundingClientRect();
|
|
||||||
var vw = window.innerWidth || document.documentElement.clientWidth;
|
|
||||||
var vh = window.innerHeight || document.documentElement.clientHeight;
|
|
||||||
var gap = 8;
|
|
||||||
|
|
||||||
var left = rect.left + (rect.width / 2) - (tipRect.width / 2);
|
|
||||||
var top = rect.bottom + gap;
|
|
||||||
|
|
||||||
if (left < gap) left = gap;
|
|
||||||
if (left + tipRect.width > vw - gap) left = vw - gap - tipRect.width;
|
|
||||||
if (top + tipRect.height > vh - gap) top = rect.top - gap - tipRect.height;
|
|
||||||
if (top < gap) top = gap;
|
|
||||||
|
|
||||||
tipEl.style.left = Math.round(left) + 'px';
|
|
||||||
tipEl.style.top = Math.round(top) + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideTip() {
|
|
||||||
tipEl.style.display = 'none';
|
|
||||||
tipEl.setAttribute('aria-hidden', 'true');
|
|
||||||
tipEl.textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
badges.forEach(function (badge) {
|
|
||||||
badge.addEventListener('mouseenter', function () { placeTip(badge); });
|
|
||||||
badge.addEventListener('focus', function () { placeTip(badge); });
|
|
||||||
badge.addEventListener('mouseleave', hideTip);
|
|
||||||
badge.addEventListener('blur', hideTip);
|
|
||||||
});
|
|
||||||
window.addEventListener('scroll', hideTip, true);
|
|
||||||
window.addEventListener('resize', hideTip);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<details class="mx-2 mb-4 no-print text-sm">
|
|
||||||
<summary class="cursor-pointer text-gray-600 hover:text-gray-800">연도별 요약 (참고)</summary>
|
|
||||||
<div class="flex gap-4 mt-2">
|
|
||||||
<div class="border border-gray-300 p-2 flex-1">
|
|
||||||
<div class="text-xs font-bold text-gray-700 mb-1">활성 / 비활성 / 전체</div>
|
|
||||||
<div class="text-sm">활성 <?= number_format((int) ($totalActive ?? 0)) ?> · 비활성 <?= number_format((int) ($totalInactive ?? 0)) ?> · 합 <?= number_format((int) ($totalActive ?? 0) + (int) ($totalInactive ?? 0)) ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
||||||
|
<!-- 연도별 신규등록 -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xs font-bold text-gray-700 mb-1">연도별 신규등록 (지정일)</h3>
|
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 신규등록 건수</h3>
|
||||||
<div class="border border-gray-300 overflow-auto max-h-48">
|
<div class="border border-gray-300 overflow-auto">
|
||||||
<table class="w-full data-table text-xs">
|
<table class="w-full data-table">
|
||||||
<thead><tr><th>연도</th><th>건수</th></tr></thead>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>연도</th>
|
||||||
|
<th>신규등록 건수</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php foreach (($newByYear ?? []) as $row): ?>
|
<?php foreach ($newByYear as $row): ?>
|
||||||
<tr><td class="text-center"><?= esc($row->yr) ?>년</td><td><?= number_format((int) $row->cnt) ?></td></tr>
|
<tr>
|
||||||
|
<td class="text-center"><?= esc($row->yr) ?>년</td>
|
||||||
|
<td><?= number_format((int) $row->cnt) ?></td>
|
||||||
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($newByYear)): ?>
|
<?php if (empty($newByYear)): ?>
|
||||||
<tr><td colspan="2" class="text-center text-gray-400 py-2">없음</td></tr>
|
<tr><td colspan="2" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 연도별 취소/비활성 -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xs font-bold text-gray-700 mb-1">연도별 취소/비활성 (등록일 기준)</h3>
|
<h3 class="text-sm font-bold text-gray-700 mb-1">연도별 취소/비활성 건수</h3>
|
||||||
<div class="border border-gray-300 overflow-auto max-h-48">
|
<div class="border border-gray-300 overflow-auto">
|
||||||
<table class="w-full data-table text-xs">
|
<table class="w-full data-table">
|
||||||
<thead><tr><th>연도</th><th>건수</th></tr></thead>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>연도</th>
|
||||||
|
<th>취소/비활성 건수</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php foreach (($cancelByYear ?? []) as $row): ?>
|
<?php foreach ($cancelByYear as $row): ?>
|
||||||
<tr><td class="text-center"><?= esc($row->yr) ?>년</td><td><?= number_format((int) $row->cnt) ?></td></tr>
|
<tr>
|
||||||
|
<td class="text-center"><?= esc($row->yr) ?>년</td>
|
||||||
|
<td><?= number_format((int) $row->cnt) ?></td>
|
||||||
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($cancelByYear)): ?>
|
<?php if (empty($cancelByYear)): ?>
|
||||||
<tr><td colspan="2" class="text-center text-gray-400 py-2">없음</td></tr>
|
<tr><td colspan="2" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
<span class="text-sm font-bold text-gray-700">무료용 대상 등록</span>
|
<span class="text-sm font-bold text-gray-700">대상자 등록</span>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
<form action="<?= mgmt_url('free-recipients/store') ?>" method="POST" class="space-y-4">
|
<form action="<?= mgmt_url('free-recipients/store') ?>" method="POST" class="space-y-4">
|
||||||
@@ -9,19 +9,29 @@
|
|||||||
<label class="block text-sm font-bold text-gray-700 w-28">구분 <span class="text-red-500">*</span></label>
|
<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="fr_type_code" required>
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="fr_type_code" required>
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach (($recipientTypeOptions ?? []) as $typeCode => $typeName): ?>
|
<?php foreach ($typeCodes as $cd): ?>
|
||||||
<option value="<?= esc((string) $typeCode) ?>" <?= old('fr_type_code') === (string) $typeCode ? 'selected' : '' ?>>
|
<option value="<?= esc($cd->cd_code) ?>" <?= old('fr_type_code') === $cd->cd_code ? 'selected' : '' ?>>
|
||||||
<?= esc((string) $typeName) ?>
|
<?= esc($cd->cd_name) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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>
|
<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="fr_name" type="text" value="<?= esc(old('fr_name')) ?>" required/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="fr_name" type="text" value="<?= esc(old('fr_name')) ?>" required/>
|
||||||
</div>
|
</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-60" name="fr_phone" type="text" value="<?= esc(old('fr_phone')) ?>"/>
|
||||||
|
</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-96" name="fr_addr" type="text" value="<?= esc(old('fr_addr')) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">동코드</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">동코드</label>
|
||||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="fr_dong_code">
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="fr_dong_code">
|
||||||
@@ -42,7 +52,6 @@
|
|||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">종료일</label>
|
<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="fr_end_date" type="date" value="<?= esc(old('fr_end_date')) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="fr_end_date" type="date" value="<?= esc(old('fr_end_date')) ?>"/>
|
||||||
<span class="text-xs text-gray-500">미입력 시 계속 유효</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-2">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
<span class="text-sm font-bold text-gray-700">무료용 대상 수정</span>
|
<span class="text-sm font-bold text-gray-700">대상자 수정</span>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-3xl">
|
||||||
<form action="<?= mgmt_url('free-recipients/update/' . (int) $item->fr_idx) ?>" method="POST" class="space-y-4">
|
<form action="<?= mgmt_url('free-recipients/update/' . (int) $item->fr_idx) ?>" method="POST" class="space-y-4">
|
||||||
@@ -9,19 +9,29 @@
|
|||||||
<label class="block text-sm font-bold text-gray-700 w-28">구분 <span class="text-red-500">*</span></label>
|
<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="fr_type_code" required>
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="fr_type_code" required>
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach (($recipientTypeOptions ?? []) as $typeCode => $typeName): ?>
|
<?php foreach ($typeCodes as $cd): ?>
|
||||||
<option value="<?= esc((string) $typeCode) ?>" <?= old('fr_type_code', $item->fr_type_code) === (string) $typeCode ? 'selected' : '' ?>>
|
<option value="<?= esc($cd->cd_code) ?>" <?= old('fr_type_code', $item->fr_type_code) === $cd->cd_code ? 'selected' : '' ?>>
|
||||||
<?= esc((string) $typeName) ?>
|
<?= esc($cd->cd_name) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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>
|
<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="fr_name" type="text" value="<?= esc(old('fr_name', $item->fr_name)) ?>" required/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="fr_name" type="text" value="<?= esc(old('fr_name', $item->fr_name)) ?>" required/>
|
||||||
</div>
|
</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-60" name="fr_phone" type="text" value="<?= esc(old('fr_phone', $item->fr_phone)) ?>"/>
|
||||||
|
</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-96" name="fr_addr" type="text" value="<?= esc(old('fr_addr', $item->fr_addr)) ?>"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">동코드</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">동코드</label>
|
||||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="fr_dong_code">
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="fr_dong_code">
|
||||||
@@ -42,7 +52,6 @@
|
|||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">종료일</label>
|
<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="fr_end_date" type="date" value="<?= esc(old('fr_end_date', $item->fr_end_date)) ?>"/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="fr_end_date" type="date" value="<?= esc(old('fr_end_date', $item->fr_end_date)) ?>"/>
|
||||||
<span class="text-xs text-gray-500">미입력 시 계속 유효</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
|||||||
@@ -13,36 +13,28 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-16">번호</th>
|
<th class="w-16">번호</th>
|
||||||
<th class="w-28">동코드</th>
|
<th>구분</th>
|
||||||
<th class="w-40">구분</th>
|
<th>대상자명</th>
|
||||||
<th>명칭</th>
|
<th>연락처</th>
|
||||||
<th class="w-28">종료일자</th>
|
<th>주소</th>
|
||||||
<th class="w-48">비고</th>
|
<th>동코드</th>
|
||||||
|
<th>비고</th>
|
||||||
|
<th>종료일</th>
|
||||||
<th class="w-20">상태</th>
|
<th class="w-20">상태</th>
|
||||||
<th class="w-36">작업</th>
|
<th class="w-36">작업</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php
|
|
||||||
$total = (int) ($totalCount ?? count($list));
|
|
||||||
$page = max(1, (int) ($currentPage ?? 1));
|
|
||||||
$size = max(1, (int) ($perPage ?? max(1, count($list))));
|
|
||||||
$rowNo = $total - (($page - 1) * $size);
|
|
||||||
?>
|
|
||||||
<?php foreach ($list as $row): ?>
|
<?php foreach ($list as $row): ?>
|
||||||
<?php
|
|
||||||
$typeCode = (string) ($row->fr_type_code ?? '');
|
|
||||||
$typeName = (string) (($recipientTypeOptions[$typeCode] ?? '') ?: $typeCode);
|
|
||||||
$dongCode = (string) ($row->fr_dong_code ?? '');
|
|
||||||
$dongLabel = $dongCode !== '' ? (string) (($dongNameMap[$dongCode] ?? $dongCode) . ' (' . $dongCode . ')') : '-';
|
|
||||||
?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc((string) $rowNo) ?></td>
|
<td class="text-center"><?= esc($row->fr_idx) ?></td>
|
||||||
<td class="text-center"><?= esc($dongLabel) ?></td>
|
<td class="text-center"><?= esc($row->fr_type_code) ?></td>
|
||||||
<td class="text-center"><?= esc($typeName) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc($row->fr_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->fr_name) ?></td>
|
||||||
<td class="text-center"><?= esc($row->fr_end_date ?: '9999.99.99') ?></td>
|
<td class="text-center"><?= esc($row->fr_phone) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->fr_addr) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->fr_dong_code) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->fr_note) ?></td>
|
<td class="text-left pl-2"><?= esc($row->fr_note) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->fr_end_date) ?></td>
|
||||||
<td class="text-center"><?= (int) $row->fr_state === 1 ? '사용' : '미사용' ?></td>
|
<td class="text-center"><?= (int) $row->fr_state === 1 ? '사용' : '미사용' ?></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="<?= mgmt_url('free-recipients/edit/' . (int) $row->fr_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
|
<a href="<?= mgmt_url('free-recipients/edit/' . (int) $row->fr_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
|
||||||
@@ -52,11 +44,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php $rowNo--; ?>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($list)): ?>
|
<?php if (empty($list)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center text-gray-500 py-4 text-sm space-y-1">
|
<td colspan="10" class="text-center text-gray-500 py-4 text-sm space-y-1">
|
||||||
<p>등록된 데이터가 없습니다.</p>
|
<p>등록된 데이터가 없습니다.</p>
|
||||||
<p class="text-gray-400">다른 지자체를 선택 중이면 해당 지자체 기준으로만 조회됩니다. Super Admin 은 상단에서 작업 지자체를 바꿔 보세요.</p>
|
<p class="text-gray-400">다른 지자체를 선택 중이면 해당 지자체 기준으로만 조회됩니다. Super Admin 은 상단에서 작업 지자체를 바꿔 보세요.</p>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,151 +1,179 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* 관리자 공통 레이아웃 — gov-portal 디자인 적용판.
|
|
||||||
* 헤더 + 관리자 대메뉴(클릭) + 좌측 사이드바(소메뉴) + 본문($content).
|
|
||||||
*
|
|
||||||
* @var string $title
|
|
||||||
* @var string $content
|
|
||||||
*/
|
|
||||||
helper('admin');
|
helper('admin');
|
||||||
|
$uriObj = service('request')->getUri();
|
||||||
|
$n = $uriObj->getTotalSegments();
|
||||||
|
$uri = $n >= 2 ? $uriObj->getSegment(2) : '';
|
||||||
|
$seg3 = $n >= 3 ? $uriObj->getSegment(3) : '';
|
||||||
$mbLevel = (int) session()->get('mb_level');
|
$mbLevel = (int) session()->get('mb_level');
|
||||||
$isSuperAdmin = \Config\Roles::isSuperAdminEquivalent($mbLevel);
|
$isSuperAdmin = \Config\Roles::isSuperAdminEquivalent($mbLevel);
|
||||||
$mbName = (string) (session()->get('mb_name') ?? '담당자');
|
|
||||||
$levelName = config(\Config\Roles::class)->getLevelName($mbLevel);
|
|
||||||
|
|
||||||
$effectiveLgIdx = admin_effective_lg_idx();
|
$effectiveLgIdx = admin_effective_lg_idx();
|
||||||
$effectiveLgName = '';
|
$effectiveLgName = null;
|
||||||
if ($effectiveLgIdx) {
|
if ($effectiveLgIdx) {
|
||||||
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
|
$lgRow = model(\App\Models\LocalGovernmentModel::class)->find($effectiveLgIdx);
|
||||||
$effectiveLgName = $lgRow ? (string) $lgRow->lg_name : '';
|
$effectiveLgName = $lgRow ? $lgRow->lg_name : null;
|
||||||
}
|
}
|
||||||
|
$userNav = session_user_nav_display();
|
||||||
|
$currentPath = current_nav_request_path();
|
||||||
|
$adminNavTree = get_admin_nav_tree();
|
||||||
|
|
||||||
$adminTree = function_exists('get_admin_nav_tree') ? get_admin_nav_tree() : [];
|
/** DB 링크(mm_link)만 사용. 짧게 적은 항목(menus 등)은 실제 URI(admin/menus)와 맞춰 후보 비교 */
|
||||||
$gov = gov_portal_nav_context(false, $adminTree);
|
$adminNavItemIsCurrent = static function (?string $mmLink) use ($currentPath): bool {
|
||||||
|
return menu_link_matches_request($mmLink, $currentPath, []);
|
||||||
|
};
|
||||||
|
|
||||||
// 관리자 메뉴가 비어 있으면(지자체 미선택 등) 핵심 항목 폴백 노출
|
/** 메뉴가 DB에서 안 쓰일 때만(폴백 상단바) 세그먼트 기반 활성 */
|
||||||
$navItems = $gov['navItems'];
|
$isActive = static function (string $path) use ($uri, $seg3) {
|
||||||
if ($navItems === []) {
|
if ($path === 'admin' || $path === '') return $uri === '';
|
||||||
$mk = static fn (string $name, string $path): array => [
|
if ($path === 'users') return $uri === 'users';
|
||||||
'idx' => 0, 'name' => $name, 'href' => $path, 'url' => base_url($path),
|
if ($path === 'login-history') return $uri === 'access' && $seg3 === 'login-history';
|
||||||
];
|
if ($path === 'approvals') return $uri === 'access' && $seg3 === 'approvals';
|
||||||
$navItems = [
|
if ($path === 'roles') return $uri === 'roles';
|
||||||
['idx' => 0, 'name' => '대시보드', 'href' => 'admin', 'url' => base_url('admin'), 'children' => [], 'hasChildren' => false],
|
if ($path === 'menus') return $uri === 'menus';
|
||||||
['idx' => 0, 'name' => '회원·접근', 'href' => '', 'url' => '', 'hasChildren' => true, 'children' => [
|
if ($path === 'local-governments') return $uri === 'local-governments';
|
||||||
$mk('회원 관리', 'admin/users'), $mk('로그인 이력', 'admin/access/login-history'), $mk('승인 대기', 'admin/access/approvals'),
|
if ($path === 'select-local-government') return $uri === 'select-local-government';
|
||||||
]],
|
if ($path === 'designated-shops') return $uri === 'designated-shops';
|
||||||
['idx' => 0, 'name' => '시스템', 'href' => '', 'url' => '', 'hasChildren' => true, 'children' => [
|
return false;
|
||||||
$mk('역할', 'admin/roles'), $mk('메뉴', 'admin/menus'),
|
};
|
||||||
]],
|
|
||||||
];
|
|
||||||
if ($isSuperAdmin) {
|
|
||||||
$navItems[] = ['idx' => 0, 'name' => '지자체', 'href' => '', 'url' => '', 'hasChildren' => true, 'children' => [
|
|
||||||
$mk('지자체 전환', 'admin/select-local-government'), $mk('지자체 관리', 'admin/local-governments'),
|
|
||||||
]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$navJson = json_encode($navItems, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS);
|
|
||||||
|
|
||||||
$navPartial = [
|
|
||||||
'govNavItems' => $navItems,
|
|
||||||
'govNavJson' => $navJson,
|
|
||||||
'govActiveParentIdx' => $gov['activeParentIdx'],
|
|
||||||
'govCurrentPath' => $gov['currentPath'],
|
|
||||||
'govDashboardAliases' => $gov['dashboardAliases'],
|
|
||||||
'govActiveChildHref' => $gov['currentPath'],
|
|
||||||
];
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko" class="gov-portal-html">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<title><?= esc($title ?? '관리자') ?> - 종량제 시스템</title>
|
<title><?= esc($title ?? '관리자') ?> - 종량제 시스템</title>
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"/>
|
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: { sans: ['Pretendard', '"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
||||||
colors: {
|
colors: {
|
||||||
'system-header': '#ffffff', 'title-bar': '#1a2b4b', 'control-panel': '#f8f9fa',
|
'system-header': '#ffffff',
|
||||||
'btn-search': '#243a5e', 'btn-excel-border': '#28a745', 'btn-excel-text': '#28a745',
|
'title-bar': '#2c3e50',
|
||||||
'btn-print-border': '#ced4da', 'btn-exit': '#d9534f',
|
'control-panel': '#f8f9fa',
|
||||||
|
'btn-search': '#1c4e80',
|
||||||
|
'btn-excel-border': '#28a745',
|
||||||
|
'btn-excel-text': '#28a745',
|
||||||
|
'btn-print-border': '#ced4da',
|
||||||
|
'btn-exit': '#d9534f',
|
||||||
},
|
},
|
||||||
fontSize: { 'xxs': '0.65rem' }
|
fontSize: { 'xxs': '0.65rem' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style data-purpose="table-layout">
|
||||||
<?php include __DIR__ . '/../home/_dashboard_gov_portal_brand_css.php'; ?>
|
.data-table { width: 100%; border-collapse: collapse; font-family: 'Malgun Gothic', 'Noto Sans KR', sans-serif; }
|
||||||
<?php include __DIR__ . '/../home/_dashboard_gov_portal_topnav_css.php'; ?>
|
|
||||||
<?php include __DIR__ . '/../home/_dashboard_gov_portal_chrome_css.php'; ?>
|
|
||||||
.data-table { width: 100%; border-collapse: collapse; font-family: 'Pretendard', '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; }
|
.data-table th, .data-table td { border: 1px solid #ccc; padding: 4px 8px; white-space: nowrap; font-size: 13px; }
|
||||||
.data-table th { background-color: #e9ecef; text-align: center; vertical-align: middle; font-weight: bold; color: #333; }
|
.data-table th { background-color: #e9ecef; text-align: center; vertical-align: middle; font-weight: bold; color: #333; }
|
||||||
.data-table tbody tr:nth-child(even) td { background-color: #f9f9f9; }
|
.data-table tbody tr:nth-child(even) td { background-color: #f9f9f9; }
|
||||||
|
.data-table tbody tr:nth-child(even) { background-color: #f9f9f9; }
|
||||||
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
|
.data-table tbody tr:hover td { background-color: #e6f7ff !important; }
|
||||||
|
.main-content-area { height: calc(100vh - 170px); overflow: auto; }
|
||||||
|
body { overflow: hidden; }
|
||||||
@media print {
|
@media print {
|
||||||
.portal-header, .sidebar, .portal-footer, .no-print, nav.portal-top-nav { display: none !important; }
|
header, footer, .no-print, nav { display: none !important; }
|
||||||
body.gov-portal-shell { background: #fff; display: block; }
|
.main-content-area { height: auto !important; overflow: visible !important; }
|
||||||
.gov-portal-shell .main.work-main { overflow: visible !important; padding: 0 !important; }
|
body { overflow: visible !important; }
|
||||||
|
.bg-title-bar { display: none !important; }
|
||||||
|
.bg-control-panel { break-inside: avoid; }
|
||||||
.print-header { display: block !important; }
|
.print-header { display: block !important; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="gov-portal-shell select-none">
|
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
|
||||||
<header class="portal-header">
|
<header class="relative bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-50">
|
||||||
<div class="portal-header-inner">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<?= view('home/_dashboard_gov_portal_brand', ['brandHref' => base_url('admin')]) ?>
|
<?= view('components/header_brand', ['href' => base_url('admin')]) ?>
|
||||||
<?= view('home/_dashboard_gov_portal_topnav_click', $navPartial) ?>
|
</div>
|
||||||
<div class="portal-header-utils" style="display:flex;align-items:center;gap:.5rem;">
|
<nav class="hidden md:flex gap-5 text-sm font-medium text-gray-600">
|
||||||
<span class="user-line">
|
<?php if (! empty($adminNavTree)): ?>
|
||||||
<?php if ($effectiveLgName !== ''): ?><strong><?= esc($effectiveLgName) ?></strong> · <?php endif; ?>
|
<?php foreach ($adminNavTree as $navItem): ?>
|
||||||
<?= esc($levelName) ?> · <?= esc($mbName) ?>님
|
<?php
|
||||||
|
$hasChildren = ! empty($navItem->children);
|
||||||
|
$parentLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
|
||||||
|
$parentIsCurrent = $adminNavItemIsCurrent($navItem->mm_link ?? null);
|
||||||
|
if (! $parentIsCurrent && $hasChildren) {
|
||||||
|
foreach ($navItem->children as $ch) {
|
||||||
|
if ($adminNavItemIsCurrent($ch->mm_link ?? null)) {
|
||||||
|
$parentIsCurrent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="relative group">
|
||||||
|
<a class="<?= $parentIsCurrent ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>"
|
||||||
|
href="<?= $parentLink !== '' ? base_url($parentLink) : '#' ?>">
|
||||||
|
<?= esc($navItem->mm_name) ?>
|
||||||
|
</a>
|
||||||
|
<?php if ($hasChildren): ?>
|
||||||
|
<?php /* 사이트 메뉴와 동일: 호버 끊김 방지 pt-1, 키보드 포커스, z-index */ ?>
|
||||||
|
<div class="absolute left-0 top-full z-50 hidden pt-1 min-w-[12rem] group-hover:block group-focus-within:block">
|
||||||
|
<div class="bg-white border border-gray-200 rounded shadow-lg py-1">
|
||||||
|
<?php foreach ($navItem->children as $child): ?>
|
||||||
|
<?php
|
||||||
|
$childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
|
||||||
|
$childIsCurrent = $adminNavItemIsCurrent($child->mm_link ?? null);
|
||||||
|
?>
|
||||||
|
<?php if ($childLink !== ''): ?>
|
||||||
|
<a href="<?= base_url($childLink) ?>"
|
||||||
|
class="block px-3 py-1.5 text-sm hover:bg-blue-50 whitespace-nowrap <?= $childIsCurrent ? 'text-blue-700 font-semibold bg-blue-50' : 'text-gray-700' ?>">
|
||||||
|
<?= esc($child->mm_name) ?>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="block px-3 py-1.5 text-sm text-gray-400 cursor-default whitespace-nowrap" title="메뉴 관리에서 링크를 설정해 주세요">
|
||||||
|
<?= esc($child->mm_name) ?>
|
||||||
</span>
|
</span>
|
||||||
<a href="<?= base_url('/') ?>" title="사이트로"
|
|
||||||
style="display:inline-flex;align-items:center;gap:.3rem;padding:.25rem .6rem;border-radius:6px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.3);color:#fff;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap;">
|
|
||||||
<i class="fa-solid fa-house"></i> 사이트
|
|
||||||
</a>
|
|
||||||
<a href="<?= base_url('logout') ?>" title="로그아웃"
|
|
||||||
style="display:inline-flex;align-items:center;gap:.3rem;padding:.25rem .6rem;border-radius:6px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.22);color:#fff;text-decoration:none;font-size:.75rem;font-weight:600;white-space:nowrap;">
|
|
||||||
<i class="fa-solid fa-right-from-bracket"></i> 로그아웃
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="layout">
|
|
||||||
<?= view('home/_dashboard_gov_portal_sidebar', $navPartial) ?>
|
|
||||||
|
|
||||||
<main class="main work-main main-content-area">
|
|
||||||
<?php if (! empty($title)): ?>
|
|
||||||
<h1 class="work-titlebar"><i class="fa-solid fa-gear tb-ico"></i><?= esc($title) ?></h1>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<a class="<?= $isActive('') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin') ?>">대시보드</a>
|
||||||
|
<a class="<?= $isActive('users') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/users') ?>">회원 관리</a>
|
||||||
|
<a class="<?= $isActive('login-history') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/access/login-history') ?>">로그인 이력</a>
|
||||||
|
<a class="<?= $isActive('approvals') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/access/approvals') ?>">승인 대기</a>
|
||||||
|
<a class="<?= $isActive('roles') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/roles') ?>">역할</a>
|
||||||
|
<a class="<?= $isActive('menus') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/menus') ?>">메뉴</a>
|
||||||
|
<?php if ($isSuperAdmin): ?>
|
||||||
|
<a class="<?= $isActive('select-local-government') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/select-local-government') ?>">지자체 전환</a>
|
||||||
|
<a class="<?= $isActive('local-governments') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('admin/local-governments') ?>">지자체</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a class="<?= $isActive('designated-shops') ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>" href="<?= base_url('bag/designated-shops') ?>">지정판매소</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</nav>
|
||||||
|
<?= view('components/header_user_tools', [
|
||||||
|
'userNav' => $userNav,
|
||||||
|
'effectiveLgName' => $effectiveLgName,
|
||||||
|
'showSiteLink' => true,
|
||||||
|
'showAdminLink' => false,
|
||||||
|
]) ?>
|
||||||
|
</header>
|
||||||
|
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||||
|
<?= esc($title ?? '관리자') ?>
|
||||||
|
</div>
|
||||||
<?php if (session()->getFlashdata('success')): ?>
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
<div class="work-flash ok"><?= esc(session()->getFlashdata('success')) ?></div>
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-green-50 text-green-700 text-sm" role="alert"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (session()->getFlashdata('error')): ?>
|
<?php if (session()->getFlashdata('error')): ?>
|
||||||
<div class="work-flash err"><?= esc(session()->getFlashdata('error')) ?></div>
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (session()->getFlashdata('errors')): ?>
|
<?php if (session()->getFlashdata('errors')): ?>
|
||||||
<div class="work-flash err"><?php foreach (session()->getFlashdata('errors') as $err): ?><div><?= esc($err) ?></div><?php endforeach; ?></div>
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm space-y-1" role="alert">
|
||||||
|
<?php foreach (session()->getFlashdata('errors') as $err): ?><p><?= esc($err) ?></p><?php endforeach; ?>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="work-surface">
|
<main class="main-content-area flex-grow bg-white p-4">
|
||||||
<?= $content ?>
|
<?= $content ?>
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 flex items-center justify-between shrink-0">
|
||||||
|
|
||||||
<footer class="portal-footer">
|
|
||||||
<span>종량제 시스템 관리자</span>
|
<span>종량제 시스템 관리자</span>
|
||||||
<span><?= date('Y.m.d (D) H:i') ?></span>
|
<span><?= date('Y.m.d (D) g:i:sA') ?></span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<?= view('home/_dashboard_gov_portal_nav_script_base', $navPartial) ?>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -11,18 +11,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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>
|
<label class="block text-sm font-bold text-gray-700 w-28">소속</label>
|
||||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_category" required>
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_dept_code">
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach (($categories ?? []) as $key => $label): ?>
|
<?php foreach ($deptCodes as $cd): ?>
|
||||||
<option value="<?= esc($key) ?>" <?= old('mg_category') === $key ? 'selected' : '' ?>>
|
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_dept_code') === $cd->cd_code ? 'selected' : '' ?>>
|
||||||
<?= esc($label) ?>
|
<?= esc($cd->cd_name) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="mg_position_code" value="<?= esc(old('mg_position_code', '')) ?>"/>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">직위</label>
|
||||||
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_position_code">
|
||||||
|
<option value="">선택</option>
|
||||||
|
<?php foreach ($positionCodes as $cd): ?>
|
||||||
|
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_position_code') === $cd->cd_code ? 'selected' : '' ?>>
|
||||||
|
<?= esc($cd->cd_name) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
|
||||||
|
|||||||
@@ -11,18 +11,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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>
|
<label class="block text-sm font-bold text-gray-700 w-28">소속</label>
|
||||||
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_category" required>
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_dept_code">
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach (($categories ?? []) as $key => $label): ?>
|
<?php foreach ($deptCodes as $cd): ?>
|
||||||
<option value="<?= esc($key) ?>" <?= old('mg_category', (string) ($item->mg_dept_code ?? '')) === $key ? 'selected' : '' ?>>
|
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_dept_code', $item->mg_dept_code) === $cd->cd_code ? 'selected' : '' ?>>
|
||||||
<?= esc($label) ?>
|
<?= esc($cd->cd_name) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="mg_position_code" value="<?= esc(old('mg_position_code', $item->mg_position_code)) ?>"/>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<label class="block text-sm font-bold text-gray-700 w-28">직위</label>
|
||||||
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="mg_position_code">
|
||||||
|
<option value="">선택</option>
|
||||||
|
<?php foreach ($positionCodes as $cd): ?>
|
||||||
|
<option value="<?= esc($cd->cd_code) ?>" <?= old('mg_position_code', $item->mg_position_code) === $cd->cd_code ? 'selected' : '' ?>>
|
||||||
|
<?= esc($cd->cd_name) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
|
<label class="block text-sm font-bold text-gray-700 w-28">전화</label>
|
||||||
|
|||||||
@@ -8,26 +8,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
|
||||||
<form method="GET" action="<?= mgmt_url('managers') ?>" class="flex flex-wrap items-center gap-2">
|
|
||||||
<label class="text-sm text-gray-600">카테고리</label>
|
|
||||||
<select name="category" class="border border-gray-300 rounded px-2 py-1 text-sm w-44">
|
|
||||||
<option value="">전 체</option>
|
|
||||||
<?php foreach (($categories ?? []) as $key => $label): ?>
|
|
||||||
<option value="<?= esc($key) ?>" <?= ($category ?? '') === $key ? 'selected' : '' ?>><?= esc($label) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
|
||||||
<a href="<?= mgmt_url('managers') ?>" class="text-sm text-gray-500 hover:underline">초기화</a>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-16">번호</th>
|
<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>
|
<th>이메일</th>
|
||||||
@@ -40,13 +28,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->mg_idx) ?></td>
|
<td class="text-center"><?= esc($row->mg_idx) ?></td>
|
||||||
<td class="text-center"><?= esc($row->mg_name) ?></td>
|
<td class="text-center"><?= esc($row->mg_name) ?></td>
|
||||||
<td class="text-center">
|
<td class="text-center"><?= esc($row->mg_dept_code) ?></td>
|
||||||
<?php
|
<td class="text-center"><?= esc($row->mg_position_code) ?></td>
|
||||||
$cat = (string) ($row->mg_dept_code ?? '');
|
|
||||||
$catLabel = $categories[$cat] ?? $cat;
|
|
||||||
echo esc($catLabel);
|
|
||||||
?>
|
|
||||||
</td>
|
|
||||||
<td class="text-center"><?= esc($row->mg_tel) ?></td>
|
<td class="text-center"><?= esc($row->mg_tel) ?></td>
|
||||||
<td class="text-center"><?= esc($row->mg_phone) ?></td>
|
<td class="text-center"><?= esc($row->mg_phone) ?></td>
|
||||||
<td class="text-center"><?= esc($row->mg_email) ?></td>
|
<td class="text-center"><?= esc($row->mg_email) ?></td>
|
||||||
@@ -61,7 +44,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($list)): ?>
|
<?php if (empty($list)): ?>
|
||||||
<tr><td colspan="8" class="text-center text-gray-400 py-4">등록된 데이터가 없습니다.</td></tr>
|
<tr><td colspan="9" class="text-center text-gray-400 py-4">등록된 데이터가 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ $debugMode = (bool) ($debug_mode ?? false);
|
|||||||
$debugInfo = is_array($debug_info ?? null) ? $debug_info : [];
|
$debugInfo = is_array($debug_info ?? null) ? $debug_info : [];
|
||||||
helper('admin');
|
helper('admin');
|
||||||
$adminMenusNavPath = current_nav_request_path();
|
$adminMenusNavPath = current_nav_request_path();
|
||||||
// 사이트 메뉴(mt_code=site)는 업무 URL이 /bag/* — 관리 화면(admin/menus) 맥락으로 해석하면 admin/ 링크가 잘못 선택됨
|
|
||||||
$menuListResolvePath = ($mtCode === 'site') ? 'bag/dashboard' : $adminMenusNavPath;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메뉴 관리 목록용: 저장된 mm_link → 실제 href (외부 http(s) 또는 base_url).
|
* 메뉴 관리 목록용: 저장된 mm_link → 실제 href (외부 http(s) 또는 base_url).
|
||||||
*/
|
*/
|
||||||
$adminMenuListResolveHref = static function (string $rawLink) use ($menuListResolvePath): string {
|
$adminMenuListResolveHref = static function (string $rawLink) use ($adminMenusNavPath): string {
|
||||||
$rawLink = trim($rawLink);
|
$rawLink = trim($rawLink);
|
||||||
if ($rawLink === '') {
|
if ($rawLink === '') {
|
||||||
return '';
|
return '';
|
||||||
@@ -22,7 +20,7 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
|||||||
if (preg_match('#^https?://#i', $rawLink)) {
|
if (preg_match('#^https?://#i', $rawLink)) {
|
||||||
return $rawLink;
|
return $rawLink;
|
||||||
}
|
}
|
||||||
$pathSeg = menu_link_preferred_href_path($rawLink, $menuListResolvePath);
|
$pathSeg = menu_link_preferred_href_path($rawLink, $adminMenusNavPath);
|
||||||
if ($pathSeg === '') {
|
if ($pathSeg === '') {
|
||||||
$pathSeg = normalize_menu_link_for_url($rawLink);
|
$pathSeg = normalize_menu_link_for_url($rawLink);
|
||||||
}
|
}
|
||||||
@@ -157,7 +155,6 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<form action="<?= base_url('admin/menus/delete/' . (int) $row->mm_idx) ?>" method="post" class="inline ml-1" onsubmit="return confirm('이 메뉴를 삭제하시겠습니까?');">
|
<form action="<?= base_url('admin/menus/delete/' . (int) $row->mm_idx) ?>" method="post" class="inline ml-1" onsubmit="return confirm('이 메뉴를 삭제하시겠습니까?');">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="mt_idx" value="<?= $mtIdx ?>"/>
|
|
||||||
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
|
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
@@ -318,10 +315,6 @@ $adminMenuListResolveHref = static function (string $rawLink) use ($menuListReso
|
|||||||
formTitle.textContent = '메뉴 수정';
|
formTitle.textContent = '메뉴 수정';
|
||||||
btnSubmit.textContent = '수정';
|
btnSubmit.textContent = '수정';
|
||||||
btnCancel.style.display = 'inline-block';
|
btnCancel.style.display = 'inline-block';
|
||||||
// 수정 폼이 보이도록 스크롤 최상단으로 이동
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
const sc = document.querySelector('.main-content-area');
|
|
||||||
if (sc) sc.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<label class="block text-sm font-bold text-gray-700 w-32">적용시작일 <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-gray-700 w-32">적용시작일 <span class="text-red-500">*</span></label>
|
||||||
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="pu_start_date" type="date" value="<?= esc(old('pu_start_date', date('Y-m-d'))) ?>" required/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="pu_start_date" type="date" value="<?= esc(old('pu_start_date', $item->pu_start_date)) ?>" required/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
|||||||
@@ -6,20 +6,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 overflow-auto mt-2">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<?php
|
|
||||||
$fieldLabelMap = [
|
|
||||||
'pu_box_per_pack' => '박스당 팩 수',
|
|
||||||
'pu_pack_per_sheet' => '팩당 낱장 수',
|
|
||||||
'pu_start_date' => '적용시작일',
|
|
||||||
'pu_end_date' => '적용종료일',
|
|
||||||
'pu_state' => '상태',
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
<table class="w-full data-table">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-16">번호</th>
|
<th class="w-16">번호</th>
|
||||||
<th>변경 내용</th>
|
<th>변경 필드</th>
|
||||||
<th>이전 값</th>
|
<th>이전 값</th>
|
||||||
<th>변경 값</th>
|
<th>변경 값</th>
|
||||||
<th>변경일시</th>
|
<th>변경일시</th>
|
||||||
@@ -29,7 +20,7 @@
|
|||||||
<?php foreach ($list as $row): ?>
|
<?php foreach ($list as $row): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= esc($row->puh_idx) ?></td>
|
<td class="text-center"><?= esc($row->puh_idx) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($fieldLabelMap[(string) $row->puh_field] ?? $row->puh_field) ?></td>
|
<td class="text-left pl-2"><?= esc($row->puh_field) ?></td>
|
||||||
<td><?= esc($row->puh_old_value) ?></td>
|
<td><?= esc($row->puh_old_value) ?></td>
|
||||||
<td><?= esc($row->puh_new_value) ?></td>
|
<td><?= esc($row->puh_new_value) ?></td>
|
||||||
<td class="text-center"><?= esc($row->puh_changed_at) ?></td>
|
<td class="text-center"><?= esc($row->puh_changed_at) ?></td>
|
||||||
|
|||||||
@@ -35,17 +35,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php
|
<?php foreach ($list as $row): ?>
|
||||||
$startNo = 1;
|
|
||||||
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
|
|
||||||
$currentPage = (int) $pager->getCurrentPage();
|
|
||||||
$perPage = (int) $pager->getPerPage();
|
|
||||||
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<?php foreach (($list ?? []) as $idx => $row): ?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
|
<td class="text-center"><?= esc($row->pu_idx) ?></td>
|
||||||
<td class="text-center font-mono"><?= esc($row->pu_bag_code) ?></td>
|
<td class="text-center font-mono"><?= esc($row->pu_bag_code) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->pu_bag_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->pu_bag_name) ?></td>
|
||||||
<td><?= number_format((int) $row->pu_box_per_pack) ?></td>
|
<td><?= number_format((int) $row->pu_box_per_pack) ?></td>
|
||||||
|
|||||||
@@ -44,17 +44,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php
|
<?php foreach ($list as $row): ?>
|
||||||
$startNo = 1;
|
|
||||||
if (isset($pager) && method_exists($pager, 'getCurrentPage') && method_exists($pager, 'getPerPage')) {
|
|
||||||
$currentPage = (int) $pager->getCurrentPage();
|
|
||||||
$perPage = (int) $pager->getPerPage();
|
|
||||||
$startNo = (($currentPage > 0 ? $currentPage : 1) - 1) * ($perPage > 0 ? $perPage : 20) + 1;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<?php foreach (($list ?? []) as $idx => $row): ?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= (int) $startNo + (int) $idx ?></td>
|
<td class="text-center"><?= esc($row->sa_idx) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->sa_kind ?? '') ?></td>
|
<td class="text-left pl-2"><?= esc($row->sa_kind ?? '') ?></td>
|
||||||
<td class="text-center"><?= esc($row->sa_code ?? '') ?></td>
|
<td class="text-center"><?= esc($row->sa_code ?? '') ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($row->sa_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->sa_name) ?></td>
|
||||||
|
|||||||
@@ -1,140 +1,106 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '일계표']) ?>
|
||||||
declare(strict_types=1);
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
|
||||||
/** @var list<array<string,mixed>> $tableRows */
|
|
||||||
/** @var string $date */
|
|
||||||
/** @var string $monthStart */
|
|
||||||
/** @var int $saIdx */
|
|
||||||
/** @var string $catFilter */
|
|
||||||
/** @var list<object> $agencies */
|
|
||||||
/** @var array<string,string> $catLabels */
|
|
||||||
/** @var bool $hasBsFee */
|
|
||||||
/** @var string $lgName */
|
|
||||||
/** @var string $agencyLabel */
|
|
||||||
/** @var string $catLabelFilter */
|
|
||||||
/** @var list<string> $printExtraLines */
|
|
||||||
|
|
||||||
$exportParams = array_filter([
|
|
||||||
'date' => $date ?? '',
|
|
||||||
'sa_idx' => (int) ($saIdx ?? 0),
|
|
||||||
'cat' => (string) ($catFilter ?? ''),
|
|
||||||
'export' => '1',
|
|
||||||
], static fn ($v): bool => $v !== '' && $v !== null);
|
|
||||||
$excelUrl = mgmt_url('reports/daily-summary?' . http_build_query($exportParams));
|
|
||||||
?>
|
|
||||||
<?= 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">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">일계표</span>
|
<span class="text-sm font-bold text-gray-700">일계표</span>
|
||||||
<div class="flex flex-wrap gap-2">
|
<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>
|
||||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
|
||||||
<a href="<?= esc($excelUrl, 'attr') ?>" class="inline-flex items-center border border-green-600 text-green-700 px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
<section class="p-3 bg-white border-b border-gray-200 no-print">
|
<form method="GET" action="<?= mgmt_url('reports/daily-summary') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<form method="GET" action="<?= mgmt_url('reports/daily-summary') ?>" class="flex flex-wrap items-end gap-3 text-sm">
|
<label class="text-sm text-gray-600">조회일</label>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">조회일자</label>
|
|
||||||
<input type="date" name="date" value="<?= esc($date ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
<input type="date" name="date" value="<?= esc($date ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
</div>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">대행소</label>
|
|
||||||
<select name="sa_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[12rem] max-w-[20rem]">
|
|
||||||
<option value="0">전체</option>
|
|
||||||
<?php foreach ($agencies ?? [] as $agency): ?>
|
|
||||||
<?php $aid = (int) ($agency->sa_idx ?? 0); ?>
|
|
||||||
<option value="<?= esc((string) $aid) ?>" <?= (int) ($saIdx ?? 0) === $aid ? 'selected' : '' ?>>
|
|
||||||
<?= esc(trim((string) ($agency->sa_name ?? ''))) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">구분</label>
|
|
||||||
<select name="cat" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem]">
|
|
||||||
<option value="" <?= ($catFilter ?? '') === '' ? 'selected' : '' ?>>전체</option>
|
|
||||||
<?php foreach (($catLabels ?? []) as $ck => $lab): ?>
|
|
||||||
<option value="<?= esc($ck, 'attr') ?>" <?= ($catFilter ?? '') === $ck ? 'selected' : '' ?>><?= esc($lab) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="p-3 bg-white">
|
<div class="flex gap-4 mt-2">
|
||||||
<style>
|
<!-- 당일 -->
|
||||||
@media print {
|
<div class="flex-1 border border-gray-300 overflow-auto">
|
||||||
.daily-summary-screen-title { display: none !important; }
|
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||||
}
|
<span class="text-sm font-bold text-gray-700">당일 (<?= esc($date ?? '') ?>)</span>
|
||||||
</style>
|
|
||||||
<div class="mb-2 text-center daily-summary-screen-title no-print">
|
|
||||||
<h1 class="text-lg font-bold m-0">일계표</h1>
|
|
||||||
<p class="text-sm text-gray-700 m-1"><?= esc(trim(($lgName ?? '') . ' · 조회일: ' . ($date ?? '') . ' · 대행소: ' . ($agencyLabel ?? '') . ' · 구분: ' . ($catLabelFilter ?? ''))) ?></p>
|
|
||||||
<p class="text-xs text-gray-500 m-0">누계(월): <?= esc(($monthStart ?? '') . ' ~ ' . ($date ?? '')) ?> · (단위: 매 / 원)</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<table class="w-full data-table">
|
||||||
<div class="border border-gray-300 overflow-auto">
|
|
||||||
<table class="w-full data-table text-sm" id="daily-summary-table">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="2" class="align-middle">구분</th>
|
<th>봉투코드</th>
|
||||||
<th rowspan="2" class="align-middle">봉투종류</th>
|
<th>봉투명</th>
|
||||||
<th colspan="4" class="text-center border-l border-gray-300">일계</th>
|
<th>판매수량</th>
|
||||||
<th colspan="4" class="text-center border-l border-gray-300">누계(월)</th>
|
<th>판매금액</th>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="text-right border-l border-gray-300">수량</th>
|
|
||||||
<th class="text-right">판매금액</th>
|
|
||||||
<th class="text-right">수수료</th>
|
|
||||||
<th class="text-right">징수액</th>
|
|
||||||
<th class="text-right border-l border-gray-300">수량</th>
|
|
||||||
<th class="text-right">판매금액</th>
|
|
||||||
<th class="text-right">수수료</th>
|
|
||||||
<th class="text-right">징수액</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php foreach ($tableRows ?? [] as $r): ?>
|
|
||||||
<?php
|
<?php
|
||||||
$kind = (string) ($r['kind'] ?? 'data');
|
$dailySaleQtyTotal = 0;
|
||||||
$trClass = $kind === 'subtotal' ? 'bg-gray-50 font-semibold' : ($kind === 'grand' ? 'bg-amber-50 font-bold' : '');
|
$dailySaleAmountTotal = 0;
|
||||||
$fmtFee = static function (float $v) use ($hasBsFee): string {
|
|
||||||
if (! $hasBsFee) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
return $v != 0.0 ? number_format((int) round($v)) : '';
|
|
||||||
};
|
|
||||||
?>
|
?>
|
||||||
<tr class="<?= esc($trClass, 'attr') ?>">
|
<?php foreach ($daily as $row): ?>
|
||||||
<?php if ($kind === 'grand'): ?>
|
<?php
|
||||||
<td colspan="2" class="text-center"><?= esc((string) ($r['bag_name'] ?? '합 계')) ?></td>
|
$dailySaleQtyTotal += (int) $row->sale_qty;
|
||||||
<?php else: ?>
|
$dailySaleAmountTotal += (int) $row->sale_amount;
|
||||||
<td class="text-left pl-2"><?= esc((string) ($r['cat_label'] ?? '')) ?></td>
|
?>
|
||||||
<td class="text-left pl-2"><?= esc((string) ($r['bag_name'] ?? '')) ?></td>
|
<tr>
|
||||||
<?php endif; ?>
|
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||||
<td class="border-l border-gray-200 tabular-nums"><?= number_format((int) ($r['d_qty'] ?? 0)) ?></td>
|
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||||
<td class="tabular-nums"><?= number_format((int) round((float) ($r['d_amt'] ?? 0))) ?></td>
|
<td><?= number_format((int) $row->sale_qty) ?></td>
|
||||||
<td class="tabular-nums"><?= $fmtFee((float) ($r['d_fee'] ?? 0)) ?></td>
|
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||||
<td class="tabular-nums"><?= number_format((int) round((float) ($r['d_levy'] ?? 0))) ?></td>
|
|
||||||
<td class="border-l border-gray-200 tabular-nums"><?= number_format((int) ($r['m_qty'] ?? 0)) ?></td>
|
|
||||||
<td class="tabular-nums"><?= number_format((int) round((float) ($r['m_amt'] ?? 0))) ?></td>
|
|
||||||
<td class="tabular-nums"><?= $fmtFee((float) ($r['m_fee'] ?? 0)) ?></td>
|
|
||||||
<td class="tabular-nums"><?= number_format((int) round((float) ($r['m_levy'] ?? 0))) ?></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (($tableRows ?? []) === []): ?>
|
<?php if (empty($daily)): ?>
|
||||||
<tr>
|
<tr><td colspan="4" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||||
<td colspan="10" class="text-center text-gray-400 py-6">조회된 데이터가 없습니다.</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tfoot class="bg-gray-50 font-bold text-right">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-center">합계</td>
|
||||||
|
<td><?= number_format($dailySaleQtyTotal) ?></td>
|
||||||
|
<td><?= number_format($dailySaleAmountTotal) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
<!-- 당월 누계 -->
|
||||||
|
<div class="flex-1 border border-gray-300 overflow-auto">
|
||||||
|
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||||
|
<span class="text-sm font-bold text-gray-700">당월 누계 (<?= esc($monthStart ?? '') ?> ~ <?= esc($date ?? '') ?>)</span>
|
||||||
|
</div>
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>봉투코드</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>판매수량</th>
|
||||||
|
<th>판매금액</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php
|
||||||
|
$monthlySaleQtyTotal = 0;
|
||||||
|
$monthlySaleAmountTotal = 0;
|
||||||
|
?>
|
||||||
|
<?php foreach ($monthly as $row): ?>
|
||||||
|
<?php
|
||||||
|
$monthlySaleQtyTotal += (int) $row->sale_qty;
|
||||||
|
$monthlySaleAmountTotal += (int) $row->sale_amount;
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $row->sale_qty) ?></td>
|
||||||
|
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($monthly)): ?>
|
||||||
|
<tr><td colspan="4" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
<tfoot class="bg-gray-50 font-bold text-right">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-center">합계</td>
|
||||||
|
<td><?= number_format($monthlySaleQtyTotal) ?></td>
|
||||||
|
<td><?= number_format($monthlySaleAmountTotal) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,322 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/** @var string $startDate */
|
|
||||||
/** @var string $endDate */
|
|
||||||
/** @var string $writeDate */
|
|
||||||
/** @var bool $searched */
|
|
||||||
/** @var list<string> $headers */
|
|
||||||
/** @var list<list<string>> $displayRows */
|
|
||||||
/** @var int $totalCount */
|
|
||||||
/** @var float $totalSupplyAmount */
|
|
||||||
/** @var float $totalTaxAmount */
|
|
||||||
/** @var int $missingBizCount */
|
|
||||||
/** @var string $lgName */
|
|
||||||
/** @var list<string> $printExtraLines */
|
|
||||||
/** @var list<array{label: string, sheet_name: string, cols: list<int>}> $hometaxPrintPages */
|
|
||||||
/** @var list<int> $hometaxColMinPx */
|
|
||||||
|
|
||||||
helper('admin');
|
|
||||||
|
|
||||||
$baseParams = [
|
|
||||||
'start_date' => $startDate ?? '',
|
|
||||||
'end_date' => $endDate ?? '',
|
|
||||||
'write_date' => $writeDate ?? '',
|
|
||||||
];
|
|
||||||
$searchParams = array_merge($baseParams, ['search' => '1']);
|
|
||||||
$exportParams = array_merge($searchParams, ['export' => '1']);
|
|
||||||
|
|
||||||
$searchUrl = mgmt_url('reports/hometax-export?' . http_build_query($searchParams));
|
|
||||||
$excelUrl = mgmt_url('reports/hometax-export?' . http_build_query($exportParams));
|
|
||||||
|
|
||||||
$totalGrand = (float) ($totalSupplyAmount ?? 0) + (float) ($totalTaxAmount ?? 0);
|
|
||||||
$colCount = max(1, count($headers ?? []));
|
|
||||||
|
|
||||||
/** 홈택스 28열 — 주소·상호·이메일 등 텍스트 열을 넓게 (합계 100%) */
|
|
||||||
$hometaxColWidths = [
|
|
||||||
'4.5%', '4%', '4%', '6%', '3%', '6.5%', '3.5%', '11%', '3%', '3%', '5.5%',
|
|
||||||
'6%', '3%', '6.5%', '3.5%', '11%', '3%', '3%', '5.5%',
|
|
||||||
'4%', '4%', '3%', '5.5%', '3%', '3%', '3.5%', '3.5%', '3.5%',
|
|
||||||
];
|
|
||||||
$hometaxColMinPx = $hometaxColMinPx ?? [];
|
|
||||||
$hometaxPrintPages = $hometaxPrintPages ?? [];
|
|
||||||
|
|
||||||
$hometaxWrapColIdx = [7, 15, 5, 6, 13, 14, 10, 18, 22, 27];
|
|
||||||
$hometaxNumColIdx = [19, 20, 24, 25, 26, 27];
|
|
||||||
|
|
||||||
$hometaxNormalizeColWidths = static function (array $colIndices) use ($hometaxColWidths): array {
|
|
||||||
$sum = 0.0;
|
|
||||||
foreach ($colIndices as $ci) {
|
|
||||||
$sum += (float) str_replace('%', '', (string) ($hometaxColWidths[$ci] ?? '3'));
|
|
||||||
}
|
|
||||||
$normalized = [];
|
|
||||||
foreach ($colIndices as $ci) {
|
|
||||||
$pct = (float) str_replace('%', '', (string) ($hometaxColWidths[$ci] ?? '3'));
|
|
||||||
$normalized[$ci] = ($sum > 0 ? round($pct / $sum * 100, 2) : round(100 / max(1, count($colIndices)), 2)) . '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
$hometaxCellClass = static function (int $ci) use ($hometaxWrapColIdx, $hometaxNumColIdx): string {
|
|
||||||
$class = 'text-left px-1 py-1';
|
|
||||||
if (in_array($ci, $hometaxWrapColIdx, true)) {
|
|
||||||
$class .= ' ht-wrap';
|
|
||||||
}
|
|
||||||
if (in_array($ci, $hometaxNumColIdx, true)) {
|
|
||||||
$class .= ' ht-num';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $class;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<int> $colIndices
|
|
||||||
*/
|
|
||||||
$hometaxRenderTable = static function (
|
|
||||||
array $colIndices,
|
|
||||||
string $tableExtraClass,
|
|
||||||
string $tableId,
|
|
||||||
bool $forPrint
|
|
||||||
) use (
|
|
||||||
$headers,
|
|
||||||
$displayRows,
|
|
||||||
$searched,
|
|
||||||
$colCount,
|
|
||||||
$hometaxColWidths,
|
|
||||||
$hometaxColMinPx,
|
|
||||||
$hometaxCellClass,
|
|
||||||
$hometaxNormalizeColWidths
|
|
||||||
): void {
|
|
||||||
$sliceCount = count($colIndices);
|
|
||||||
$widthsForSet = $hometaxNormalizeColWidths($colIndices);
|
|
||||||
?>
|
|
||||||
<table
|
|
||||||
class="w-full data-table text-xs <?= esc($tableExtraClass, 'attr') ?>"
|
|
||||||
id="<?= esc($tableId, 'attr') ?>"
|
|
||||||
<?= $forPrint ? 'data-hometax-print="1"' : '' ?>
|
|
||||||
>
|
|
||||||
<colgroup>
|
|
||||||
<?php foreach ($colIndices as $ci):
|
|
||||||
$wPct = $widthsForSet[$ci] ?? (string) round(100 / max(1, $sliceCount), 2) . '%';
|
|
||||||
$wPx = (int) ($hometaxColMinPx[$ci] ?? 56);
|
|
||||||
?>
|
|
||||||
<col style="width: <?= esc($wPct, 'attr') ?>;<?= $forPrint ? '' : ' min-width: ' . $wPx . 'px' ?>"/>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<?php foreach ($colIndices as $ci): ?>
|
|
||||||
<th class="<?= esc($hometaxCellClass($ci), 'attr') ?>"><?= esc((string) ($headers[$ci] ?? '')) ?></th>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="text-right">
|
|
||||||
<?php if (! ($searched ?? true)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="<?= (int) $sliceCount ?>" class="text-center text-gray-500 py-8">조회를 건너뛴 상태입니다. <strong>조회</strong>를 눌러 주세요.</td>
|
|
||||||
</tr>
|
|
||||||
<?php elseif (($displayRows ?? []) === []): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="<?= (int) $sliceCount ?>" class="text-center text-gray-500 py-8">조회된 판매 내역이 없습니다.</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($displayRows as $row): ?>
|
|
||||||
<tr>
|
|
||||||
<?php foreach ($colIndices as $ci):
|
|
||||||
$tdClass = 'tabular-nums text-left px-1 py-0.5 border-t border-gray-100 ' . $hometaxCellClass($ci);
|
|
||||||
?>
|
|
||||||
<td class="<?= esc(trim($tdClass), 'attr') ?>"><?= esc((string) ($row[$ci] ?? '')) ?></td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<?php
|
|
||||||
};
|
|
||||||
?>
|
|
||||||
<?= view('components/print_header', [
|
|
||||||
'printTitle' => '홈택스 처리',
|
|
||||||
'printExtraLines' => $printExtraLines ?? [],
|
|
||||||
]) ?>
|
|
||||||
|
|
||||||
<section class="border-b border-gray-300 p-3 shrink-0 bg-control-panel no-print">
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
|
|
||||||
<h1 class="text-base font-bold text-gray-800">홈택스 처리</h1>
|
|
||||||
<div class="flex flex-wrap gap-2 items-center">
|
|
||||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
|
||||||
<a href="<?= esc($excelUrl, 'attr') ?>" class="inline-flex items-center border border-green-600 text-green-700 px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="get" action="<?= esc(mgmt_url('reports/hometax-export'), 'attr') ?>" id="hometax-process-form" class="flex flex-wrap items-end gap-3 text-sm">
|
|
||||||
<input type="hidden" name="search" value="1"/>
|
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">판매일자</label>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
|
||||||
<span class="text-gray-500">~</span>
|
|
||||||
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">작성일자</label>
|
|
||||||
<input type="date" name="write_date" value="<?= esc($writeDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
|
||||||
</div>
|
|
||||||
<div class="pt-5">
|
|
||||||
<button type="submit" class="border border-blue-600 bg-blue-50 text-blue-800 px-4 py-1 rounded-sm text-sm font-medium hover:bg-blue-100 transition">조회</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="p-3 bg-white border-b border-gray-200 hometax-report-section">
|
|
||||||
<style>
|
|
||||||
.hometax-print-only { display: none; }
|
|
||||||
@media print {
|
|
||||||
@page {
|
|
||||||
size: A4 landscape;
|
|
||||||
margin: 4mm 5mm;
|
|
||||||
}
|
|
||||||
.hometax-report-section {
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
.hometax-screen-only {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.hometax-print-only {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
.hometax-print-page {
|
|
||||||
page-break-after: always;
|
|
||||||
break-after: page;
|
|
||||||
}
|
|
||||||
.hometax-print-page:last-child {
|
|
||||||
page-break-after: auto;
|
|
||||||
break-after: auto;
|
|
||||||
}
|
|
||||||
.hometax-print-page-label {
|
|
||||||
font-size: 8pt;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 4px;
|
|
||||||
}
|
|
||||||
.hometax-print-scroll {
|
|
||||||
overflow: visible !important;
|
|
||||||
border: 1px solid #333 !important;
|
|
||||||
}
|
|
||||||
.hometax-print-table {
|
|
||||||
font-size: 8pt !important;
|
|
||||||
width: 100% !important;
|
|
||||||
table-layout: fixed !important;
|
|
||||||
}
|
|
||||||
.hometax-print-table th,
|
|
||||||
.hometax-print-table td {
|
|
||||||
padding: 3px 5px !important;
|
|
||||||
white-space: normal !important;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
line-height: 1.3;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
.hometax-print-table th {
|
|
||||||
font-size: 7.5pt !important;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.hometax-print-table .ht-num {
|
|
||||||
white-space: nowrap !important;
|
|
||||||
word-break: normal !important;
|
|
||||||
}
|
|
||||||
.hometax-print-table thead {
|
|
||||||
display: table-header-group;
|
|
||||||
}
|
|
||||||
.hometax-print-table tr {
|
|
||||||
page-break-inside: avoid;
|
|
||||||
break-inside: avoid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen {
|
|
||||||
#hometax-result-table {
|
|
||||||
width: max(100%, 4200px);
|
|
||||||
min-width: 4200px;
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
#hometax-result-table th,
|
|
||||||
#hometax-result-table td {
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 4px 6px !important;
|
|
||||||
}
|
|
||||||
#hometax-result-table .ht-wrap {
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
max-width: 220px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="text-sm font-semibold text-gray-700 mb-2 no-print">조회결과</div>
|
|
||||||
<div class="hometax-screen-only hometax-scroll-wrap overflow-x-auto border border-gray-300" style="max-width: 100%;">
|
|
||||||
<?php
|
|
||||||
$hometaxRenderTable(
|
|
||||||
range(0, max(0, $colCount - 1)),
|
|
||||||
'',
|
|
||||||
'hometax-result-table',
|
|
||||||
false
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hometax-print-only" aria-hidden="true">
|
|
||||||
<?php foreach ($hometaxPrintPages as $ppi => $page):
|
|
||||||
$pageCols = $page['cols'];
|
|
||||||
if ($pageCols === []) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<section class="hometax-print-page">
|
|
||||||
<p class="hometax-print-page-label"><?= esc((string) ($page['label'] ?? '')) ?></p>
|
|
||||||
<div class="hometax-print-scroll">
|
|
||||||
<?php
|
|
||||||
$hometaxRenderTable(
|
|
||||||
$pageCols,
|
|
||||||
'hometax-print-table',
|
|
||||||
'hometax-print-table-' . (int) $ppi,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap gap-6 text-sm border-t border-gray-200 pt-3 no-print">
|
|
||||||
<div><span class="text-gray-600">총 건수</span> <strong class="tabular-nums"><?= (int) ($totalCount ?? 0) ?></strong> 건</div>
|
|
||||||
<div><span class="text-gray-600">총 금액</span> <strong class="tabular-nums"><?= esc(number_format((int) round($totalGrand))) ?></strong> 원 <span class="text-gray-400 text-xs">(공급가액+세액)</span></div>
|
|
||||||
<div><span class="text-gray-600">사업자등록번호 없음</span> <strong class="tabular-nums text-amber-800"><?= (int) ($missingBizCount ?? 0) ?></strong> 건</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2 text-xs text-gray-500 no-print print:hidden">
|
|
||||||
인쇄·엑셀저장은 동일하게 2쪽 열 구성입니다(1쪽: 공급자·공급받는자, 2쪽: 금액·품목). 요약·결재란은 인쇄용 헤더에 포함됩니다.
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style media="print">
|
|
||||||
.no-print { display: none !important; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
let savedTitle = document.title;
|
|
||||||
function stamp() {
|
|
||||||
const d = new Date();
|
|
||||||
const p = (n) => String(n).padStart(2, '0');
|
|
||||||
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '_' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
|
|
||||||
}
|
|
||||||
window.addEventListener('beforeprint', function () {
|
|
||||||
savedTitle = document.title;
|
|
||||||
document.title = '홈택스처리_' + stamp();
|
|
||||||
});
|
|
||||||
window.addEventListener('afterprint', function () {
|
|
||||||
document.title = savedTitle;
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@@ -1,218 +1,99 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => 'LOT 수불 조회']) ?>
|
||||||
$barcode = (string) ($barcode ?? '');
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
$lotNo = (string) ($lotNo ?? '');
|
|
||||||
$result = is_array($result ?? null) ? $result : [];
|
|
||||||
$queried = (bool) ($queried ?? false);
|
|
||||||
|
|
||||||
$ok = (bool) ($result['ok'] ?? false);
|
|
||||||
$message = (string) ($result['message'] ?? '');
|
|
||||||
$rows = is_array($result['rows'] ?? null) ? $result['rows'] : [];
|
|
||||||
$unit = (string) ($result['unit'] ?? '');
|
|
||||||
$bagName = (string) ($result['bag_name'] ?? '');
|
|
||||||
$bagCode = (string) ($result['bag_code'] ?? '');
|
|
||||||
$lotLabel = (string) ($result['lot_no'] ?? $lotNo);
|
|
||||||
$qtyBox = (int) ($result['qty_box'] ?? 0);
|
|
||||||
$qtyPack = (int) ($result['qty_pack'] ?? 0);
|
|
||||||
$qtySheet = (int) ($result['qty_sheet'] ?? 0);
|
|
||||||
$testSamples = is_array($testSamples ?? null) ? $testSamples : [];
|
|
||||||
|
|
||||||
$printExtra = [];
|
|
||||||
if ($queried && $barcode !== '') {
|
|
||||||
$printExtra[] = '봉투번호(바코드): ' . $barcode;
|
|
||||||
}
|
|
||||||
if ($bagName !== '' || $bagCode !== '') {
|
|
||||||
$printExtra[] = '품목: ' . trim($bagName . ($bagCode !== '' ? ' (' . $bagCode . ')' : ''));
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<?= view('components/print_header', [
|
|
||||||
'printTitle' => 'LOT 수불 조회',
|
|
||||||
'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">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">LOT 수불 조회</span>
|
<span class="text-sm font-bold text-gray-700">LOT 수불 조회</span>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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>
|
||||||
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print text-sm">
|
<form method="GET" action="<?= mgmt_url('reports/lot-flow') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<form method="get" action="<?= mgmt_url('reports/lot-flow') ?>" class="flex flex-wrap items-end gap-x-4 gap-y-2">
|
<label class="text-sm text-gray-600">LOT 번호</label>
|
||||||
<input type="hidden" name="search" value="1"/>
|
<input type="text" name="lot_no" value="<?= esc($lotNo ?? '') ?>" placeholder="LOT-YYYYMMDD-XXXXXX" class="border border-gray-300 rounded px-2 py-1 text-sm w-64"/>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<label class="font-bold text-gray-700 whitespace-nowrap">봉투번호</label>
|
|
||||||
<input type="text" name="barcode" id="lot-flow-barcode-input" value="<?= esc($barcode) ?>"
|
|
||||||
placeholder="바코드·팩·박스·낱장 코드 입력"
|
|
||||||
class="border border-gray-300 rounded px-2 py-1 w-80 font-mono text-sm" autocomplete="off"/>
|
|
||||||
<span class="text-gray-500 text-xs">(바코드 스캔 = 번호 직접 입력)</span>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
|
||||||
</form>
|
</form>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
|
||||||
낱장 번호 조회 시 <strong>해당 장(바코드)의 판매·반품</strong>만 표시합니다. 팩·박스·LOT 조회는 해당 단위 이력입니다.
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<?php if ($queried && ! $ok && $message !== ''): ?>
|
<?php if ($lotNo !== '' && $order): ?>
|
||||||
<div class="m-2 p-3 border border-amber-300 bg-amber-50 text-sm text-amber-900 no-print">
|
<!-- 발주 정보 -->
|
||||||
<?= esc($message) ?>
|
<div class="border border-gray-300 p-3 mt-2 bg-gray-50">
|
||||||
|
<h3 class="text-sm font-bold text-gray-700 mb-2">발주 정보</h3>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
|
||||||
|
<div><span class="text-gray-500">LOT번호:</span> <span class="font-mono"><?= esc($order->bo_lot_no) ?></span></div>
|
||||||
|
<div><span class="text-gray-500">발주일:</span> <?= esc($order->bo_order_date) ?></div>
|
||||||
|
<div><span class="text-gray-500">상태:</span>
|
||||||
|
<?php $statusMap = ['normal' => '정상', 'cancelled' => '취소', 'deleted' => '삭제']; ?>
|
||||||
|
<?= esc($statusMap[$order->bo_status] ?? $order->bo_status) ?>
|
||||||
|
</div>
|
||||||
|
<div><span class="text-gray-500">등록일:</span> <?= esc($order->bo_regdate) ?></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (! $queried): ?>
|
<!-- 발주 품목 -->
|
||||||
<div class="m-2 p-3 border border-blue-200 bg-blue-50 text-sm text-blue-900 no-print">
|
<h3 class="text-sm font-bold text-gray-700 mt-3 mb-1">발주 품목</h3>
|
||||||
봉투번호(바코드)를 입력한 뒤 <strong>조회</strong>를 눌러 주세요.
|
<div class="border border-gray-300 overflow-auto">
|
||||||
</div>
|
<table class="w-full data-table">
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<section class="mx-2 mb-2 border border-amber-400 bg-amber-50/80 rounded-sm no-print" id="lot-flow-test-samples">
|
|
||||||
<div class="px-3 py-2 border-b border-amber-300 bg-amber-100/80">
|
|
||||||
<strong class="text-amber-950 text-sm">[개발용 임시] 등록·조회 가능 봉투번호 샘플</strong>
|
|
||||||
<span class="text-xs text-amber-800 ml-2">행 클릭 → 봉투번호 입력 후 조회 · 현재 지자체 DB 기준</span>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-auto max-h-48">
|
|
||||||
<table class="w-full text-xs data-table">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-14 text-center">구분</th>
|
<th>봉투코드</th>
|
||||||
<th>봉투번호(입력값)</th>
|
<th>봉투명</th>
|
||||||
<th class="w-28">품목</th>
|
<th>발주수량(박스)</th>
|
||||||
<th class="w-24">LOT</th>
|
<th>발주수량(매)</th>
|
||||||
<th class="w-14 text-center">상태</th>
|
<th>단가</th>
|
||||||
<th class="w-32">비고</th>
|
<th>금액</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="text-right">
|
||||||
<?php if ($testSamples === []): ?>
|
<?php foreach ($items as $item): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center text-gray-500 py-4">
|
<td class="text-center font-mono"><?= esc($item->boi_bag_code) ?></td>
|
||||||
<code>bag_receiving_pack_code</code> 데이터가 없습니다. 입고 처리 후 표시됩니다.
|
<td class="text-left pl-2"><?= esc($item->boi_bag_name) ?></td>
|
||||||
</td>
|
<td><?= number_format((int) $item->boi_qty_box) ?></td>
|
||||||
</tr>
|
<td><?= number_format((int) $item->boi_qty_sheet) ?></td>
|
||||||
<?php endif; ?>
|
<td><?= number_format((int) $item->boi_unit_price) ?></td>
|
||||||
<?php foreach ($testSamples as $sample): ?>
|
<td><?= number_format((int) $item->boi_amount) ?></td>
|
||||||
<tr class="lot-flow-sample-row cursor-pointer hover:bg-amber-100"
|
|
||||||
data-code="<?= esc((string) ($sample['code'] ?? ''), 'attr') ?>"
|
|
||||||
title="클릭하여 조회">
|
|
||||||
<td class="text-center"><?= esc((string) ($sample['kind'] ?? '')) ?></td>
|
|
||||||
<td class="font-mono text-left pl-2 break-all"><?= esc((string) ($sample['code'] ?? '')) ?></td>
|
|
||||||
<td class="text-left pl-1 truncate max-w-[7rem]" title="<?= esc((string) ($sample['bag_name'] ?? ''), 'attr') ?>">
|
|
||||||
<?= esc((string) ($sample['bag_name'] ?? '')) ?>
|
|
||||||
</td>
|
|
||||||
<td class="font-mono text-left pl-1 truncate max-w-[6rem]"><?= esc((string) ($sample['lot_no'] ?? '')) ?></td>
|
|
||||||
<td class="text-center"><?= esc((string) ($sample['state'] ?? '')) ?></td>
|
|
||||||
<td class="text-gray-600 pl-1"><?= esc((string) ($sample['hint'] ?? '')) ?></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($items)): ?>
|
||||||
|
<tr><td colspan="6" class="text-center text-gray-400 py-4">품목이 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="lot-flow-layout m-2 flex flex-col lg:flex-row gap-3 min-h-[320px]">
|
<!-- 입고 내역 -->
|
||||||
<!-- 좌: 품목·단위 요약 (레거시 BOX/PACK/낱장) -->
|
<h3 class="text-sm font-bold text-gray-700 mt-3 mb-1">입고 내역</h3>
|
||||||
<div class="lot-flow-summary border border-gray-300 bg-gray-50 p-3 lg:w-64 shrink-0">
|
<div class="border border-gray-300 overflow-auto">
|
||||||
<h3 class="text-sm font-bold text-gray-700 mb-2">봉투 정보</h3>
|
<table class="w-full data-table">
|
||||||
<?php if ($ok): ?>
|
<thead>
|
||||||
<dl class="text-sm space-y-1.5">
|
<tr>
|
||||||
<div><dt class="text-gray-500 inline">품목</dt>
|
<th>입고일</th>
|
||||||
<dd class="font-medium"><?= esc($bagName !== '' ? $bagName : '-') ?></dd></div>
|
<th>봉투코드</th>
|
||||||
<?php if ($bagCode !== ''): ?>
|
<th>봉투명</th>
|
||||||
<div><dt class="text-gray-500 inline">코드</dt>
|
<th>입고수량(박스)</th>
|
||||||
<dd class="font-mono text-xs"><?= esc($bagCode) ?></dd></div>
|
<th>입고수량(매)</th>
|
||||||
|
<th>납품자</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php foreach ($receivings as $recv): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><?= esc($recv->br_receive_date) ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($recv->br_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($recv->br_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $recv->br_qty_box) ?></td>
|
||||||
|
<td><?= number_format((int) $recv->br_qty_sheet) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($recv->br_sender_name ?? '') ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($receivings)): ?>
|
||||||
|
<tr><td colspan="6" class="text-center text-gray-400 py-4">입고 내역이 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($lotLabel !== ''): ?>
|
</tbody>
|
||||||
<div><dt class="text-gray-500 inline">LOT</dt>
|
</table>
|
||||||
<dd class="font-mono text-xs break-all"><?= esc($lotLabel) ?></dd></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div><dt class="text-gray-500 inline">봉투번호</dt>
|
|
||||||
<dd class="font-mono text-xs break-all"><?= esc($barcode !== '' ? $barcode : '-') ?></dd></div>
|
|
||||||
<?php if ($unit !== ''): ?>
|
|
||||||
<div><dt class="text-gray-500 inline">조회단위</dt>
|
|
||||||
<dd><?= esc($unit) ?></dd></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</dl>
|
|
||||||
<div class="grid grid-cols-3 gap-2 mt-4 text-center text-xs">
|
|
||||||
<div class="border border-gray-300 bg-white rounded p-2">
|
|
||||||
<div class="text-gray-500 font-bold">BOX</div>
|
|
||||||
<div class="text-lg font-semibold tabular-nums"><?= $qtyBox > 0 ? number_format($qtyBox) : '—' ?></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-gray-300 bg-white rounded p-2">
|
|
||||||
<div class="text-gray-500 font-bold">PACK</div>
|
<?php elseif ($lotNo !== '' && !$order): ?>
|
||||||
<div class="text-lg font-semibold tabular-nums"><?= $qtyPack > 0 ? number_format($qtyPack) : '—' ?></div>
|
<div class="border border-gray-300 p-4 mt-2 text-center text-gray-400">해당 LOT 번호의 발주를 찾을 수 없습니다.</div>
|
||||||
</div>
|
|
||||||
<div class="border border-gray-300 bg-white rounded p-2">
|
|
||||||
<div class="text-gray-500 font-bold">낱장</div>
|
|
||||||
<div class="text-lg font-semibold tabular-nums"><?= $qtySheet > 0 ? number_format($qtySheet) : '—' ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php elseif ($queried): ?>
|
|
||||||
<p class="text-sm text-gray-500">조회 결과 없음</p>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p class="text-sm text-gray-400">조회 후 표시</p>
|
<div class="border border-gray-300 p-4 mt-2 text-center text-gray-400">LOT 번호를 입력하고 조회해 주세요.</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 우: LOT 수불 현황 -->
|
|
||||||
<div class="lot-flow-table-wrap flex-1 border border-gray-300 flex flex-col min-w-0">
|
|
||||||
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
|
||||||
<span class="text-sm font-bold text-gray-700">LOT 수불 현황</span>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-auto flex-1">
|
|
||||||
<table class="w-full data-table text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="w-28">일자</th>
|
|
||||||
<th>입출고처</th>
|
|
||||||
<th class="w-24 text-center">구분</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if ($queried && $ok && $rows === []): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="3" class="text-center text-gray-500 py-8">수불 이력이 없습니다.</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (! $queried): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="3" class="text-center text-gray-400 py-8">봉투번호 입력 후 조회</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php foreach ($rows as $row): ?>
|
|
||||||
<tr>
|
|
||||||
<td class="text-center"><?= esc((string) ($row['flow_date'] ?? '')) ?></td>
|
|
||||||
<td class="text-left pl-2"><?= esc((string) ($row['counterparty'] ?? '')) ?></td>
|
|
||||||
<td class="text-center"><?= esc((string) ($row['flow_type'] ?? '')) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const input = document.getElementById('lot-flow-barcode-input');
|
|
||||||
const form = input?.closest('form');
|
|
||||||
document.querySelectorAll('.lot-flow-sample-row').forEach((row) => {
|
|
||||||
row.addEventListener('click', () => {
|
|
||||||
const code = row.getAttribute('data-code') || '';
|
|
||||||
if (!input || !form || code === '') return;
|
|
||||||
input.value = code;
|
|
||||||
form.submit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@media print {
|
|
||||||
.no-print { display: none !important; }
|
|
||||||
#lot-flow-test-samples { display: none !important; }
|
|
||||||
.lot-flow-layout { margin: 0; flex-direction: row; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,369 +1,84 @@
|
|||||||
<?= view('components/print_header', ['printTitle' => '기타 입출고']) ?>
|
<?= view('components/print_header', ['printTitle' => '기타 입출고']) ?>
|
||||||
<?php
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
$filters = is_array($filters ?? null) ? $filters : [];
|
|
||||||
$flowYear = (string) ($filters['flow_y'] ?? '');
|
|
||||||
$flowMonthNum = (string) ($filters['flow_m'] ?? '');
|
|
||||||
$dateYearMin = (int) ($dateYearMin ?? ((int) date('Y') - 12));
|
|
||||||
$dateYearMax = (int) ($dateYearMax ?? ((int) date('Y') + 2));
|
|
||||||
$bagCodeFilter = (string) ($filters['bag_code'] ?? '');
|
|
||||||
$bagKind = (string) ($filters['bag_kind'] ?? '');
|
|
||||||
$bagCancelOnly = ! empty($filters['bag_cancel']);
|
|
||||||
$selKey = (string) ($filters['sel_key'] ?? '');
|
|
||||||
$selectedGroup = is_array($selectedGroup ?? null) ? $selectedGroup : null;
|
|
||||||
$detailLines = is_array($detailLines ?? null) ? $detailLines : [];
|
|
||||||
$groupList = is_array($groupList ?? null) ? $groupList : [];
|
|
||||||
$packagingMap = is_array($packagingMap ?? null) ? $packagingMap : [];
|
|
||||||
$bagKindOptions = is_array($bagKindOptions ?? null) ? $bagKindOptions : [];
|
|
||||||
$bagCodes = is_array($bagCodes ?? null) ? $bagCodes : [];
|
|
||||||
|
|
||||||
$miscFlowListUrl = static function (array $extra = []) use ($filters): string {
|
|
||||||
$qs = array_merge($filters, $extra);
|
|
||||||
unset($qs['sel_key']);
|
|
||||||
if (isset($extra['sel_key'])) {
|
|
||||||
$qs['sel_key'] = $extra['sel_key'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return mgmt_url('reports/misc-flow') . ($qs !== [] ? '?' . http_build_query($qs) : '');
|
|
||||||
};
|
|
||||||
|
|
||||||
$detailTotalQty = 0;
|
|
||||||
foreach ($detailLines as $line) {
|
|
||||||
$detailTotalQty += (int) ($line->bmf_qty ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$registerDate = $selectedGroup ? (string) ($selectedGroup['date'] ?? date('Y-m-d')) : date('Y-m-d');
|
|
||||||
$registerType = $selectedGroup ? (string) ($selectedGroup['type'] ?? 'in') : 'in';
|
|
||||||
$registerReason = $selectedGroup ? (string) ($selectedGroup['reason'] ?? '') : '';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<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">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">기타 입출고</span>
|
<span class="text-sm font-bold text-gray-700">기타 입출고</span>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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>
|
||||||
<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="<?= esc(work_area_home_url()) ?>" class="border border-gray-400 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">종료</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<?php if (!($tableExists ?? false)): ?>
|
<?php if (!($tableExists ?? false)): ?>
|
||||||
<div class="border border-orange-300 bg-orange-50 p-3 m-2 text-sm text-orange-700 no-print">
|
<div class="border border-orange-300 bg-orange-50 p-3 mt-2 text-sm text-orange-700">
|
||||||
bag_misc_flow 테이블이 생성되지 않았습니다. <code>writable/database/bag_misc_flow_tables.sql</code>을 실행해 주세요.
|
bag_misc_flow 테이블이 생성되지 않았습니다. <code>writable/database/bag_misc_flow_tables.sql</code>을 실행해 주세요.
|
||||||
</div>
|
</div>
|
||||||
<?php elseif (($totalRowsForLg ?? 0) === 0): ?>
|
|
||||||
<div class="border border-blue-200 bg-blue-50 p-3 m-2 text-sm text-blue-900 no-print">
|
|
||||||
선택한 지자체에 등록된 <strong>기타 입출고</strong> 데이터가 없습니다. 아래 <strong>품목 등록</strong>으로 첫 건을 넣으면 좌측 리스트에 표시됩니다.
|
|
||||||
</div>
|
|
||||||
<?php elseif (($groupList ?? []) === [] && ($fetchedRowCount ?? 0) === 0 && (($flowYear ?? '') !== '' || ($flowMonthNum ?? '') !== '' || ($bagCodeFilter ?? '') !== '' || ($bagKind ?? '') !== '' || ! empty($filters['bag_cancel']))): ?>
|
|
||||||
<div class="border border-amber-200 bg-amber-50 p-3 m-2 text-sm text-amber-900 no-print">
|
|
||||||
조회 조건(수불 년월·봉투코드·구분 등)에 맞는 내역이 없습니다. <strong>수불 년월을 「전체」</strong>로 두거나 조건을 넓혀 다시 조회해 주세요.
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (session()->getFlashdata('success')): ?>
|
<!-- 등록 폼 -->
|
||||||
<div class="mx-2 mt-2 border border-green-300 bg-green-50 text-green-800 text-sm px-3 py-2 no-print"><?= esc((string) session()->getFlashdata('success')) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (session()->getFlashdata('error')): ?>
|
|
||||||
<div class="mx-2 mt-2 border border-red-300 bg-red-50 text-red-800 text-sm px-3 py-2 no-print"><?= esc((string) session()->getFlashdata('error')) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- 조회 조건 (레거시: 수불 년월, 봉투코드, 봉투 취소, 구분, 조회) -->
|
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||||
<form method="get" action="<?= mgmt_url('reports/misc-flow') ?>" class="flex flex-wrap items-end gap-2 text-sm">
|
<form method="POST" action="<?= mgmt_url('reports/misc-flow') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<span class="font-bold text-gray-700 whitespace-nowrap">수불 년월</span>
|
|
||||||
<select name="flow_y" class="border border-gray-300 rounded px-2 py-1 min-w-[5.5rem]" aria-label="수불 년도">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<?php for ($yy = $dateYearMax; $yy >= $dateYearMin; $yy--): ?>
|
|
||||||
<option value="<?= $yy ?>" <?= $flowYear === (string) $yy ? 'selected' : '' ?>><?= $yy ?>년</option>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</select>
|
|
||||||
<select name="flow_m" class="border border-gray-300 rounded px-2 py-1 min-w-[4.5rem]" aria-label="수불 월" <?= $flowYear === '' ? 'disabled' : '' ?>>
|
|
||||||
<option value=""><?= $flowYear === '' ? '—' : '전체' ?></option>
|
|
||||||
<?php for ($mi = 1; $mi <= 12; $mi++): ?>
|
|
||||||
<option value="<?= $mi ?>" <?= $flowMonthNum !== '' && (int) $flowMonthNum === $mi ? 'selected' : '' ?>><?= $mi ?>월</option>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label class="font-bold text-gray-700 whitespace-nowrap">봉투코드</label>
|
|
||||||
<input type="text" name="bag_code" value="<?= esc($bagCodeFilter) ?>" placeholder="코드 일부"
|
|
||||||
class="border border-gray-300 rounded px-2 py-1 w-28 font-mono"/>
|
|
||||||
|
|
||||||
<label class="inline-flex items-center gap-1 text-gray-700 whitespace-nowrap">
|
|
||||||
<input type="checkbox" name="bag_cancel" value="1" <?= $bagCancelOnly ? 'checked' : '' ?>/>
|
|
||||||
봉투 취소
|
|
||||||
</label>
|
|
||||||
<span class="text-xs text-gray-500 hidden sm:inline" title="출고 건만 조회">(출고만)</span>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
|
||||||
<a href="<?= mgmt_url('reports/misc-flow') ?>" class="text-gray-500 hover:text-gray-800 px-2 py-1">초기화</a>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-4 gap-2 p-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-[520px]">
|
|
||||||
<table class="w-full data-table text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="w-24">수불일자</th>
|
|
||||||
<th class="w-16">수량</th>
|
|
||||||
<th class="w-14">구분</th>
|
|
||||||
<th>메모</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if ($groupList !== []): ?>
|
|
||||||
<?php foreach ($groupList as $grp): ?>
|
|
||||||
<?php
|
|
||||||
$key = (string) ($grp['key'] ?? '');
|
|
||||||
$isSelected = $key !== '' && $key === $selKey;
|
|
||||||
$listUrl = $miscFlowListUrl(['sel_key' => $key]);
|
|
||||||
?>
|
|
||||||
<tr
|
|
||||||
class="<?= $isSelected ? 'bg-blue-100 font-semibold' : '' ?> cursor-pointer hover:bg-blue-50"
|
|
||||||
onclick="window.location.href='<?= esc($listUrl, 'attr') ?>'"
|
|
||||||
>
|
|
||||||
<td class="text-center <?= $isSelected ? 'border-l-4 border-blue-600' : '' ?>"><?= esc((string) ($grp['date'] ?? '')) ?></td>
|
|
||||||
<td class="text-right pr-2"><?= number_format((int) ($grp['totalQty'] ?? 0)) ?></td>
|
|
||||||
<td class="text-center">
|
|
||||||
<?php if ((string) ($grp['type'] ?? '') === 'in'): ?>
|
|
||||||
<span class="text-blue-700">입고</span>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="text-red-700">출고</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td class="text-left pl-2 truncate max-w-[8rem]" title="<?= esc((string) ($grp['reason'] ?? '')) ?>"><?= esc((string) ($grp['reason'] ?? '')) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<tr><td colspan="4" class="text-center text-gray-400 py-6">
|
|
||||||
<?php if (($totalRowsForLg ?? 0) === 0): ?>
|
|
||||||
등록된 기타 입출고가 없습니다.
|
|
||||||
<?php elseif (($fetchedRowCount ?? 0) === 0): ?>
|
|
||||||
선택한 기간·조건에 해당하는 내역이 없습니다.
|
|
||||||
<?php else: ?>
|
|
||||||
조회 결과가 없습니다.
|
|
||||||
<?php endif; ?>
|
|
||||||
</td></tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 우측: 입출고 정보 + 봉투 코드 + 등록 -->
|
|
||||||
<div class="xl:col-span-3 space-y-2">
|
|
||||||
<form method="post" action="<?= mgmt_url('reports/misc-flow/delete') ?>" id="misc-flow-delete-form" class="no-print">
|
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="flow_y" value="<?= esc($flowYear) ?>"/>
|
<label class="text-sm text-gray-600">구분</label>
|
||||||
<input type="hidden" name="flow_m" value="<?= esc($flowMonthNum) ?>"/>
|
<select name="bmf_type" class="border border-gray-300 rounded px-2 py-1 text-sm" required>
|
||||||
<input type="hidden" name="bag_code" value="<?= esc($bagCodeFilter) ?>"/>
|
<option value="in">입고</option>
|
||||||
<input type="hidden" name="bag_kind" value="<?= esc($bagKind) ?>"/>
|
<option value="out">출고</option>
|
||||||
<?php if ($bagCancelOnly): ?><input type="hidden" name="bag_cancel" value="1"/><?php endif; ?>
|
</select>
|
||||||
<input type="hidden" name="sel_key" value="<?= esc($selKey) ?>"/>
|
<label class="text-sm text-gray-600">봉투</label>
|
||||||
<div class="flex flex-wrap gap-2 mb-1">
|
<select name="bmf_bag_code" class="border border-gray-300 rounded px-2 py-1 text-sm" required>
|
||||||
<button type="submit" class="bg-red-600 text-white px-4 py-1 rounded-sm text-sm disabled:opacity-40"
|
<option value="">선택</option>
|
||||||
<?= $selKey === '' ? 'disabled' : '' ?>
|
<?php foreach ($bagCodes as $bc): ?>
|
||||||
onclick="return confirm('선택한 입출고 건을 삭제할까요? 재고가 복원됩니다.');">삭제</button>
|
<option value="<?= esc($bc->cd_code) ?>"><?= esc($bc->cd_code . ' - ' . $bc->cd_name) ?></option>
|
||||||
<?php
|
<?php endforeach; ?>
|
||||||
$cancelQs = $filters;
|
</select>
|
||||||
unset($cancelQs['sel_key']);
|
<label class="text-sm text-gray-600">수량</label>
|
||||||
$cancelUrl = mgmt_url('reports/misc-flow') . ($cancelQs !== [] ? '?' . http_build_query($cancelQs) : '');
|
<input type="number" name="bmf_qty" min="1" class="border border-gray-300 rounded px-2 py-1 text-sm w-24" required/>
|
||||||
?>
|
<label class="text-sm text-gray-600">일자</label>
|
||||||
<a href="<?= esc($cancelUrl) ?>" class="border border-gray-400 text-gray-700 px-4 py-1 rounded-sm text-sm hover:bg-gray-50 inline-block">취소</a>
|
<input type="date" name="bmf_date" value="<?= date('Y-m-d') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm" required/>
|
||||||
</div>
|
<label class="text-sm text-gray-600">사유</label>
|
||||||
|
<input type="text" name="bmf_reason" placeholder="입출고 사유" class="border border-gray-300 rounded px-2 py-1 text-sm w-48" required/>
|
||||||
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">등록</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- 입출고 일자 (상세) -->
|
|
||||||
<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="p-2 grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
|
||||||
<?php if ($selectedGroup): ?>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="font-bold text-gray-700 whitespace-nowrap">수불 일자</span>
|
|
||||||
<span class="border border-gray-200 bg-gray-50 px-2 py-1 rounded"><?= esc((string) ($selectedGroup['date'] ?? '')) ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="font-bold text-gray-700 whitespace-nowrap">선택</span>
|
|
||||||
<span class="border border-gray-200 bg-gray-50 px-2 py-1 rounded"><?= esc((string) ($selectedGroup['typeLabel'] ?? '')) ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="font-bold text-gray-700 whitespace-nowrap">분류</span>
|
|
||||||
<span class="border border-gray-200 bg-gray-50 px-2 py-1 rounded min-w-[6rem]">
|
|
||||||
<?= esc($selectedBagKindLabel ?? '') !== '' ? esc($selectedBagKindLabel) : '—' ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<span class="font-bold text-gray-700 block mb-1">비고</span>
|
|
||||||
<div class="border border-gray-200 bg-gray-50 rounded px-2 py-2 min-h-[4rem] whitespace-pre-wrap"><?= esc((string) ($selectedGroup['reason'] ?? '')) ?></div>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<p class="text-gray-400 col-span-2 py-2">좌측 입출고 리스트에서 건을 선택하거나, 아래에서 신규 등록해 주세요.</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 입출고 봉투 코드 -->
|
<!-- 조회 필터 -->
|
||||||
<section class="border border-gray-300 bg-white">
|
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
||||||
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">입출고 봉투 코드</div>
|
<form method="GET" action="<?= mgmt_url('reports/misc-flow') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<div class="overflow-auto max-h-[280px]">
|
<label class="text-sm text-gray-600">시작일</label>
|
||||||
<table class="w-full data-table text-sm">
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<label class="text-sm text-gray-600">~</label>
|
||||||
|
<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 rounded-sm text-sm">조회</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-10">No</th>
|
<th>번호</th>
|
||||||
|
<th>구분</th>
|
||||||
|
<th>일자</th>
|
||||||
<th>봉투코드</th>
|
<th>봉투코드</th>
|
||||||
<th>봉투 종류</th>
|
<th>봉투명</th>
|
||||||
<th class="w-20">수량</th>
|
<th>수량</th>
|
||||||
<th class="w-14">단위</th>
|
<th>사유</th>
|
||||||
|
<th>등록일</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php if ($detailLines !== []): ?>
|
<?php foreach ($result as $row): ?>
|
||||||
<?php foreach ($detailLines as $idx => $line): ?>
|
|
||||||
<?php
|
|
||||||
$code = (string) ($line->bmf_bag_code ?? '');
|
|
||||||
$pu = $packagingMap[$code] ?? null;
|
|
||||||
$unitLabel = '매';
|
|
||||||
if ($pu && (int) ($pu->pu_pack_per_sheet ?? 0) > 0) {
|
|
||||||
$unitLabel = '매';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><?= $idx + 1 ?></td>
|
<td class="text-center"><?= (int) $row->bmf_idx ?></td>
|
||||||
<td class="text-center font-mono"><?= esc($code) ?></td>
|
<td class="text-center"><?= $row->bmf_type === 'in' ? '<span class="text-blue-600">입고</span>' : '<span class="text-red-600">출고</span>' ?></td>
|
||||||
<td class="text-left pl-2"><?= esc((string) ($line->bmf_bag_name ?? '')) ?></td>
|
<td class="text-center"><?= esc($row->bmf_date) ?></td>
|
||||||
<td class="pr-2"><?= number_format((int) ($line->bmf_qty ?? 0)) ?></td>
|
<td class="text-center font-mono"><?= esc($row->bmf_bag_code) ?></td>
|
||||||
<td class="text-center"><?= esc($unitLabel) ?></td>
|
<td class="text-left pl-2"><?= esc($row->bmf_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $row->bmf_qty) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bmf_reason) ?></td>
|
||||||
|
<td class="text-center"><?= esc($row->bmf_regdate) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php if (empty($result)): ?>
|
||||||
<tr><td colspan="5" class="text-center text-gray-400 py-6">봉투 코드 내역이 없습니다.</td></tr>
|
<tr><td colspan="8" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
|
||||||
<tr class="bg-gray-50 font-semibold">
|
|
||||||
<td colspan="3" class="text-center">합계</td>
|
|
||||||
<td class="text-right pr-2"><?= number_format($detailTotalQty) ?></td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 품목 등록 (동일 수불일자·구분·비고로 묶임) -->
|
|
||||||
<?php if ($tableExists ?? false): ?>
|
|
||||||
<section class="border border-gray-300 bg-white no-print">
|
|
||||||
<div class="border-b border-gray-300 bg-gray-50 px-2 py-1 text-sm font-bold text-gray-700">품목 등록</div>
|
|
||||||
<form method="post" action="<?= mgmt_url('reports/misc-flow') ?>" class="p-2 flex flex-wrap items-end gap-2 text-sm">
|
|
||||||
<?= csrf_field() ?>
|
|
||||||
<input type="hidden" name="flow_y" value="<?= esc($flowYear) ?>"/>
|
|
||||||
<input type="hidden" name="flow_m" value="<?= esc($flowMonthNum) ?>"/>
|
|
||||||
<input type="hidden" name="bag_code" value="<?= esc($bagCodeFilter) ?>"/>
|
|
||||||
<input type="hidden" name="bag_kind" value="<?= esc($bagKind) ?>"/>
|
|
||||||
<?php if ($bagCancelOnly): ?><input type="hidden" name="bag_cancel" value="1"/><?php endif; ?>
|
|
||||||
<input type="hidden" name="sel_key" value="<?= esc($selKey) ?>"/>
|
|
||||||
|
|
||||||
<label class="font-bold text-gray-700">수불 일자</label>
|
|
||||||
<input type="date" name="bmf_date" value="<?= esc($registerDate) ?>" class="border border-gray-300 rounded px-2 py-1" required/>
|
|
||||||
|
|
||||||
<label class="font-bold text-gray-700">선택</label>
|
|
||||||
<select name="bmf_type" class="border border-gray-300 rounded px-2 py-1 min-w-[7.5rem] w-32">
|
|
||||||
<option value="in" <?= $registerType === 'in' ? 'selected' : '' ?>>입고</option>
|
|
||||||
<option value="out" <?= $registerType === 'out' ? 'selected' : '' ?>>출고</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label class="font-bold text-gray-700">분류</label>
|
|
||||||
<select name="bmf_bag_kind" id="bmf-bag-kind" class="border border-gray-300 rounded px-2 py-1 min-w-[7rem]">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<?php foreach ($bagKindOptions as $opt): ?>
|
|
||||||
<option value="<?= esc((string) $opt->cd_code) ?>" <?= ($selectedBagKind ?? '') === (string) $opt->cd_code ? 'selected' : '' ?>>
|
|
||||||
<?= esc((string) $opt->cd_name) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label class="font-bold text-gray-700">비고</label>
|
|
||||||
<input type="text" name="bmf_reason" value="<?= esc($registerReason) ?>" placeholder="입출고 메모" maxlength="200"
|
|
||||||
class="border border-gray-300 rounded px-2 py-1 w-40 max-w-[10rem] shrink-0" required/>
|
|
||||||
|
|
||||||
<label class="font-bold text-gray-700">봉투 코드</label>
|
|
||||||
<select name="bmf_bag_code" id="bmf-bag-code" class="border border-gray-300 rounded px-2 py-1 min-w-[11rem]" required>
|
|
||||||
<option value="">선택</option>
|
|
||||||
<?php foreach ($bagCodes as $bc): ?>
|
|
||||||
<?php $code = (string) $bc->cd_code; ?>
|
|
||||||
<option value="<?= esc($code) ?>" data-kind-prefix="<?= esc(substr($code, 0, 2)) ?>">
|
|
||||||
<?= esc($code . ' - ' . $bc->cd_name) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label class="font-bold text-gray-700">수량</label>
|
|
||||||
<input type="number" name="bmf_qty" min="1" class="border border-gray-300 rounded px-2 py-1 w-24" required/>
|
|
||||||
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">등록</button>
|
|
||||||
</form>
|
|
||||||
<p class="px-2 pb-2 text-xs text-gray-500">동일 수불일자·입출고·비고로 등록한 품목은 좌측 리스트에서 한 건으로 묶여 표시됩니다.</p>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const kindSelect = document.getElementById('bmf-bag-kind');
|
|
||||||
const bagSelect = document.getElementById('bmf-bag-code');
|
|
||||||
if (!kindSelect || !bagSelect) return;
|
|
||||||
|
|
||||||
const allOptions = Array.from(bagSelect.querySelectorAll('option[data-kind-prefix]'));
|
|
||||||
|
|
||||||
function filterBagCodes() {
|
|
||||||
const prefix = kindSelect.value;
|
|
||||||
const current = bagSelect.value;
|
|
||||||
allOptions.forEach(function (opt) {
|
|
||||||
const show = prefix === '' || opt.getAttribute('data-kind-prefix') === prefix;
|
|
||||||
opt.hidden = !show;
|
|
||||||
opt.disabled = !show;
|
|
||||||
});
|
|
||||||
const selected = bagSelect.querySelector('option[value="' + CSS.escape(current) + '"]');
|
|
||||||
if (selected && !selected.hidden) {
|
|
||||||
bagSelect.value = current;
|
|
||||||
} else {
|
|
||||||
bagSelect.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kindSelect.addEventListener('change', filterBagCodes);
|
|
||||||
filterBagCodes();
|
|
||||||
|
|
||||||
const flowYearSelect = document.querySelector('select[name="flow_y"]');
|
|
||||||
const flowMonthSelect = document.querySelector('form[method="get"] select[name="flow_m"]');
|
|
||||||
if (flowYearSelect && flowMonthSelect) {
|
|
||||||
const syncFlowMonthSelect = function () {
|
|
||||||
const hasYear = flowYearSelect.value !== '';
|
|
||||||
flowMonthSelect.disabled = !hasYear;
|
|
||||||
if (!hasYear) {
|
|
||||||
flowMonthSelect.value = '';
|
|
||||||
}
|
|
||||||
const firstOpt = flowMonthSelect.querySelector('option[value=""]');
|
|
||||||
if (firstOpt) {
|
|
||||||
firstOpt.textContent = hasYear ? '전체' : '—';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
flowYearSelect.addEventListener('change', syncFlowMonthSelect);
|
|
||||||
syncFlowMonthSelect();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,184 +1,73 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '기간별 판매현황']) ?>
|
||||||
declare(strict_types=1);
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
|
||||||
/** @var list<array<string,mixed>> $lines */
|
|
||||||
/** @var string $startDate */
|
|
||||||
/** @var string $endDate */
|
|
||||||
/** @var int $saIdx */
|
|
||||||
/** @var string $catFilter */
|
|
||||||
/** @var string $mode */
|
|
||||||
/** @var list<object> $agencies */
|
|
||||||
/** @var array<string,string> $catLabels */
|
|
||||||
/** @var bool $hasBsFee */
|
|
||||||
/** @var string $lgName */
|
|
||||||
/** @var string $agencyLabel */
|
|
||||||
/** @var string $catLabelFilter */
|
|
||||||
/** @var list<string> $printExtraLines */
|
|
||||||
|
|
||||||
$byDaily = ($mode ?? 'daily') === 'daily';
|
|
||||||
|
|
||||||
$exportParams = array_filter([
|
|
||||||
'start_date' => $startDate ?? '',
|
|
||||||
'end_date' => $endDate ?? '',
|
|
||||||
'sa_idx' => (int) ($saIdx ?? 0),
|
|
||||||
'cat' => (string) ($catFilter ?? ''),
|
|
||||||
'mode' => $byDaily ? '' : 'period',
|
|
||||||
'export' => '1',
|
|
||||||
], static fn ($v): bool => $v !== '' && $v !== null);
|
|
||||||
$excelUrl = mgmt_url('reports/period-sales?' . http_build_query($exportParams));
|
|
||||||
|
|
||||||
$fmtFee = static function (float $v) use ($hasBsFee): string {
|
|
||||||
if (! $hasBsFee) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
return number_format((int) round($v));
|
|
||||||
};
|
|
||||||
|
|
||||||
$rowClass = static function (string $kind): string {
|
|
||||||
return match ($kind) {
|
|
||||||
'day_sub_all' => 'bg-gray-100 font-semibold',
|
|
||||||
'day_sub_bag' => 'bg-sky-50 font-semibold text-sky-900',
|
|
||||||
'day_sub_fs' => 'bg-violet-50 font-semibold text-violet-900',
|
|
||||||
'foot_all' => 'bg-red-50 font-bold text-red-700',
|
|
||||||
'foot_bag' => 'bg-blue-50 font-bold text-blue-700',
|
|
||||||
'foot_fs' => 'bg-purple-50 font-bold text-purple-800',
|
|
||||||
default => '',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
?>
|
|
||||||
<?= 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">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">기간별 판매현황</span>
|
<span class="text-sm font-bold text-gray-700">기간별 판매현황</span>
|
||||||
<div class="flex flex-wrap gap-2">
|
<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>
|
||||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
|
||||||
<a href="<?= esc($excelUrl, 'attr') ?>" class="inline-flex items-center border border-green-600 text-green-700 px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
<section class="p-3 bg-white border-b border-gray-200 no-print">
|
<form method="GET" action="<?= mgmt_url('reports/period-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<form method="GET" action="<?= mgmt_url('reports/period-sales') ?>" class="flex flex-wrap items-end gap-3 text-sm">
|
<label class="text-sm text-gray-600">시작일</label>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">시작일</label>
|
|
||||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
</div>
|
<label class="text-sm text-gray-600">~</label>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">종료일</label>
|
|
||||||
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
</div>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">대행소</label>
|
|
||||||
<select name="sa_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[12rem] max-w-[20rem]">
|
|
||||||
<option value="0">전체</option>
|
|
||||||
<?php foreach ($agencies ?? [] as $agency): ?>
|
|
||||||
<?php $aid = (int) ($agency->sa_idx ?? 0); ?>
|
|
||||||
<option value="<?= esc((string) $aid) ?>" <?= (int) ($saIdx ?? 0) === $aid ? 'selected' : '' ?>>
|
|
||||||
<?= esc(trim((string) ($agency->sa_name ?? ''))) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">구분</label>
|
|
||||||
<select name="cat" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem]">
|
|
||||||
<option value="" <?= ($catFilter ?? '') === '' ? 'selected' : '' ?>>전체</option>
|
|
||||||
<?php foreach (($catLabels ?? []) as $ck => $lab): ?>
|
|
||||||
<option value="<?= esc($ck, 'attr') ?>" <?= ($catFilter ?? '') === $ck ? 'selected' : '' ?>><?= esc($lab) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">집계 방식</label>
|
|
||||||
<select name="mode" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[9rem]">
|
|
||||||
<option value="daily" <?= $byDaily ? 'selected' : '' ?>>일자별</option>
|
|
||||||
<option value="period" <?= ! $byDaily ? 'selected' : '' ?>>기간별</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="p-3 bg-white">
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<style>
|
<table class="w-full data-table">
|
||||||
@media print {
|
|
||||||
.period-sales-screen-title { display: none !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="mb-2 text-center period-sales-screen-title no-print">
|
|
||||||
<h1 class="text-lg font-bold m-0">기간별 판매현황<?= $byDaily ? ' [일집계]' : ' [기간집계]' ?></h1>
|
|
||||||
<p class="text-sm text-gray-700 m-1"><?= esc(trim(($lgName ?? '') . ' · ' . ($startDate ?? '') . ' ~ ' . ($endDate ?? '') . ' · 대행소: ' . ($agencyLabel ?? '') . ' · 구분: ' . ($catLabelFilter ?? ''))) ?></p>
|
|
||||||
<p class="text-xs text-gray-500 m-0">집계: <?= $byDaily ? '일자별' : '기간별' ?> · (단위: 매 / 원)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border border-gray-300 overflow-auto">
|
|
||||||
<table class="w-full data-table text-sm" id="period-sales-table">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<?php if ($byDaily): ?>
|
<th>봉투코드</th>
|
||||||
<th rowspan="2" class="align-middle whitespace-nowrap">일자</th>
|
<th>봉투명</th>
|
||||||
<?php endif; ?>
|
<th>판매수량</th>
|
||||||
<th rowspan="2" class="align-middle text-left pl-2">품목</th>
|
<th>판매금액</th>
|
||||||
<th colspan="4" class="text-center border-l border-gray-300">판매</th>
|
<th>반품수량</th>
|
||||||
<th colspan="2" class="text-center border-l border-gray-300">반품</th>
|
<th>반품금액</th>
|
||||||
<th colspan="4" class="text-center border-l border-gray-300">계</th>
|
<th>합계수량</th>
|
||||||
</tr>
|
<th>합계금액</th>
|
||||||
<tr>
|
|
||||||
<th class="text-right border-l border-gray-300">수량</th>
|
|
||||||
<th class="text-right">판매금액</th>
|
|
||||||
<th class="text-right">수수료</th>
|
|
||||||
<th class="text-right">징수액</th>
|
|
||||||
<th class="text-right border-l border-gray-300">수량</th>
|
|
||||||
<th class="text-right">금액</th>
|
|
||||||
<th class="text-right border-l border-gray-300">수량</th>
|
|
||||||
<th class="text-right">판매금액</th>
|
|
||||||
<th class="text-right">수수료</th>
|
|
||||||
<th class="text-right">징수액</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php
|
<?php
|
||||||
$emptyColspan = $byDaily ? 12 : 11;
|
$grandSaleQty = 0;
|
||||||
|
$grandSaleAmount = 0;
|
||||||
|
$grandReturnQty = 0;
|
||||||
|
$grandReturnAmount = 0;
|
||||||
?>
|
?>
|
||||||
<?php foreach ($lines ?? [] as $ln): ?>
|
<?php foreach ($result as $row): ?>
|
||||||
<?php
|
<?php
|
||||||
$kind = (string) ($ln['kind'] ?? 'data');
|
$grandSaleQty += (int) $row->sale_qty;
|
||||||
$trCls = $rowClass($kind);
|
$grandSaleAmount += (int) $row->sale_amount;
|
||||||
$isData = $kind === 'data';
|
$grandReturnQty += (int) $row->return_qty;
|
||||||
$rs = (int) ($ln['ymd_rowspan'] ?? 0);
|
$grandReturnAmount += (int) $row->return_amount;
|
||||||
?>
|
?>
|
||||||
<tr class="<?= esc($trCls, 'attr') ?>">
|
<tr>
|
||||||
<?php if ($byDaily): ?>
|
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||||
<?php if ($isData && $rs > 0): ?>
|
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||||
<td rowspan="<?= $rs ?>" class="text-center align-top whitespace-nowrap tabular-nums pt-1"><?= esc((string) ($ln['ymd'] ?? '')) ?></td>
|
<td><?= number_format((int) $row->sale_qty) ?></td>
|
||||||
<?php elseif (str_starts_with($kind, 'foot_')): ?>
|
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||||
<td class="bg-inherit"></td>
|
<td><?= number_format((int) $row->return_qty) ?></td>
|
||||||
<?php endif; ?>
|
<td><?= number_format((int) $row->return_amount) ?></td>
|
||||||
<?php endif; ?>
|
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc((string) ($ln['name'] ?? '')) ?></td>
|
<td class="font-bold"><?= number_format((int) $row->sale_amount - (int) $row->return_amount) ?></td>
|
||||||
<td class="border-l border-gray-200 tabular-nums"><?= number_format((int) ($ln['s_qty'] ?? 0)) ?></td>
|
|
||||||
<td class="tabular-nums"><?= number_format((int) round((float) ($ln['s_amt'] ?? 0))) ?></td>
|
|
||||||
<td class="tabular-nums"><?= $fmtFee((float) ($ln['s_fee'] ?? 0)) ?></td>
|
|
||||||
<td class="tabular-nums"><?= number_format((int) round((float) ($ln['s_levy'] ?? 0))) ?></td>
|
|
||||||
<td class="border-l border-gray-200 tabular-nums"><?= number_format((int) ($ln['r_qty'] ?? 0)) ?></td>
|
|
||||||
<td class="tabular-nums"><?= number_format((int) round((float) ($ln['r_amt'] ?? 0))) ?></td>
|
|
||||||
<td class="border-l border-gray-200 tabular-nums"><?= number_format((int) ($ln['t_qty'] ?? 0)) ?></td>
|
|
||||||
<td class="tabular-nums"><?= number_format((int) round((float) ($ln['t_amt'] ?? 0))) ?></td>
|
|
||||||
<td class="tabular-nums"><?= $fmtFee((float) ($ln['t_fee'] ?? 0)) ?></td>
|
|
||||||
<td class="tabular-nums"><?= number_format((int) round((float) ($ln['t_levy'] ?? 0))) ?></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (($lines ?? []) === []): ?>
|
<?php if (empty($result)): ?>
|
||||||
<tr>
|
<tr><td colspan="8" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||||
<td colspan="<?= (int) $emptyColspan ?>" class="text-center text-gray-400 py-6">조회된 데이터가 없습니다.</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tfoot class="bg-gray-50 font-bold text-right">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-center">합계</td>
|
||||||
|
<td><?= number_format($grandSaleQty) ?></td>
|
||||||
|
<td><?= number_format($grandSaleAmount) ?></td>
|
||||||
|
<td><?= number_format($grandReturnQty) ?></td>
|
||||||
|
<td><?= number_format($grandReturnAmount) ?></td>
|
||||||
|
<td><?= number_format($grandSaleQty - $grandReturnQty) ?></td>
|
||||||
|
<td><?= number_format($grandSaleAmount - $grandReturnAmount) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|||||||
@@ -1,178 +1,59 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '반품/파기 현황']) ?>
|
||||||
$startDate = (string) ($startDate ?? date('Y-m-01'));
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
$endDate = (string) ($endDate ?? date('Y-m-d'));
|
|
||||||
$ioType = (string) ($ioType ?? 'out');
|
|
||||||
$result = is_array($result ?? null) ? $result : (array) ($result ?? []);
|
|
||||||
$queried = (bool) ($queried ?? false);
|
|
||||||
$exportParams = $queried ? array_filter([
|
|
||||||
'search' => '1',
|
|
||||||
'start_date' => $startDate,
|
|
||||||
'end_date' => $endDate,
|
|
||||||
'io_type' => $ioType,
|
|
||||||
], static fn ($v) => $v !== null && $v !== '') : [];
|
|
||||||
$excelUrl = $exportParams !== []
|
|
||||||
? mgmt_url('reports/returns/export') . '?' . http_build_query($exportParams)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
$fmtKrDate = static function (string $ymd): string {
|
|
||||||
$ts = strtotime($ymd);
|
|
||||||
|
|
||||||
return $ts ? date('Y년 m월 d일', $ts) : $ymd;
|
|
||||||
};
|
|
||||||
|
|
||||||
$ioLabel = $ioType === 'in' ? '입고' : '출고';
|
|
||||||
$periodLabel = $fmtKrDate($startDate) . ' ~ ' . $fmtKrDate($endDate);
|
|
||||||
$printExtraLines = [
|
|
||||||
'조회기간: ' . $periodLabel,
|
|
||||||
'입출고 구분: ' . $ioLabel,
|
|
||||||
'(단위: 매)',
|
|
||||||
];
|
|
||||||
|
|
||||||
$typeLabel = static function (string $bsType): string {
|
|
||||||
return match ($bsType) {
|
|
||||||
'return' => '반품',
|
|
||||||
'dispose' => '파기',
|
|
||||||
'cancel' => '파기',
|
|
||||||
default => $bsType,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
$kindLabel = static function (object $row): string {
|
|
||||||
$name = trim((string) ($row->bs_bag_name ?? ''));
|
|
||||||
if ($name !== '') {
|
|
||||||
return $name;
|
|
||||||
}
|
|
||||||
$code = trim((string) ($row->bs_bag_code ?? ''));
|
|
||||||
|
|
||||||
return $code !== '' ? $code : '-';
|
|
||||||
};
|
|
||||||
|
|
||||||
$totalQty = 0;
|
|
||||||
foreach ($result as $row) {
|
|
||||||
$totalQty += (int) ($row->qty ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$tipPage = "지정판매소 반품·물류 입고분 파기 내역을 기간·입출고 구분으로 조회합니다.\n"
|
|
||||||
. "· 출고: 지정판매소 반품 등록 화면에서 처리된 반품\n"
|
|
||||||
. "· 입고: 물류 창고 입고분 파기 처리 내역\n"
|
|
||||||
. "조회 후 표·엑셀·인쇄에 반영됩니다.";
|
|
||||||
?>
|
|
||||||
<?= 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">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700 inline-flex items-center gap-1">
|
<span class="text-sm font-bold text-gray-700">반품/파기 현황</span>
|
||||||
반품/파기 현황
|
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||||
<?= view('components/field_tooltip', ['text' => $tipPage, 'placement' => 'below']) ?>
|
|
||||||
</span>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<?php if ($excelUrl !== ''): ?>
|
|
||||||
<a href="<?= esc($excelUrl, 'attr') ?>" target="_blank" rel="noopener noreferrer"
|
|
||||||
class="bg-green-700 text-white px-3 py-1 rounded-sm text-sm hover:bg-green-800">엑셀저장</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="bg-gray-300 text-gray-600 px-3 py-1 rounded-sm text-sm cursor-not-allowed" title="조회 후 이용">엑셀저장</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">인쇄</button>
|
|
||||||
<a href="<?= base_url('dashboard') ?>" class="border border-gray-400 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">종료</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print">
|
<form method="GET" action="<?= mgmt_url('reports/returns') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<form method="get" action="<?= mgmt_url('reports/returns') ?>" class="flex flex-wrap items-end gap-x-4 gap-y-2 text-sm">
|
<label class="text-sm text-gray-600">시작일</label>
|
||||||
<input type="hidden" name="search" value="1"/>
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<label class="text-sm text-gray-600">~</label>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
<label class="font-bold text-gray-700 whitespace-nowrap">조회기간</label>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<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-3">
|
|
||||||
<span class="font-bold text-gray-700 whitespace-nowrap">입출고 구분</span>
|
|
||||||
<label class="inline-flex items-center gap-1">
|
|
||||||
<input type="radio" name="io_type" value="in" <?= $ioType === 'in' ? 'checked' : '' ?>/>
|
|
||||||
입고
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center gap-1">
|
|
||||||
<input type="radio" name="io_type" value="out" <?= $ioType === 'out' ? 'checked' : '' ?>/>
|
|
||||||
출고
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<?php if (! $queried): ?>
|
<table class="w-full data-table">
|
||||||
<div class="m-2 p-3 border border-blue-200 bg-blue-50 text-sm text-blue-900 no-print">
|
|
||||||
조회기간과 입출고 구분을 선택한 뒤 <strong>조회</strong> 버튼을 눌러 주세요.
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="border border-gray-300 overflow-auto m-2 print:m-0">
|
|
||||||
<table class="w-full data-table text-sm">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-28">일자</th>
|
<th>일자</th>
|
||||||
<th>반품처</th>
|
<th>판매소</th>
|
||||||
<th>종류</th>
|
<th>봉투코드</th>
|
||||||
<th class="w-24 text-right">수량</th>
|
<th>봉투명</th>
|
||||||
<th class="w-20 text-center">구분</th>
|
<th>구분</th>
|
||||||
|
<th>수량</th>
|
||||||
|
<th>금액</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="text-right">
|
||||||
<?php if ($queried && $result === []): ?>
|
<?php
|
||||||
|
$totalQty = 0; $totalAmt = 0;
|
||||||
|
$typeMap = ['return' => '반품', 'cancel' => '취소/파기'];
|
||||||
|
foreach ($result as $row):
|
||||||
|
$totalQty += (int) $row->qty;
|
||||||
|
$totalAmt += (int) $row->amount;
|
||||||
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="text-center text-gray-500 py-8">해당 자료가 없습니다.</td>
|
<td class="text-center"><?= esc($row->bs_sale_date) ?></td>
|
||||||
</tr>
|
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
|
||||||
<?php endif; ?>
|
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||||
<?php foreach ($result as $row): ?>
|
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||||
<tr>
|
<td class="text-center"><?= esc($typeMap[$row->bs_type] ?? $row->bs_type) ?></td>
|
||||||
<td class="text-center"><?= esc((string) $row->bs_sale_date) ?></td>
|
<td><?= number_format((int) $row->qty) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc((string) ($row->bs_ds_name ?? '')) ?></td>
|
<td><?= number_format((int) $row->amount) ?></td>
|
||||||
<td class="text-left pl-2"><?= esc($kindLabel($row)) ?></td>
|
|
||||||
<td class="text-right pr-2 tabular-nums"><?= number_format((int) ($row->qty ?? 0)) ?></td>
|
|
||||||
<td class="text-center"><?= esc($typeLabel((string) ($row->bs_type ?? ''))) ?></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if ($queried && $result !== []): ?>
|
<?php if (empty($result)): ?>
|
||||||
|
<tr><td colspan="7" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
<tr class="font-bold bg-gray-100">
|
<tr class="font-bold bg-gray-100">
|
||||||
<td colspan="3" class="text-center">합계</td>
|
<td colspan="5" class="text-center">합계</td>
|
||||||
<td class="text-right pr-2 tabular-nums"><?= number_format($totalQty) ?></td>
|
<td><?= number_format($totalQty) ?></td>
|
||||||
<td></td>
|
<td><?= number_format($totalAmt) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.field-tip { position: relative; display: inline-flex; vertical-align: middle; }
|
|
||||||
.field-tip-btn {
|
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
|
||||||
width: 14px; height: 14px; font-size: 10px; font-weight: 700; line-height: 1;
|
|
||||||
color: #6b7280; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 50%;
|
|
||||||
cursor: help; user-select: none;
|
|
||||||
}
|
|
||||||
.field-tip-btn:hover, .field-tip-btn:focus { color: #1d4ed8; border-color: #93c5fd; background: #eff6ff; outline: none; }
|
|
||||||
.field-tip-panel {
|
|
||||||
position: absolute; z-index: 60; left: 50%; transform: translateX(-50%);
|
|
||||||
bottom: calc(100% + 6px); width: max-content; max-width: 280px;
|
|
||||||
padding: 0.35rem 0.5rem; border-radius: 4px;
|
|
||||||
background: #1f2937; color: #f9fafb; font-size: 11px; font-weight: 500; line-height: 1.35;
|
|
||||||
text-align: left; white-space: pre-line; box-shadow: 0 2px 8px rgba(0,0,0,.15);
|
|
||||||
opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .12s, visibility .12s;
|
|
||||||
}
|
|
||||||
.field-tip--below .field-tip-panel { bottom: auto; top: calc(100% + 6px); }
|
|
||||||
.field-tip:hover .field-tip-panel,
|
|
||||||
.field-tip:focus-within .field-tip-panel { opacity: 1; visibility: visible; }
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.no-print { display: none !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,258 +1,97 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '판매 대장']) ?>
|
||||||
declare(strict_types=1);
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
|
||||||
/** @var list<object> $shops */
|
|
||||||
/** @var list<object> $agencies */
|
|
||||||
/** @var list<array<string,mixed>> $ledgerRows */
|
|
||||||
/** @var int $saleLineCount */
|
|
||||||
/** @var string $startDate */
|
|
||||||
/** @var string $endDate */
|
|
||||||
/** @var string $mode */
|
|
||||||
/** @var int $dsIdx */
|
|
||||||
/** @var int $saIdx */
|
|
||||||
/** @var list<string> $cats */
|
|
||||||
/** @var string $lgName */
|
|
||||||
/** @var string $filterAgencyLabel */
|
|
||||||
/** @var list<string> $printSubtitleLines */
|
|
||||||
|
|
||||||
$printTitle = ($mode ?? 'daily') === 'daily' ? '[지정판매소] 일자별 판매대장' : '[지정판매소] 기간별 판매대장';
|
|
||||||
$printDate = date('Y-m-d');
|
|
||||||
$printExtraLines = $printSubtitleLines ?? [];
|
|
||||||
$catKeys = ['general', 'food', 'sticker', 'reuse', 'apt', 'public_use', 'container', 'waste'];
|
|
||||||
$catLabels = [
|
|
||||||
'general' => '일반용',
|
|
||||||
'food' => '음식물',
|
|
||||||
'sticker' => '스티커',
|
|
||||||
'reuse' => '재사용',
|
|
||||||
'apt' => '공동주택용',
|
|
||||||
'public_use' => '공공용',
|
|
||||||
'container' => '용기',
|
|
||||||
'waste' => '폐기물',
|
|
||||||
];
|
|
||||||
|
|
||||||
$exportParams = [
|
|
||||||
'start_date' => $startDate,
|
|
||||||
'end_date' => $endDate,
|
|
||||||
'mode' => $mode,
|
|
||||||
'ds_idx' => $dsIdx,
|
|
||||||
'sa_idx' => $saIdx ?? 0,
|
|
||||||
'export' => '1',
|
|
||||||
];
|
|
||||||
if ($cats !== []) {
|
|
||||||
$exportParams['cat'] = $cats;
|
|
||||||
}
|
|
||||||
$excelUrl = mgmt_url('reports/sales-ledger?' . http_build_query($exportParams));
|
|
||||||
?>
|
|
||||||
<?= view('components/print_header', [
|
|
||||||
'printTitle' => $printTitle,
|
|
||||||
'printDate' => $printDate,
|
|
||||||
'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">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">지정 판매소 판매 대장</span>
|
<span class="text-sm font-bold text-gray-700">판매 대장</span>
|
||||||
<div class="flex flex-wrap gap-2">
|
<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>
|
||||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
|
||||||
<a href="<?= esc($excelUrl, 'attr') ?>" class="inline-flex items-center border border-green-600 text-green-700 px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
<section class="p-3 bg-white border-b border-gray-200 no-print">
|
<form method="GET" action="<?= mgmt_url('reports/sales-ledger') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<form method="get" action="<?= mgmt_url('reports/sales-ledger') ?>" id="sales-ledger-form" class="space-y-3 text-sm">
|
<label class="text-sm text-gray-600">시작일</label>
|
||||||
<div class="flex flex-wrap items-end gap-3">
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
<div>
|
<label class="text-sm text-gray-600">~</label>
|
||||||
<label class="block text-gray-600 mb-0.5">조회일자</label>
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
<div class="flex items-center gap-1">
|
<label class="text-sm text-gray-600">조회방식</label>
|
||||||
<input type="date" name="start_date" value="<?= esc($startDate) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
<select name="mode" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||||
<span>~</span>
|
<option value="daily" <?= ($mode ?? '') === 'daily' ? 'selected' : '' ?>>일자별</option>
|
||||||
<input type="date" name="end_date" value="<?= esc($endDate) ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
<option value="period" <?= ($mode ?? '') === 'period' ? 'selected' : '' ?>>기간별</option>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">지정판매소</label>
|
|
||||||
<select name="ds_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[14rem] max-w-[20rem]">
|
|
||||||
<option value="0">전체</option>
|
|
||||||
<?php foreach ($shops as $s): ?>
|
|
||||||
<?php $sid = (int) ($s->ds_idx ?? 0); ?>
|
|
||||||
<option value="<?= esc((string) $sid) ?>" <?= $dsIdx === $sid ? 'selected' : '' ?>>
|
|
||||||
<?= esc(trim((string) ($s->ds_shop_no ?? '') . ' ' . (string) ($s->ds_name ?? ''))) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">대행소</label>
|
|
||||||
<select name="sa_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[12rem] max-w-[20rem]">
|
|
||||||
<option value="0">전체</option>
|
|
||||||
<?php foreach ($agencies ?? [] as $agency): ?>
|
|
||||||
<?php $aid = (int) ($agency->sa_idx ?? 0); ?>
|
|
||||||
<option value="<?= esc((string) $aid) ?>" <?= (int) ($saIdx ?? 0) === $aid ? 'selected' : '' ?>>
|
|
||||||
<?= esc(trim((string) ($agency->sa_name ?? ''))) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="block text-gray-600 mb-0.5">집계 방식</span>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<label class="inline-flex items-center gap-1"><input type="radio" name="mode" value="daily" <?= $mode === 'daily' ? 'checked' : '' ?>/> 일자별</label>
|
|
||||||
<label class="inline-flex items-center gap-1"><input type="radio" name="mode" value="period" <?= $mode === 'period' ? 'checked' : '' ?>/> 기간별</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<fieldset class="border border-gray-200 rounded p-2">
|
|
||||||
<legend class="text-xs text-gray-600 px-1">품목</legend>
|
|
||||||
<div class="flex flex-wrap gap-x-4 gap-y-1">
|
|
||||||
<label class="inline-flex items-center gap-1">
|
|
||||||
<input type="checkbox" name="cat[]" value="all" id="cat-all" <?= $cats === [] ? 'checked' : '' ?>/>
|
|
||||||
전체
|
|
||||||
</label>
|
|
||||||
<?php foreach ($catKeys as $ck): ?>
|
|
||||||
<label class="inline-flex items-center gap-1">
|
|
||||||
<input type="checkbox" name="cat[]" value="<?= esc($ck, 'attr') ?>" class="cat-item" <?= in_array($ck, $cats, true) ? 'checked' : '' ?>/>
|
|
||||||
<?= esc($catLabels[$ck] ?? $ck) ?>
|
|
||||||
</label>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<div>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="p-3 bg-white sales-ledger-report-section">
|
<?php if (($mode ?? 'daily') === 'daily'): ?>
|
||||||
<style>
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
@media print {
|
<table class="w-full data-table">
|
||||||
/* 일계표 등 다른 리포트와 동일: 브라우저 기본 세로 A4 (landscape 지정 안 함) */
|
|
||||||
.sales-ledger-screen-title { display: none !important; }
|
|
||||||
.sales-ledger-report-section { padding: 0 !important; }
|
|
||||||
.sales-ledger-scroll-wrap {
|
|
||||||
overflow: visible !important;
|
|
||||||
border: 1px solid #333 !important;
|
|
||||||
}
|
|
||||||
#sales-ledger-table {
|
|
||||||
font-size: 7.5pt !important;
|
|
||||||
width: 100% !important;
|
|
||||||
table-layout: fixed !important;
|
|
||||||
}
|
|
||||||
#sales-ledger-table th,
|
|
||||||
#sales-ledger-table td {
|
|
||||||
min-width: 0 !important;
|
|
||||||
padding: 3px 4px !important;
|
|
||||||
white-space: normal !important;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
line-height: 1.35;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
#sales-ledger-table th {
|
|
||||||
font-size: 7pt !important;
|
|
||||||
padding-top: 4px !important;
|
|
||||||
padding-bottom: 4px !important;
|
|
||||||
}
|
|
||||||
/* 세로 A4 폭에 맞춘 열 비율 (긴 칸은 줄바꿈) */
|
|
||||||
#sales-ledger-table .sl-col-date { width: 9%; }
|
|
||||||
#sales-ledger-table .sl-col-designation { width: 9%; }
|
|
||||||
#sales-ledger-table .sl-col-shop { width: 10%; }
|
|
||||||
#sales-ledger-table .sl-col-rep { width: 7%; }
|
|
||||||
#sales-ledger-table .sl-col-addr { width: 18%; }
|
|
||||||
#sales-ledger-table .sl-col-product { width: 12%; }
|
|
||||||
#sales-ledger-table .sl-col-num { width: 7%; }
|
|
||||||
#sales-ledger-table.sl-period .sl-col-addr { width: 22%; }
|
|
||||||
#sales-ledger-table.sl-period .sl-col-product { width: 14%; }
|
|
||||||
}
|
|
||||||
@media screen {
|
|
||||||
#sales-ledger-table th,
|
|
||||||
#sales-ledger-table td {
|
|
||||||
padding: 4px 8px;
|
|
||||||
line-height: 1.45;
|
|
||||||
font-size: 13px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
#sales-ledger-table .sl-col-date,
|
|
||||||
#sales-ledger-table .sl-col-num { white-space: nowrap; }
|
|
||||||
#sales-ledger-table .sl-col-addr,
|
|
||||||
#sales-ledger-table .sl-col-shop,
|
|
||||||
#sales-ledger-table .sl-col-product {
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="mb-2 text-center sales-ledger-screen-title no-print">
|
|
||||||
<h1 class="text-lg font-bold m-0"><?= esc($printTitle) ?></h1>
|
|
||||||
<p class="text-sm text-gray-700 m-1"><?= esc(trim(($lgName ?? '') . ' / 지정판매소: ' . ($filterShopLabel ?? '') . ' / 대행소: ' . ($filterAgencyLabel ?? '전체'))) ?></p>
|
|
||||||
<p class="text-xs text-gray-500 m-0">(단위: 매 / 원) · <?= esc($startDate) ?> ~ <?= esc($endDate) ?></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sales-ledger-scroll-wrap border border-gray-300 overflow-auto">
|
|
||||||
<table class="w-full data-table text-sm <?= ($mode ?? 'daily') === 'period' ? 'sl-period' : '' ?>" id="sales-ledger-table">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<?php if (($mode ?? 'daily') === 'daily'): ?>
|
<th>판매일</th>
|
||||||
<th class="sl-col-date">일자</th>
|
<th>판매소</th>
|
||||||
<?php endif; ?>
|
<th>봉투코드</th>
|
||||||
<th class="sl-col-designation">지정번호</th>
|
<th>봉투명</th>
|
||||||
<th class="sl-col-shop text-left">판매소명</th>
|
<th>구분</th>
|
||||||
<th class="sl-col-rep">대표자</th>
|
<th>수량</th>
|
||||||
<th class="sl-col-addr text-left">소재지</th>
|
<th>금액</th>
|
||||||
<th class="sl-col-product text-left">품명</th>
|
|
||||||
<th class="text-right sl-col-num">판매량</th>
|
|
||||||
<th class="text-right sl-col-num">판매금액</th>
|
|
||||||
<th class="text-right sl-col-num">수수료</th>
|
|
||||||
<th class="text-right sl-col-num">총액</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="text-right">
|
||||||
<?php foreach ($ledgerRows as $r): ?>
|
<?php foreach ($result as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center"><?= esc($row->bs_sale_date) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||||
|
<td class="text-center">
|
||||||
<?php
|
<?php
|
||||||
$kind = (string) ($r['kind'] ?? 'data');
|
$typeMap = ['sale' => '판매', 'return' => '반품'];
|
||||||
$trClass = $kind === 'subtotal' ? 'bg-gray-50 font-semibold' : ($kind === 'grand' ? 'bg-amber-50 font-bold' : '');
|
echo esc($typeMap[$row->bs_type] ?? $row->bs_type);
|
||||||
?>
|
?>
|
||||||
<tr class="<?= esc($trClass, 'attr') ?>">
|
</td>
|
||||||
<?php if (($mode ?? 'daily') === 'daily'): ?>
|
<td><?= number_format((int) $row->total_qty) ?></td>
|
||||||
<td class="text-center sl-col-date"><?= esc((string) ($r['sale_date'] ?? '')) ?></td>
|
<td><?= number_format((int) $row->total_amount) ?></td>
|
||||||
<?php endif; ?>
|
|
||||||
<td class="text-center sl-col-designation"><?= esc((string) ($r['designation_no'] ?? '')) ?></td>
|
|
||||||
<td class="text-left pl-1 sl-col-shop"><?= esc((string) ($r['shop_name'] ?? '')) ?></td>
|
|
||||||
<td class="text-center sl-col-rep"><?= esc((string) ($r['rep_name'] ?? '')) ?></td>
|
|
||||||
<td class="text-left pl-1 sl-col-addr"><?= esc((string) ($r['address'] ?? '')) ?></td>
|
|
||||||
<td class="text-left pl-1 sl-col-product"><?= esc((string) ($r['product_name'] ?? '')) ?></td>
|
|
||||||
<td class="text-right tabular-nums sl-col-num"><?= esc((string) ($r['qty'] ?? '')) ?></td>
|
|
||||||
<td class="text-right tabular-nums sl-col-num"><?= esc((string) ($r['amount'] ?? '')) ?></td>
|
|
||||||
<td class="text-right tabular-nums sl-col-num"><?= esc((string) ($r['fee'] ?? '')) ?></td>
|
|
||||||
<td class="text-right tabular-nums sl-col-num"><?= esc((string) ($r['total'] ?? '')) ?></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if ($ledgerRows === []): ?>
|
<?php if (empty($result)): ?>
|
||||||
<tr><td colspan="<?= ($mode ?? 'daily') === 'daily' ? '10' : '9' ?>" class="text-center text-gray-400 py-6">조회된 판매 데이터가 없습니다.</td></tr>
|
<tr><td colspan="7" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-gray-700 mt-2 mb-0 no-print">판매건수(상세 행): <?= number_format((int) ($saleLineCount ?? 0)) ?>건</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
<?php else: ?>
|
||||||
(function () {
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
const form = document.getElementById('sales-ledger-form');
|
<table class="w-full data-table">
|
||||||
const catAll = document.getElementById('cat-all');
|
<thead>
|
||||||
const items = () => Array.from(document.querySelectorAll('.cat-item'));
|
<tr>
|
||||||
if (!form || !catAll) return;
|
<th>판매소</th>
|
||||||
catAll.addEventListener('change', () => {
|
<th>봉투코드</th>
|
||||||
if (catAll.checked) items().forEach((el) => { el.checked = false; });
|
<th>봉투명</th>
|
||||||
});
|
<th>판매수량</th>
|
||||||
items().forEach((el) => {
|
<th>판매금액</th>
|
||||||
el.addEventListener('change', () => {
|
<th>반품수량</th>
|
||||||
if (el.checked) catAll.checked = false;
|
<th>반품금액</th>
|
||||||
if (!items().some((x) => x.checked)) catAll.checked = true;
|
<th>계(수량)</th>
|
||||||
});
|
<th>계(금액)</th>
|
||||||
});
|
</tr>
|
||||||
form.addEventListener('submit', () => {
|
</thead>
|
||||||
if (catAll.checked) items().forEach((el) => { el.checked = false; });
|
<tbody class="text-right">
|
||||||
});
|
<?php foreach ($result as $row): ?>
|
||||||
})();
|
<tr>
|
||||||
</script>
|
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $row->sale_qty) ?></td>
|
||||||
|
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||||
|
<td><?= number_format((int) $row->return_qty) ?></td>
|
||||||
|
<td><?= number_format((int) $row->return_amount) ?></td>
|
||||||
|
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
|
||||||
|
<td class="font-bold"><?= number_format((int) $row->sale_amount - (int) $row->return_amount) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($result)): ?>
|
||||||
|
<tr><td colspan="9" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -1,227 +1,64 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '지정판매소별 판매현황']) ?>
|
||||||
declare(strict_types=1);
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
|
||||||
/** @var string $startDate */
|
|
||||||
/** @var string $endDate */
|
|
||||||
/** @var string $zoneCode */
|
|
||||||
/** @var string $bagCode */
|
|
||||||
/** @var string $catFilter */
|
|
||||||
/** @var string $metric */
|
|
||||||
/** @var list<string> $zoneOptions */
|
|
||||||
/** @var array<string, string> $bagOptions */
|
|
||||||
/** @var array<string, string> $catLabels */
|
|
||||||
/** @var list<array<string, mixed>> $reportRows */
|
|
||||||
/** @var list<float> $grandMonths */
|
|
||||||
/** @var float $grandTotal */
|
|
||||||
/** @var string $lgName */
|
|
||||||
/** @var string $zoneLabel */
|
|
||||||
/** @var string $bagLabel */
|
|
||||||
/** @var string $catLabelFilter */
|
|
||||||
/** @var string $metricLabel */
|
|
||||||
/** @var list<string> $printExtraLines */
|
|
||||||
|
|
||||||
$isAmt = ($metric ?? 'qty') === 'amt';
|
|
||||||
$fmtVal = static function (float $v) use ($isAmt): string {
|
|
||||||
return number_format((int) round($v));
|
|
||||||
};
|
|
||||||
|
|
||||||
$exportParams = array_merge([
|
|
||||||
'start_date' => $startDate ?? '',
|
|
||||||
'end_date' => $endDate ?? '',
|
|
||||||
'metric' => ($metric ?? 'qty') === 'amt' ? 'amt' : 'qty',
|
|
||||||
'export' => '1',
|
|
||||||
], array_filter([
|
|
||||||
'zone_code' => (string) ($zoneCode ?? ''),
|
|
||||||
'bag_code' => (string) ($bagCode ?? ''),
|
|
||||||
'cat' => (string) ($catFilter ?? ''),
|
|
||||||
], static fn ($v): bool => $v !== '' && $v !== null));
|
|
||||||
|
|
||||||
$excelUrl = mgmt_url('reports/shop-sales?' . http_build_query($exportParams));
|
|
||||||
$colCount = 16;
|
|
||||||
?>
|
|
||||||
<?= 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">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">지정판매소별 판매현황</span>
|
<span class="text-sm font-bold text-gray-700">지정판매소별 판매현황</span>
|
||||||
<div class="flex flex-wrap gap-2">
|
<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>
|
||||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
|
||||||
<a href="<?= esc($excelUrl, 'attr') ?>" class="inline-flex items-center border border-green-600 text-green-700 px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<p class="text-xs text-gray-500 mt-2">
|
|
||||||
<?php if ($isAmt): ?>
|
|
||||||
금액은 조회기간 내 판매(sale) 건의 판매금액을 거래 월별로 합산합니다(반품·취소는 제외).
|
|
||||||
<?php else: ?>
|
|
||||||
수량은 반품·판매취소를 연초부터 판매와 품목별 선입선출로 맞추고, 반품취소는 원복합니다. 조회에 포함되지 않은 달의 수치는 조회 구간의 첫 달에 합쳐 집계됩니다.
|
|
||||||
<?php endif; ?>
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
<section class="p-3 bg-white border-b border-gray-200 no-print">
|
<form method="GET" action="<?= mgmt_url('reports/shop-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<form method="GET" action="<?= mgmt_url('reports/shop-sales') ?>" class="flex flex-wrap items-end gap-3 text-sm">
|
<label class="text-sm text-gray-600">시작일</label>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">시작일</label>
|
|
||||||
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
</div>
|
<label class="text-sm text-gray-600">~</label>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">종료일</label>
|
|
||||||
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
</div>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">읍면동</label>
|
|
||||||
<select name="zone_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[8rem] max-w-[16rem]">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<?php foreach ($zoneOptions ?? [] as $z): ?>
|
|
||||||
<option value="<?= esc($z, 'attr') ?>" <?= ($zoneCode ?? '') === $z ? 'selected' : '' ?>><?= esc($z) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">봉투 종류</label>
|
|
||||||
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem] max-w-[20rem]">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<?php foreach (($bagOptions ?? []) as $bc => $bn): ?>
|
|
||||||
<option value="<?= esc((string) $bc, 'attr') ?>" <?= ($bagCode ?? '') === (string) $bc ? 'selected' : '' ?>>
|
|
||||||
<?= esc(trim((string) $bc . (($bn ?? '') !== '' ? ' · ' . $bn : ''))) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">구분</label>
|
|
||||||
<select name="cat" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[9rem]">
|
|
||||||
<option value="" <?= ($catFilter ?? '') === '' ? 'selected' : '' ?>>전체</option>
|
|
||||||
<?php foreach (($catLabels ?? []) as $ck => $lab): ?>
|
|
||||||
<option value="<?= esc($ck, 'attr') ?>" <?= ($catFilter ?? '') === $ck ? 'selected' : '' ?>><?= esc($lab) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<fieldset class="border border-gray-200 rounded px-2 py-1">
|
|
||||||
<legend class="text-xs text-gray-600 px-1">집계 대상</legend>
|
|
||||||
<label class="inline-flex items-center gap-1 mr-3 cursor-pointer">
|
|
||||||
<input type="radio" name="metric" value="qty" <?= ! $isAmt ? 'checked' : '' ?>/>
|
|
||||||
<span>수량</span>
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center gap-1 cursor-pointer">
|
|
||||||
<input type="radio" name="metric" value="amt" <?= $isAmt ? 'checked' : '' ?>/>
|
|
||||||
<span>금액</span>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<section class="p-3 bg-white shop-sales-report-section">
|
<table class="w-full data-table">
|
||||||
<style>
|
|
||||||
@media print {
|
|
||||||
@page {
|
|
||||||
size: A4 landscape;
|
|
||||||
margin: 5mm 6mm;
|
|
||||||
}
|
|
||||||
.shop-sales-report-section { padding: 0 !important; }
|
|
||||||
.shop-sales-scroll-wrap {
|
|
||||||
overflow: visible !important;
|
|
||||||
border: 1px solid #333 !important;
|
|
||||||
}
|
|
||||||
#shop-sales-table {
|
|
||||||
font-size: 7pt !important;
|
|
||||||
width: 100% !important;
|
|
||||||
table-layout: fixed !important;
|
|
||||||
}
|
|
||||||
#shop-sales-table th,
|
|
||||||
#shop-sales-table td {
|
|
||||||
min-width: 0 !important;
|
|
||||||
padding: 1px 2px !important;
|
|
||||||
white-space: normal !important;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
line-height: 1.12;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
#shop-sales-table th { font-size: 6.5pt !important; }
|
|
||||||
}
|
|
||||||
@media screen {
|
|
||||||
#shop-sales-table td.num-cell { white-space: nowrap; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="mb-2 text-center no-print">
|
|
||||||
<h1 class="text-lg font-bold m-0">지정 판매소별 판매현황</h1>
|
|
||||||
<p class="text-sm text-gray-700 m-1"><?= esc(trim(($lgName ?? '') . ' · ' . ($startDate ?? '') . ' ~ ' . ($endDate ?? '') . ' · 읍면동: ' . ($zoneLabel ?? '') . ' · 집계: ' . ($metricLabel ?? ''))) ?></p>
|
|
||||||
<p class="text-xs text-gray-500 m-0"><?= $isAmt ? '(단위: 원)' : '(단위: 매)' ?></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shop-sales-scroll-wrap border border-gray-300 overflow-x-auto">
|
|
||||||
<table class="w-full data-table text-xs" id="shop-sales-table">
|
|
||||||
<colgroup>
|
|
||||||
<col style="width: 14%;"/>
|
|
||||||
<col style="width: 7%;"/>
|
|
||||||
<col style="width: 16%;"/>
|
|
||||||
<col style="width: 6%;"/>
|
|
||||||
<?php for ($i = 0; $i < 12; $i++): ?>
|
|
||||||
<col style="width: <?= esc((string) round(57 / 12, 2), 'attr') ?>%;"/>
|
|
||||||
<?php endfor; ?>
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left pl-2">지정판매소</th>
|
<th>판매소명</th>
|
||||||
<th>대표자명</th>
|
<th>판매수량</th>
|
||||||
<th class="text-left pl-1">주소</th>
|
<th>판매금액</th>
|
||||||
<th>합계</th>
|
<th>반품수량</th>
|
||||||
<?php for ($m = 1; $m <= 12; $m++): ?>
|
<th>반품금액</th>
|
||||||
<th><?= $m ?>월</th>
|
<th>순판매수량</th>
|
||||||
<?php endfor; ?>
|
<th>순판매금액</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php foreach ($reportRows ?? [] as $rw): ?>
|
<?php
|
||||||
|
$totSaleQty = 0; $totSaleAmt = 0; $totRetQty = 0; $totRetAmt = 0;
|
||||||
|
foreach ($result as $row):
|
||||||
|
$totSaleQty += (int) $row->sale_qty;
|
||||||
|
$totSaleAmt += (int) $row->sale_amount;
|
||||||
|
$totRetQty += (int) $row->return_qty;
|
||||||
|
$totRetAmt += (int) $row->return_amount;
|
||||||
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-left pl-2 font-medium"><?= esc((string) ($rw['name'] ?? '')) ?></td>
|
<td class="text-left pl-2"><?= esc($row->bs_ds_name) ?></td>
|
||||||
<td class="text-center"><?= esc((string) ($rw['rep'] ?? '')) ?></td>
|
<td><?= number_format((int) $row->sale_qty) ?></td>
|
||||||
<td class="text-left pl-1"><?= esc((string) ($rw['address'] ?? '')) ?></td>
|
<td><?= number_format((int) $row->sale_amount) ?></td>
|
||||||
<td class="num-cell tabular-nums"><?= $fmtVal((float) ($rw['total'] ?? 0)) ?></td>
|
<td><?= number_format((int) $row->return_qty) ?></td>
|
||||||
<?php foreach (($rw['months'] ?? []) as $mv): ?>
|
<td><?= number_format((int) $row->return_amount) ?></td>
|
||||||
<td class="num-cell tabular-nums"><?= $fmtVal((float) $mv) ?></td>
|
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
|
||||||
<?php endforeach; ?>
|
<td class="font-bold"><?= number_format((int) $row->sale_amount - (int) $row->return_amount) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (($reportRows ?? []) === []): ?>
|
<?php if (empty($result)): ?>
|
||||||
<tr>
|
<tr><td colspan="7" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||||
<td colspan="<?= (int) $colCount ?>" class="text-center text-gray-400 py-6">조회된 데이터가 없습니다.</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<tr class="bg-gray-100 font-bold">
|
<tr class="font-bold bg-gray-100">
|
||||||
<td colspan="3" class="text-center">전체 합계</td>
|
<td class="text-center">합계</td>
|
||||||
<td class="num-cell tabular-nums"><?= $fmtVal((float) ($grandTotal ?? 0)) ?></td>
|
<td><?= number_format($totSaleQty) ?></td>
|
||||||
<?php foreach (($grandMonths ?? []) as $gm): ?>
|
<td><?= number_format($totSaleAmt) ?></td>
|
||||||
<td class="num-cell tabular-nums"><?= $fmtVal((float) $gm) ?></td>
|
<td><?= number_format($totRetQty) ?></td>
|
||||||
<?php endforeach; ?>
|
<td><?= number_format($totRetAmt) ?></td>
|
||||||
|
<td><?= number_format($totSaleQty - $totRetQty) ?></td>
|
||||||
|
<td><?= number_format($totSaleAmt - $totRetAmt) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const metric = <?= json_encode($isAmt ? 'amt' : 'qty', JSON_THROW_ON_ERROR) ?>;
|
|
||||||
const start = <?= json_encode((string) ($startDate ?? ''), JSON_THROW_ON_ERROR) ?>;
|
|
||||||
const end = <?= json_encode((string) ($endDate ?? ''), JSON_THROW_ON_ERROR) ?>;
|
|
||||||
let savedTitle = document.title;
|
|
||||||
function stamp() {
|
|
||||||
const d = new Date();
|
|
||||||
const p = (n) => String(n).padStart(2, '0');
|
|
||||||
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '_' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
|
|
||||||
}
|
|
||||||
window.addEventListener('beforeprint', function () {
|
|
||||||
savedTitle = document.title;
|
|
||||||
document.title = '지정판매소별판매현황_' + metric + '_' + start + '_' + end + '_' + stamp();
|
|
||||||
});
|
|
||||||
window.addEventListener('afterprint', function () {
|
|
||||||
document.title = savedTitle;
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,278 +1,134 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '봉투 수불 현황']) ?>
|
||||||
$refDate = (string) ($refDate ?? date('Y-m-d'));
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
$leadDays = (int) ($leadDays ?? 40);
|
|
||||||
$stockScope = (string) ($stockScope ?? 'all');
|
|
||||||
$salesScope = (string) ($salesScope ?? 'all');
|
|
||||||
$rows = is_array($rows ?? null) ? $rows : [];
|
|
||||||
$queried = (bool) ($queried ?? false);
|
|
||||||
$stockLabel = (string) ($stockLabel ?? 'ALL');
|
|
||||||
$salesLabel = (string) ($salesLabel ?? 'ALL');
|
|
||||||
|
|
||||||
$fmtKrRef = static function (string $ymd): string {
|
|
||||||
$ts = strtotime($ymd);
|
|
||||||
|
|
||||||
return $ts ? date('Y.m.d', $ts) . ' 현재' : $ymd;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 툴팁: 의미 + 계산(간단) */
|
|
||||||
$tipPage = "봉투 품목별로 재고가 며칠 버티는지, 언제·얼마나 발주할지 보는 수급·발주 계획표입니다.";
|
|
||||||
$tipLead = "의미: 발주 후 입고까지 걸리는 제작기일(일). 재고 소진 전에 발주하려는 여유.\n계산: 발주예정일 = 기준일 + 소진일수 − 보유일수";
|
|
||||||
$tipStock = "의미: 표에 넣을 현재고·총재고 범위.\n기존=바코드 미등록(수기), 바코드=등록 품목.";
|
|
||||||
$tipSales = "의미: 소진일수에 쓸 판매 속도 범위.\n최근 12개월 순판매(또는 바코드 판매) 월평균.";
|
|
||||||
$tipTotal = "의미: 지금·곧 쓸 수 있는 재고 합계.\n계산: 현재고 + 입고예정량";
|
|
||||||
$tipMonth = "의미: 요즘 한 달 판매 규모(평균).\n최근 12개월 월평균 판매량.";
|
|
||||||
$tipDepl = "의미: 이 판매 속도면 재고가 며칠 남는지.\n계산: (총재고 ÷ 월판매량) × 30";
|
|
||||||
$tipSched = "의미: 발주를 넣기 좋은 날(제작기일 반영).\n계산: 기준일 + 소진일수 − 보유일수. 기한 지남=빨간색·긴급";
|
|
||||||
$tipOrder = "의미: 그 시점에 맞춰 제안하는 추가 발주 장수.\n촉박하거나 발주예정일이 지난 품목만 표시.";
|
|
||||||
|
|
||||||
$printExtraLines = [
|
|
||||||
$fmtKrRef($refDate),
|
|
||||||
'적정재고 보유일수(제작기일): ' . $leadDays . '일',
|
|
||||||
'현재고: ' . $stockLabel . ' · 월평균판매량: ' . $salesLabel,
|
|
||||||
'※ 제작기일 ' . $leadDays . '일 기준으로 발주예정일 산정 (레거시 화면 유추)',
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
<?= view('components/print_header', [
|
|
||||||
'printTitle' => '쓰레기봉투 수급 계획',
|
|
||||||
'printExtraLines' => $printExtraLines,
|
|
||||||
]) ?>
|
|
||||||
|
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print">
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700 inline-flex items-center gap-1">
|
<span class="text-sm font-bold text-gray-700">봉투 수불 현황</span>
|
||||||
쓰레기봉투 수급 계획
|
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||||
<?= view('components/field_tooltip', ['text' => $tipPage, 'placement' => 'below']) ?>
|
|
||||||
</span>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">인쇄</button>
|
|
||||||
<a href="<?= base_url('dashboard') ?>" class="border border-gray-400 text-gray-700 px-3 py-1 rounded-sm text-sm hover:bg-gray-50">종료</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
<section class="p-2 bg-white border-b border-gray-200 no-print text-sm">
|
<form method="GET" action="<?= mgmt_url('reports/supply-demand') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<form method="get" action="<?= mgmt_url('reports/supply-demand') ?>" class="flex flex-wrap items-end gap-x-4 gap-y-3">
|
<label class="text-sm text-gray-600">시작일</label>
|
||||||
<input type="hidden" name="search" value="1"/>
|
<input type="date" name="start_date" value="<?= esc($startDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
|
<label class="text-sm text-gray-600">~</label>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<input type="date" name="end_date" value="<?= esc($endDate ?? '') ?>" class="border border-gray-300 rounded px-2 py-1 text-sm"/>
|
||||||
<label class="font-bold text-gray-700 whitespace-nowrap">기준일</label>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<input type="date" name="ref_date" value="<?= esc($refDate) ?>" class="border border-gray-300 rounded px-2 py-1" required/>
|
|
||||||
<span class="text-gray-600"><?= esc($fmtKrRef($refDate)) ?></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<label class="font-bold text-gray-700 whitespace-nowrap inline-flex items-center gap-0.5">
|
|
||||||
적정재고 보유일수
|
|
||||||
<?= view('components/field_tooltip', ['text' => $tipLead]) ?>
|
|
||||||
</label>
|
|
||||||
<input type="number" name="lead_days" value="<?= (int) $leadDays ?>" min="1" max="365"
|
|
||||||
class="border border-gray-300 rounded px-2 py-1 w-20 text-right"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset class="flex flex-wrap items-center gap-2 border-0 p-0 m-0">
|
|
||||||
<legend class="font-bold text-gray-700 whitespace-nowrap mr-1 inline-flex items-center gap-0.5">
|
|
||||||
현재고 선택 옵션
|
|
||||||
<?= view('components/field_tooltip', ['text' => $tipStock]) ?>
|
|
||||||
</legend>
|
|
||||||
<?php foreach (['all' => 'ALL', 'legacy' => '기존 봉투', 'barcode' => '바코드 봉투'] as $val => $lab): ?>
|
|
||||||
<label class="inline-flex items-center gap-1">
|
|
||||||
<input type="radio" name="stock_scope" value="<?= esc($val) ?>" <?= $stockScope === $val ? 'checked' : '' ?>/>
|
|
||||||
<?= esc($lab) ?>
|
|
||||||
</label>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset class="flex flex-wrap items-center gap-2 border-0 p-0 m-0">
|
|
||||||
<legend class="font-bold text-gray-700 whitespace-nowrap mr-1 inline-flex items-center gap-0.5">
|
|
||||||
월 평균판매량 선택 옵션
|
|
||||||
<?= view('components/field_tooltip', ['text' => $tipSales]) ?>
|
|
||||||
</legend>
|
|
||||||
<?php foreach (['all' => 'ALL', 'legacy' => '기존 봉투', 'barcode' => '바코드 봉투'] as $val => $lab): ?>
|
|
||||||
<label class="inline-flex items-center gap-1">
|
|
||||||
<input type="radio" name="sales_scope" value="<?= esc($val) ?>" <?= $salesScope === $val ? 'checked' : '' ?>/>
|
|
||||||
<?= esc($lab) ?>
|
|
||||||
</label>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm">조회</button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<?php if (! $queried): ?>
|
<div class="grid grid-cols-2 gap-4 mt-2">
|
||||||
<div class="m-2 p-3 border border-blue-200 bg-blue-50 text-sm text-blue-900 no-print">
|
<!-- 현재 재고 -->
|
||||||
기준일·옵션을 선택한 뒤 <strong>조회</strong>를 눌러 주세요.
|
<div class="border border-gray-300 rounded overflow-auto">
|
||||||
|
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||||
|
<span class="text-sm font-bold text-gray-700">현재 재고</span>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<table class="w-full data-table">
|
||||||
|
|
||||||
<div class="supply-plan-print-sheet">
|
|
||||||
<div class="supply-plan-print m-2 border border-gray-300 overflow-auto print:m-0">
|
|
||||||
<table class="w-full data-table text-sm supply-plan-table">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-100">
|
|
||||||
<th colspan="4" class="text-center border-b border-gray-300 sp-group-h">최근 발주 내역</th>
|
|
||||||
<th colspan="5" class="text-center border-b border-gray-300 border-l sp-group-h">현재고 및 예상 판매일수</th>
|
|
||||||
<th colspan="2" class="text-center border-b border-gray-300 border-l sp-group-h">추가발주 예정내역</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th class="sp-col-date">발주일자</th>
|
<th>봉투코드</th>
|
||||||
<th class="sp-col-name">봉투종류</th>
|
<th>봉투명</th>
|
||||||
<th class="sp-col-num text-right">발주량</th>
|
<th>재고수량</th>
|
||||||
<th class="sp-col-num text-right">발주시재고</th>
|
|
||||||
<th class="sp-col-num text-right border-l">현재고</th>
|
|
||||||
<th class="sp-col-num text-right">입고예정량</th>
|
|
||||||
<th class="sp-col-num text-right">
|
|
||||||
<span class="inline-flex items-center justify-end gap-0.5 w-full">총재고<?= view('components/field_tooltip', ['text' => $tipTotal, 'placement' => 'below']) ?></span>
|
|
||||||
</th>
|
|
||||||
<th class="sp-col-num text-right">
|
|
||||||
<span class="inline-flex items-center justify-end gap-0.5 w-full">월판매량<?= view('components/field_tooltip', ['text' => $tipMonth, 'placement' => 'below']) ?></span>
|
|
||||||
</th>
|
|
||||||
<th class="sp-col-num text-right">
|
|
||||||
<span class="inline-flex items-center justify-end gap-0.5 w-full">소진일수(일)<?= view('components/field_tooltip', ['text' => $tipDepl, 'placement' => 'below']) ?></span>
|
|
||||||
</th>
|
|
||||||
<th class="sp-col-date text-center border-l">
|
|
||||||
<span class="inline-flex items-center justify-center gap-0.5">발주예정일<?= view('components/field_tooltip', ['text' => $tipSched, 'placement' => 'below']) ?></span>
|
|
||||||
</th>
|
|
||||||
<th class="sp-col-num text-right">
|
|
||||||
<span class="inline-flex items-center justify-end gap-0.5 w-full">발주수량<?= view('components/field_tooltip', ['text' => $tipOrder, 'placement' => 'below']) ?></span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="text-right">
|
||||||
<?php if ($queried && $rows === []): ?>
|
<?php foreach ($inventory as $row): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="11" class="text-center text-gray-500 py-8">표시할 품목이 없습니다.</td>
|
<td class="text-center font-mono"><?= esc($row->bi_bag_code) ?></td>
|
||||||
</tr>
|
<td class="text-left pl-2"><?= esc($row->bi_bag_name) ?></td>
|
||||||
<?php endif; ?>
|
<td class="font-bold"><?= number_format((int) $row->bi_qty) ?></td>
|
||||||
<?php foreach ($rows as $row): ?>
|
|
||||||
<?php
|
|
||||||
$depl = (int) ($row['depletion_days'] ?? 0);
|
|
||||||
$deplDisplay = $depl <= 0 ? '—' : number_format($depl);
|
|
||||||
$sched = (string) ($row['schedule_date'] ?? '');
|
|
||||||
$schedOver = (bool) ($row['schedule_overdue'] ?? false);
|
|
||||||
$schedDisplay = '—';
|
|
||||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $sched, $m)) {
|
|
||||||
$y = (int) $m[1];
|
|
||||||
if ($y >= 1990 && $y <= 2200) {
|
|
||||||
$schedDisplay = $m[1] . '.' . $m[2] . '.' . $m[3];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td class="sp-col-date text-center"><?= ($row['last_order_date'] ?? '') !== '' ? esc(str_replace('-', '.', (string) $row['last_order_date'])) : '—' ?></td>
|
|
||||||
<td class="sp-col-name text-left"><?= esc((string) ($row['bag_name'] ?? $row['bag_code'] ?? '')) ?></td>
|
|
||||||
<td class="sp-col-num text-right tabular-nums"><?= (int) ($row['last_order_qty'] ?? 0) > 0 ? number_format((int) $row['last_order_qty']) : '—' ?></td>
|
|
||||||
<td class="sp-col-num text-right tabular-nums"><?= (int) ($row['stock_at_order'] ?? 0) > 0 ? number_format((int) $row['stock_at_order']) : '—' ?></td>
|
|
||||||
<td class="sp-col-num text-right tabular-nums border-l"><?= number_format((int) ($row['current_stock'] ?? 0)) ?></td>
|
|
||||||
<td class="sp-col-num text-right tabular-nums"><?= number_format((int) ($row['pending_inbound'] ?? 0)) ?></td>
|
|
||||||
<td class="sp-col-num text-right tabular-nums font-semibold"><?= number_format((int) ($row['total_stock'] ?? 0)) ?></td>
|
|
||||||
<td class="sp-col-num text-right tabular-nums"><?= number_format((int) ($row['monthly_avg_sales'] ?? 0)) ?></td>
|
|
||||||
<td class="sp-col-num text-right tabular-nums"><?= esc($deplDisplay) ?></td>
|
|
||||||
<td class="sp-col-date text-center border-l <?= $schedOver ? 'text-red-600 font-bold' : '' ?>"><?= esc($schedDisplay) ?></td>
|
|
||||||
<td class="sp-col-num text-right tabular-nums <?= (int) ($row['order_qty'] ?? 0) > 0 ? 'text-red-600 font-bold' : '' ?>">
|
|
||||||
<?= number_format((int) ($row['order_qty'] ?? 0)) ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($inventory)): ?>
|
||||||
|
<tr><td colspan="3" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 기간 입고 -->
|
||||||
|
<div class="border border-gray-300 rounded overflow-auto">
|
||||||
|
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||||
|
<span class="text-sm font-bold text-gray-700">기간 입고</span>
|
||||||
|
</div>
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>봉투코드</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>입고수량</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php foreach ($receiving as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->br_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->br_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $row->recv_qty) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($receiving)): ?>
|
||||||
|
<tr><td colspan="3" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 기간 판매 -->
|
||||||
|
<div class="border border-gray-300 rounded overflow-auto">
|
||||||
|
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||||
|
<span class="text-sm font-bold text-gray-700">기간 판매</span>
|
||||||
|
</div>
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>봉투코드</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>판매수량</th>
|
||||||
|
<th>반품수량</th>
|
||||||
|
<th>순판매</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php foreach ($sales as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $row->sale_qty) ?></td>
|
||||||
|
<td><?= number_format((int) $row->return_qty) ?></td>
|
||||||
|
<td class="font-bold"><?= number_format((int) $row->sale_qty - (int) $row->return_qty) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($sales)): ?>
|
||||||
|
<tr><td colspan="5" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 기간 불출 -->
|
||||||
|
<div class="border border-gray-300 rounded overflow-auto">
|
||||||
|
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1.5">
|
||||||
|
<span class="text-sm font-bold text-gray-700">기간 불출</span>
|
||||||
|
</div>
|
||||||
|
<table class="w-full data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>봉투코드</th>
|
||||||
|
<th>봉투명</th>
|
||||||
|
<th>불출수량</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-right">
|
||||||
|
<?php foreach ($issues as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-center font-mono"><?= esc($row->bi2_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bi2_bag_name) ?></td>
|
||||||
|
<td><?= number_format((int) $row->issue_qty) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($issues)): ?>
|
||||||
|
<tr><td colspan="3" class="text-center text-gray-400 py-4">데이터가 없습니다.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.field-tip { position: relative; display: inline-flex; vertical-align: middle; }
|
|
||||||
.field-tip-btn {
|
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
|
||||||
width: 14px; height: 14px; font-size: 10px; font-weight: 700; line-height: 1;
|
|
||||||
color: #6b7280; background: #f3f4f6; border: 1px solid #d1d5db; border-radius: 50%;
|
|
||||||
cursor: help; user-select: none;
|
|
||||||
}
|
|
||||||
.field-tip-btn:hover, .field-tip-btn:focus { color: #1d4ed8; border-color: #93c5fd; background: #eff6ff; outline: none; }
|
|
||||||
.field-tip-panel {
|
|
||||||
position: absolute; z-index: 60; left: 50%; transform: translateX(-50%);
|
|
||||||
bottom: calc(100% + 6px); width: max-content; max-width: 280px;
|
|
||||||
padding: 0.35rem 0.5rem; border-radius: 4px;
|
|
||||||
background: #1f2937; color: #f9fafb; font-size: 11px; font-weight: 500; line-height: 1.35;
|
|
||||||
text-align: left; white-space: pre-line; box-shadow: 0 2px 8px rgba(0,0,0,.15);
|
|
||||||
opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .12s, visibility .12s;
|
|
||||||
}
|
|
||||||
.field-tip--below .field-tip-panel { bottom: auto; top: calc(100% + 6px); }
|
|
||||||
.field-tip:hover .field-tip-panel,
|
|
||||||
.field-tip:focus-within .field-tip-panel { opacity: 1; visibility: visible; }
|
|
||||||
|
|
||||||
.supply-plan-table thead th { font-size: 0.75rem; line-height: 1.2; padding: 0.35rem 0.25rem; overflow: visible; }
|
|
||||||
.supply-plan-table thead th .field-tip-panel { max-width: 260px; }
|
|
||||||
.supply-plan-table tbody td { padding: 0.25rem 0.35rem; }
|
|
||||||
|
|
||||||
@media screen {
|
|
||||||
.supply-plan-print { overflow-x: auto; }
|
|
||||||
.supply-plan-table { min-width: 960px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
@page {
|
|
||||||
size: A4 portrait;
|
|
||||||
margin: 10mm 8mm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-print { display: none !important; }
|
|
||||||
|
|
||||||
.supply-plan-print-sheet {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.supply-plan-print {
|
|
||||||
border: none !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.supply-plan-table.data-table {
|
|
||||||
min-width: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
table-layout: fixed !important;
|
|
||||||
font-size: 6px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.supply-plan-table.data-table th,
|
|
||||||
.supply-plan-table.data-table td {
|
|
||||||
white-space: normal !important;
|
|
||||||
word-break: keep-all;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
padding: 1px 1px !important;
|
|
||||||
line-height: 1.1;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.supply-plan-table .sp-group-h {
|
|
||||||
font-size: 5px !important;
|
|
||||||
padding: 1px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 세로 A4: 날짜 2×4.5% + 품목 11% + 수치 8×10% = 100% */
|
|
||||||
.supply-plan-table .sp-col-date {
|
|
||||||
width: 4.5%;
|
|
||||||
font-size: 5px !important;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.supply-plan-table .sp-col-name {
|
|
||||||
width: 11%;
|
|
||||||
text-align: left !important;
|
|
||||||
font-size: 5px !important;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.supply-plan-table .sp-col-num {
|
|
||||||
width: 10%;
|
|
||||||
font-size: 5px !important;
|
|
||||||
text-align: right !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,232 +1,62 @@
|
|||||||
<?php
|
<?= view('components/print_header', ['printTitle' => '년 판매 현황']) ?>
|
||||||
declare(strict_types=1);
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
|
|
||||||
/** @var int $year */
|
|
||||||
/** @var string $gugunCode */
|
|
||||||
/** @var int $saIdx */
|
|
||||||
/** @var list<object> $agencies */
|
|
||||||
/** @var list<array{code: string, name: string}> $gugunOptions */
|
|
||||||
/** @var list<array{id: string, label: string}> $colSpec */
|
|
||||||
/** @var list<array{name: string, lines: list<array<string,mixed>>}> $itemBlocks */
|
|
||||||
/** @var array{name: string, lines: list<array<string,mixed>>} $footerBlock */
|
|
||||||
/** @var bool $hasBsFee */
|
|
||||||
/** @var string $lgName */
|
|
||||||
/** @var string $gugunLabel */
|
|
||||||
/** @var string $agencyLabel */
|
|
||||||
/** @var list<string> $printExtraLines */
|
|
||||||
/** @var bool $hasYearlyData */
|
|
||||||
|
|
||||||
$yMax = (int) date('Y') + 1;
|
|
||||||
$yMin = 2020;
|
|
||||||
|
|
||||||
$exportParams = array_merge([
|
|
||||||
'year' => (string) ($year ?? date('Y')),
|
|
||||||
'export' => '1',
|
|
||||||
], array_filter([
|
|
||||||
'gugun_code' => (string) ($gugunCode ?? ''),
|
|
||||||
'sa_idx' => (int) ($saIdx ?? 0) > 0 ? (string) (int) ($saIdx ?? 0) : '',
|
|
||||||
], static fn ($v): bool => $v !== '' && $v !== null && $v !== 0));
|
|
||||||
$excelUrl = mgmt_url('reports/yearly-sales?' . http_build_query($exportParams));
|
|
||||||
|
|
||||||
$colCount = 2 + count($colSpec ?? []);
|
|
||||||
$nMetricCols = max(1, count($colSpec ?? []));
|
|
||||||
$metricColPct = round(86 / $nMetricCols, 4);
|
|
||||||
|
|
||||||
$fmtMeasureCell = static function (array $cell, string $measureKey, bool $hasBsFee): string {
|
|
||||||
if ($measureKey === 'fee' && ! $hasBsFee) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
if ($measureKey === 'qty') {
|
|
||||||
return number_format((int) ($cell['qty'] ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
return number_format((int) round((float) ($cell[$measureKey] ?? 0)));
|
|
||||||
};
|
|
||||||
?>
|
|
||||||
<?= view('components/print_header', [
|
|
||||||
'printTitle' => ((int) ($year ?? date('Y'))) . '년 판매 현황',
|
|
||||||
'printExtraLines' => $printExtraLines ?? [],
|
|
||||||
]) ?>
|
|
||||||
|
|
||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel no-print">
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<span class="text-sm font-bold text-gray-700">년 판매 현황</span>
|
<span class="text-sm font-bold text-gray-700">년 판매 현황 (월별)</span>
|
||||||
<div class="flex flex-wrap gap-2">
|
<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>
|
||||||
<button type="button" onclick="window.print()" class="border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
|
||||||
<a href="<?= esc($excelUrl, 'attr') ?>" class="inline-flex items-center border border-green-600 text-green-700 px-3 py-1 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="p-2 bg-white border-b border-gray-200">
|
||||||
<section class="p-3 bg-white border-b border-gray-200 no-print">
|
<form method="GET" action="<?= mgmt_url('reports/yearly-sales') ?>" class="flex flex-wrap items-center gap-2">
|
||||||
<form method="GET" action="<?= mgmt_url('reports/yearly-sales') ?>" class="flex flex-wrap items-end gap-3 text-sm">
|
<label class="text-sm text-gray-600">연도</label>
|
||||||
<div>
|
<select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm">
|
||||||
<label class="block text-gray-600 mb-0.5">조회 년도</label>
|
<?php for ($y = (int) date('Y'); $y >= 2020; $y--): ?>
|
||||||
<select name="year" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[7rem]">
|
|
||||||
<?php for ($y = $yMax; $y >= $yMin; $y--): ?>
|
|
||||||
<option value="<?= $y ?>" <?= (int)($year ?? date('Y')) === $y ? 'selected' : '' ?>><?= $y ?>년</option>
|
<option value="<?= $y ?>" <?= (int)($year ?? date('Y')) === $y ? 'selected' : '' ?>><?= $y ?>년</option>
|
||||||
<?php endfor; ?>
|
<?php endfor; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
<button type="submit" class="bg-btn-search text-white px-4 py-1 rounded-sm text-sm">조회</button>
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">구·군</label>
|
|
||||||
<select name="gugun_code" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[10rem] max-w-[18rem]">
|
|
||||||
<option value="">전체</option>
|
|
||||||
<?php foreach ($gugunOptions ?? [] as $g): ?>
|
|
||||||
<?php $gc = (string) ($g['code'] ?? ''); ?>
|
|
||||||
<option value="<?= esc($gc, 'attr') ?>" <?= ($gugunCode ?? '') === $gc ? 'selected' : '' ?>><?= esc((string) ($g['name'] ?? $gc)) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-gray-600 mb-0.5">대행소</label>
|
|
||||||
<select name="sa_idx" class="border border-gray-300 rounded px-2 py-1 text-sm min-w-[12rem] max-w-[20rem]">
|
|
||||||
<option value="0">전체</option>
|
|
||||||
<?php foreach ($agencies ?? [] as $agency): ?>
|
|
||||||
<?php $aid = (int) ($agency->sa_idx ?? 0); ?>
|
|
||||||
<option value="<?= esc((string) $aid) ?>" <?= (int) ($saIdx ?? 0) === $aid ? 'selected' : '' ?>>
|
|
||||||
<?= esc(trim((string) ($agency->sa_name ?? ''))) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-1.5 rounded-sm text-sm">조회</button>
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
<div class="border border-gray-300 overflow-auto mt-2">
|
||||||
<section class="p-3 bg-white yearly-sales-report-section">
|
<table class="w-full data-table">
|
||||||
<style>
|
|
||||||
/* 화면: 가로 스크롤. 인쇄: 가로 용지 + 작은 글자 + 셀 줄바꿈으로 한 페이지에 맞춤 */
|
|
||||||
@media print {
|
|
||||||
@page {
|
|
||||||
size: A4 landscape;
|
|
||||||
margin: 5mm 6mm;
|
|
||||||
}
|
|
||||||
.yearly-sales-report-section {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
.yearly-sales-scroll-wrap {
|
|
||||||
overflow: visible !important;
|
|
||||||
border: 1px solid #333 !important;
|
|
||||||
max-width: none !important;
|
|
||||||
}
|
|
||||||
#yearly-sales-table {
|
|
||||||
font-size: 7pt !important;
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100% !important;
|
|
||||||
table-layout: fixed !important;
|
|
||||||
}
|
|
||||||
#yearly-sales-table th,
|
|
||||||
#yearly-sales-table td {
|
|
||||||
min-width: 0 !important;
|
|
||||||
max-width: none !important;
|
|
||||||
padding: 1px 2px !important;
|
|
||||||
white-space: normal !important;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
line-height: 1.15;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
#yearly-sales-table th {
|
|
||||||
font-size: 6.5pt !important;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen {
|
|
||||||
#yearly-sales-table td.tabular-nums {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="mb-2 text-center no-print">
|
|
||||||
<h1 class="text-lg font-bold m-0"><?= (int) ($year ?? date('Y')) ?>년 판매 현황</h1>
|
|
||||||
<p class="text-sm text-gray-700 m-1"><?= esc(trim(($lgName ?? '') . ' · 구·군: ' . ($gugunLabel ?? '') . ' · 대행소: ' . ($agencyLabel ?? ''))) ?></p>
|
|
||||||
<p class="text-xs text-gray-500 m-0">(단위: 매 / 원)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="yearly-sales-scroll-wrap border border-gray-300 overflow-x-auto">
|
|
||||||
<table class="w-full data-table text-xs sm:text-sm" id="yearly-sales-table">
|
|
||||||
<colgroup>
|
|
||||||
<col style="width: 9%;"/>
|
|
||||||
<col style="width: 5%;"/>
|
|
||||||
<?php foreach (($colSpec ?? []) as $_): ?>
|
|
||||||
<col style="width: <?= esc((string) $metricColPct, 'attr') ?>%;"/>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="align-middle min-w-0 sm:min-w-[7rem] max-w-[10rem] sm:max-w-[12rem] text-left pl-2">품목</th>
|
<th>봉투코드</th>
|
||||||
<th class="align-middle min-w-0 sm:min-w-[4.5rem]">구분</th>
|
<th>봉투명</th>
|
||||||
<?php foreach ($colSpec ?? [] as $col): ?>
|
<th>1월</th><th>2월</th><th>3월</th><th>4월</th><th>5월</th><th>6월</th>
|
||||||
<th class="align-middle text-center min-w-0 sm:min-w-[4.5rem] border-l border-gray-200"><?= esc((string) ($col['label'] ?? '')) ?></th>
|
<th>7월</th><th>8월</th><th>9월</th><th>10월</th><th>11월</th><th>12월</th>
|
||||||
<?php endforeach; ?>
|
<th class="bg-gray-100">합계</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-right">
|
<tbody class="text-right">
|
||||||
<?php if (! ($hasYearlyData ?? false)): ?>
|
<?php
|
||||||
|
$grandTotal = array_fill(1, 13, 0); // 1~12 + 13=total
|
||||||
|
foreach ($result as $row):
|
||||||
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="<?= (int) $colCount ?>" class="text-center text-gray-400 py-6">조회된 데이터가 없습니다.</td>
|
<td class="text-center font-mono"><?= esc($row->bs_bag_code) ?></td>
|
||||||
|
<td class="text-left pl-2"><?= esc($row->bs_bag_name) ?></td>
|
||||||
|
<?php for ($m = 1; $m <= 12; $m++):
|
||||||
|
$key = 'm' . sprintf('%02d', $m);
|
||||||
|
$val = (int) $row->$key;
|
||||||
|
$grandTotal[$m] += $val;
|
||||||
|
?>
|
||||||
|
<td><?= $val > 0 ? number_format($val) : '-' ?></td>
|
||||||
|
<?php endfor; ?>
|
||||||
|
<?php $grandTotal[13] += (int) $row->total; ?>
|
||||||
|
<td class="font-bold bg-gray-50"><?= number_format((int) $row->total) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($result)): ?>
|
||||||
|
<tr><td colspan="15" class="text-center text-gray-400 py-4">조회된 데이터가 없습니다.</td></tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($itemBlocks ?? [] as $block): ?>
|
<tr class="font-bold bg-gray-100">
|
||||||
<?php $lines = $block['lines'] ?? []; ?>
|
<td colspan="2" class="text-center">합계</td>
|
||||||
<?php foreach ($lines as $liIdx => $li): ?>
|
<?php for ($m = 1; $m <= 12; $m++): ?>
|
||||||
<tr class="odd:bg-white even:bg-gray-50/80">
|
<td><?= $grandTotal[$m] > 0 ? number_format($grandTotal[$m]) : '-' ?></td>
|
||||||
<?php if ($liIdx === 0): ?>
|
<?php endfor; ?>
|
||||||
<td rowspan="4" class="text-left align-top pl-2 pt-1 font-medium border-r border-gray-200"><?= esc((string) ($block['name'] ?? '')) ?></td>
|
<td class="bg-gray-200"><?= number_format($grandTotal[13]) ?></td>
|
||||||
<?php endif; ?>
|
|
||||||
<td class="text-left pl-2 border-r border-gray-100"><?= esc((string) ($li['measure'] ?? '')) ?></td>
|
|
||||||
<?php
|
|
||||||
$cells = (array) ($li['cells'] ?? []);
|
|
||||||
$mk = (string) ($li['measureKey'] ?? '');
|
|
||||||
?>
|
|
||||||
<?php foreach ($colSpec ?? [] as $col): ?>
|
|
||||||
<?php $cid = (string) ($col['id'] ?? ''); ?>
|
|
||||||
<?php $cell = (array) ($cells[$cid] ?? ['qty' => 0, 'amt' => 0.0, 'fee' => 0.0, 'levy' => 0.0]); ?>
|
|
||||||
<td class="border-l border-gray-100 tabular-nums"><?= $fmtMeasureCell($cell, $mk, (bool) ($hasBsFee ?? false)) ?></td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<?php $fLines = $footerBlock['lines'] ?? []; ?>
|
|
||||||
<?php foreach ($fLines as $fIdx => $li): ?>
|
|
||||||
<tr class="bg-amber-50 font-semibold border-t-2 border-amber-200">
|
|
||||||
<?php if ($fIdx === 0): ?>
|
|
||||||
<td rowspan="4" class="text-center align-middle text-amber-900 border-r border-amber-200"><?= esc((string) ($footerBlock['name'] ?? '전체 합계')) ?></td>
|
|
||||||
<?php endif; ?>
|
|
||||||
<td class="text-left pl-2 border-r border-amber-100"><?= esc((string) ($li['measure'] ?? '')) ?></td>
|
|
||||||
<?php
|
|
||||||
$cells = (array) ($li['cells'] ?? []);
|
|
||||||
$mk = (string) ($li['measureKey'] ?? '');
|
|
||||||
?>
|
|
||||||
<?php foreach ($colSpec ?? [] as $col): ?>
|
|
||||||
<?php $cid = (string) ($col['id'] ?? ''); ?>
|
|
||||||
<?php $cell = (array) ($cells[$cid] ?? ['qty' => 0, 'amt' => 0.0, 'fee' => 0.0, 'levy' => 0.0]); ?>
|
|
||||||
<td class="border-l border-amber-100 tabular-nums"><?= $fmtMeasureCell($cell, $mk, (bool) ($hasBsFee ?? false)) ?></td>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const year = <?= json_encode((int) ($year ?? (int) date('Y')), JSON_THROW_ON_ERROR) ?>;
|
|
||||||
let savedTitle = document.title;
|
|
||||||
function stamp() {
|
|
||||||
const d = new Date();
|
|
||||||
const p = (n) => String(n).padStart(2, '0');
|
|
||||||
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '_' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
|
|
||||||
}
|
|
||||||
window.addEventListener('beforeprint', function () {
|
|
||||||
savedTitle = document.title;
|
|
||||||
document.title = '년판매현황_' + year + '_' + stamp();
|
|
||||||
});
|
|
||||||
window.addEventListener('afterprint', function () {
|
|
||||||
document.title = savedTitle;
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,301 +1,74 @@
|
|||||||
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
|
||||||
<span class="text-sm font-bold text-gray-700">주문 접수</span>
|
<span class="text-sm font-bold text-gray-700">주문 접수</span>
|
||||||
</section>
|
</section>
|
||||||
<div class="border border-gray-300 p-4 mt-2 bg-white">
|
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-4xl">
|
||||||
<form action="<?= mgmt_url('shop-orders/store') ?>" method="POST" class="space-y-4">
|
<form action="<?= mgmt_url('shop-orders/store') ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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>
|
<label class="block text-sm font-bold text-gray-700 w-28">판매소 <span class="text-red-500">*</span></label>
|
||||||
<input id="shop-search" class="border border-gray-300 rounded px-3 py-1.5 text-sm w-72" type="text" list="shop-search-list" placeholder="코드/사업자번호/대표자명/상호/전화/주소"/>
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-60" name="so_ds_idx" required>
|
||||||
<datalist id="shop-search-list">
|
|
||||||
<?php foreach ($shops as $shop): ?>
|
|
||||||
<option value="<?= esc(trim(($shop->ds_shop_no ?? '') . ' ' . ($shop->ds_name ?? '') . ' ' . ($shop->ds_rep_name ?? '') . ' ' . ($shop->ds_biz_no ?? '') . ' ' . ($shop->ds_tel ?? '') . ' ' . ($shop->ds_addr ?? ''))) ?>"></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</datalist>
|
|
||||||
</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-72" name="so_ds_idx" required>
|
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach ($shops as $shop): ?>
|
<?php foreach ($shops as $shop): ?>
|
||||||
<option
|
<option value="<?= esc($shop->ds_idx) ?>" <?= (int) old('so_ds_idx') === (int) $shop->ds_idx ? 'selected' : '' ?>>
|
||||||
value="<?= esc($shop->ds_idx) ?>"
|
<?= esc($shop->ds_name) ?>
|
||||||
data-shop-no="<?= esc((string) ($shop->ds_shop_no ?? '')) ?>"
|
|
||||||
data-name="<?= esc((string) ($shop->ds_name ?? '')) ?>"
|
|
||||||
data-rep-name="<?= esc((string) ($shop->ds_rep_name ?? '')) ?>"
|
|
||||||
data-rep-phone="<?= esc((string) ($shop->ds_rep_phone ?? '')) ?>"
|
|
||||||
data-tel="<?= esc((string) ($shop->ds_tel ?? '')) ?>"
|
|
||||||
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 ?? '')) ?>"
|
|
||||||
<?= (int) old('so_ds_idx') === (int) $shop->ds_idx ? 'selected' : '' ?>
|
|
||||||
>
|
|
||||||
<?= esc(($shop->ds_shop_no ? '[' . $shop->ds_shop_no . '] ' : '') . $shop->ds_name) ?>
|
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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 id="shop-info-code" 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-rep" 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>
|
|
||||||
<tr><th class="text-left py-1">가상계좌</th><td id="shop-info-va" class="py-1 text-gray-700">-</td></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</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 bg-gray-100" type="date" value="<?= esc(date('Y-m-d')) ?>" readonly/>
|
|
||||||
<span class="text-xs text-gray-500">배달일 기본값은 접수일 다음날입니다.</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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>
|
<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="so_delivery_date" type="date" value="<?= esc(old('so_delivery_date', date('Y-m-d', strtotime('+1 day')))) ?>" required/>
|
<input class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="so_delivery_date" type="date" value="<?= esc(old('so_delivery_date', date('Y-m-d', strtotime('+1 day')))) ?>" required/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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>
|
<label class="block text-sm font-bold text-gray-700 w-28">결제방법 <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-44" name="so_payment_type" required>
|
<select class="border border-gray-300 rounded px-3 py-1.5 text-sm w-44" name="so_payment_type" required>
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<option value="이체" <?= old('so_payment_type') === '이체' ? 'selected' : '' ?>>이체</option>
|
<option value="이체" <?= old('so_payment_type') === '이체' ? 'selected' : '' ?>>이체</option>
|
||||||
<option value="가상계좌" <?= old('so_payment_type') === '가상계좌' ? 'selected' : '' ?>>가상계좌</option>
|
<option value="가상계좌" <?= old('so_payment_type') === '가상계좌' ? 'selected' : '' ?>>가상계좌</option>
|
||||||
</select>
|
</select>
|
||||||
<span id="payment-guide" class="text-xs text-gray-500"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<label class="block text-sm font-bold text-gray-700 mb-2">주문 품목</label>
|
||||||
<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">
|
<div class="border border-gray-300 overflow-auto">
|
||||||
<table class="w-full data-table text-sm">
|
<table class="w-full data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-14">순번</th>
|
<th class="w-16">순번</th>
|
||||||
<th class="w-48">품목</th>
|
<th>봉투</th>
|
||||||
<th class="w-36">1박스(낱장/판매가)</th>
|
<th class="w-32">수량</th>
|
||||||
<th class="w-36">1팩(낱장/판매가)</th>
|
|
||||||
<th class="w-24">단가</th>
|
|
||||||
<th class="w-28">주문수량</th>
|
|
||||||
<th class="w-28">금액</th>
|
|
||||||
<th class="w-32">포장(박스/팩/낱장)</th>
|
|
||||||
<th class="w-20">행삭제</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="order-rows">
|
<tbody>
|
||||||
<?php for ($i = 0; $i < 3; $i++): ?>
|
<?php for ($i = 0; $i < 3; $i++): ?>
|
||||||
<tr class="order-row">
|
<tr>
|
||||||
<td class="text-center row-no"><?= $i + 1 ?></td>
|
<td class="text-center"><?= $i + 1 ?></td>
|
||||||
<td>
|
<td>
|
||||||
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full bag-code-select" name="item_bag_code[]">
|
<select class="border border-gray-300 rounded px-2 py-1 text-sm w-full" name="item_bag_code[]">
|
||||||
<option value="">선택</option>
|
<option value="">선택</option>
|
||||||
<?php foreach ($bagCodes as $cd): ?>
|
<?php foreach ($bagCodes as $cd): ?>
|
||||||
<?php $code = (string) $cd->cd_code; $price = $priceMap[$code] ?? null; $unit = $unitMap[$code] ?? null; ?>
|
<option value="<?= esc($cd->cd_code) ?>">
|
||||||
<option value="<?= esc($code) ?>" data-unit-price="<?= esc((string) (int) ($price->bp_consumer ?? 0)) ?>" data-box-sheets="<?= esc((string) (int) ($unit->pu_total_per_box ?? 0)) ?>" data-box-packs="<?= esc((string) (int) ($unit->pu_box_per_pack ?? 0)) ?>" data-pack-sheets="<?= esc((string) (int) ($unit->pu_pack_per_sheet ?? 0)) ?>">
|
|
||||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_name) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right px-2 box-info-cell">0 / 0</td>
|
<td>
|
||||||
<td class="text-right px-2 pack-info-cell">0 / 0</td>
|
<input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right" name="item_qty[]" type="number" min="0" value="0"/>
|
||||||
<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><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-amount-input" type="number" min="0" step="1" value="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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endfor; ?>
|
<?php endfor; ?>
|
||||||
</tbody>
|
</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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 pt-2">
|
<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>
|
<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="<?= mgmt_url('shop-orders') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
<a href="<?= mgmt_url('shop-orders') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<template id="order-row-template">
|
|
||||||
<tr class="order-row">
|
|
||||||
<td class="text-center row-no">1</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; $price = $priceMap[$code] ?? null; $unit = $unitMap[$code] ?? null; ?>
|
|
||||||
<option value="<?= esc($code) ?>" data-unit-price="<?= esc((string) (int) ($price->bp_consumer ?? 0)) ?>" data-box-sheets="<?= esc((string) (int) ($unit->pu_total_per_box ?? 0)) ?>" data-box-packs="<?= esc((string) (int) ($unit->pu_box_per_pack ?? 0)) ?>" data-pack-sheets="<?= esc((string) (int) ($unit->pu_pack_per_sheet ?? 0)) ?>">
|
|
||||||
<?= esc($cd->cd_code) ?> — <?= esc($cd->cd_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><input class="border border-gray-300 rounded px-2 py-1 text-sm w-full text-right item-amount-input" type="number" min="0" step="1" value="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>
|
|
||||||
(function () {
|
|
||||||
const shopSearch = document.getElementById('shop-search');
|
|
||||||
const shopSelect = document.getElementById('shop-select');
|
|
||||||
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 = shopSelect.closest('form');
|
|
||||||
function nf(n) { return 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-name').textContent = opt?.dataset?.name || '-';
|
|
||||||
document.getElementById('shop-info-rep').textContent = opt?.dataset?.repName || '-';
|
|
||||||
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 matchShopByKeyword(keyword) {
|
|
||||||
const q = (keyword || '').trim().toLowerCase();
|
|
||||||
if (!q) { return; }
|
|
||||||
for (let i = 0; i < shopSelect.options.length; i++) {
|
|
||||||
const opt = shopSelect.options[i];
|
|
||||||
const merged = [opt.dataset.shopNo || '', opt.dataset.name || '', opt.dataset.repName || '', opt.dataset.tel || '', opt.dataset.address || '', opt.text || ''].join(' ').toLowerCase();
|
|
||||||
if (merged.includes(q)) { shopSelect.selectedIndex = i; updateShopInfo(); return; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function calcRow(row, source) {
|
|
||||||
const select = row.querySelector('.bag-code-select');
|
|
||||||
const qtyInput = row.querySelector('.item-qty-input');
|
|
||||||
const amountInput = row.querySelector('.item-amount-input');
|
|
||||||
const selected = select.options[select.selectedIndex];
|
|
||||||
let 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 boxPacks = parseInt(selected?.dataset?.boxPacks || '0', 10) || 0;
|
|
||||||
const packSheets = parseInt(selected?.dataset?.packSheets || '0', 10) || 0;
|
|
||||||
const rawAmount = parseInt(amountInput?.value || '0', 10) || 0;
|
|
||||||
if (source === 'amount' && unitPrice > 0) {
|
|
||||||
qty = Math.max(0, Math.round(rawAmount / unitPrice));
|
|
||||||
qtyInput.value = String(qty);
|
|
||||||
}
|
|
||||||
let box = 0, pack = 0, 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('.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);
|
|
||||||
if (amountInput && source !== 'amount') {
|
|
||||||
amountInput.value = String(amount);
|
|
||||||
}
|
|
||||||
const innerPackCount = box * boxPacks;
|
|
||||||
const innerSheetCount = box * boxSheets;
|
|
||||||
row.querySelector('.pack-result-cell').textContent = '박스=' + nf(box) + '(내부 팩=' + nf(innerPackCount) + ', 내부 낱장=' + nf(innerSheetCount) + '), 잔여 팩=' + nf(pack) + ', 잔여 낱장=' + nf(sheet);
|
|
||||||
return { qty, amount, box, pack, sheet };
|
|
||||||
}
|
|
||||||
function recalcAllRows(sourceRow, sourceType) {
|
|
||||||
let sumQty = 0, sumAmount = 0, sumBox = 0, sumPack = 0, sumSheet = 0;
|
|
||||||
document.querySelectorAll('.order-row').forEach((row, index) => {
|
|
||||||
const noCell = row.querySelector('.row-no');
|
|
||||||
if (noCell) {
|
|
||||||
noCell.textContent = String(index + 1);
|
|
||||||
}
|
|
||||||
const source = row === sourceRow ? sourceType : 'qty';
|
|
||||||
const r = calcRow(row, source);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
shopSearch?.addEventListener('change', (e) => matchShopByKeyword(e.target.value));
|
|
||||||
shopSearch?.addEventListener('blur', (e) => matchShopByKeyword(e.target.value));
|
|
||||||
shopSelect?.addEventListener('change', updateShopInfo);
|
|
||||||
paymentType?.addEventListener('change', updateShopInfo);
|
|
||||||
orderRows?.addEventListener('change', function (e) {
|
|
||||||
const row = e.target.closest('.order-row');
|
|
||||||
if (e.target.closest('.bag-code-select') || e.target.closest('.item-qty-input')) {
|
|
||||||
recalcAllRows(row, 'qty');
|
|
||||||
} else if (e.target.closest('.item-amount-input')) {
|
|
||||||
recalcAllRows(row, 'amount');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
orderRows?.addEventListener('input', function (e) {
|
|
||||||
const row = e.target.closest('.order-row');
|
|
||||||
if (e.target.closest('.item-qty-input')) {
|
|
||||||
recalcAllRows(row, 'qty');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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(null, 'qty');
|
|
||||||
});
|
|
||||||
addRowButton?.addEventListener('click', function () {
|
|
||||||
if (!rowTemplate || !orderRows) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fragment = rowTemplate.content.cloneNode(true);
|
|
||||||
orderRows.appendChild(fragment);
|
|
||||||
recalcAllRows(null, 'qty');
|
|
||||||
});
|
|
||||||
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(null, 'qty');
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
<th>판매소</th>
|
<th>판매소</th>
|
||||||
<th>접수일</th>
|
<th>접수일</th>
|
||||||
<th>배달일</th>
|
<th>배달일</th>
|
||||||
<th>접수채널</th>
|
|
||||||
<th>결제</th>
|
<th>결제</th>
|
||||||
<th>입금</th>
|
<th>입금</th>
|
||||||
<th>수령</th>
|
<th>수령</th>
|
||||||
@@ -43,12 +42,6 @@
|
|||||||
<td class="text-left pl-2"><?= esc($row->so_ds_name) ?></td>
|
<td class="text-left pl-2"><?= esc($row->so_ds_name) ?></td>
|
||||||
<td class="text-center"><?= esc($row->so_order_date) ?></td>
|
<td class="text-center"><?= esc($row->so_order_date) ?></td>
|
||||||
<td class="text-center"><?= esc($row->so_delivery_date) ?></td>
|
<td class="text-center"><?= esc($row->so_delivery_date) ?></td>
|
||||||
<td class="text-center">
|
|
||||||
<?php
|
|
||||||
$channelMap = ['phone' => '전화', 'web' => '웹', 'app' => '앱', 'counter' => '창구'];
|
|
||||||
echo esc($channelMap[$row->so_channel ?? ''] ?? ($row->so_channel ?? '전화'));
|
|
||||||
?>
|
|
||||||
</td>
|
|
||||||
<td class="text-center"><?= esc($row->so_payment_type) ?></td>
|
<td class="text-center"><?= esc($row->so_payment_type) ?></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<?php
|
<?php
|
||||||
@@ -79,7 +72,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($list)): ?>
|
<?php if (empty($list)): ?>
|
||||||
<tr><td colspan="12" class="text-center text-gray-400 py-4">등록된 주문이 없습니다.</td></tr>
|
<tr><td colspan="11" class="text-center text-gray-400 py-4">등록된 주문이 없습니다.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
/**
|
|
||||||
* 인증 페이지 공통 셸 — gov-portal 디자인.
|
|
||||||
* 사용: 자식 뷰 상단에서 $this->extend('auth/_shell'),
|
|
||||||
* 섹션 'heading'(카드 제목)·'content'(본문) 정의.
|
|
||||||
* 선택 변수: $subtitle(카드 헤더 소제목), $cardMax(예: 'max-w-lg', 기본 'max-w-md')
|
|
||||||
*/
|
|
||||||
$cardMax = $cardMax ?? 'max-w-md';
|
|
||||||
$subtitle = $subtitle ?? '종량제 쓰레기봉투 물류시스템';
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
|
||||||
<title><?= esc($pageTitle ?? '종량제 시스템') ?></title>
|
|
||||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css"/>
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
|
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
fontFamily: { sans: ['Pretendard', '"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
|
||||||
colors: {
|
|
||||||
'navy': '#1a2b4b', 'title-bar': '#1a2b4b', 'portal-bg': '#f0f4f8',
|
|
||||||
'btn-search': '#243a5e', 'btn-exit': '#d9534f',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; letter-spacing: -0.01em; }</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-portal-bg text-gray-700 flex flex-col min-h-screen font-sans antialiased">
|
|
||||||
<header class="bg-navy text-white h-12 flex items-center justify-between px-4 shrink-0 shadow">
|
|
||||||
<a href="<?= base_url() ?>" class="flex items-center gap-2 shrink-0 text-base font-bold tracking-tight hover:opacity-90" title="종량제 시스템">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-white shrink-0" aria-hidden="true" focusable="false">
|
|
||||||
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/>
|
|
||||||
</svg>
|
|
||||||
<span class="whitespace-nowrap">종량제 시스템</span>
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="flex-grow p-6 flex items-center justify-center">
|
|
||||||
<section class="w-full <?= esc($cardMax) ?> bg-white border border-[#dde4ec] rounded-2xl shadow-[0_2px_12px_rgba(26,43,75,0.08)] overflow-hidden">
|
|
||||||
<div class="bg-gradient-to-br from-navy to-[#007bff] text-white px-6 py-5">
|
|
||||||
<p class="text-xs text-white/70"><?= esc($subtitle) ?></p>
|
|
||||||
<h1 class="text-lg font-bold mt-0.5"><?= $this->renderSection('heading') ?></h1>
|
|
||||||
</div>
|
|
||||||
<div class="p-6">
|
|
||||||
<?php if (session()->getFlashdata('error')): ?>
|
|
||||||
<div class="mb-4 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (session()->getFlashdata('errors')): ?>
|
|
||||||
<div class="mb-4 p-3 rounded-lg bg-red-50 text-red-700 text-sm space-y-1" role="alert">
|
|
||||||
<?php foreach (session()->getFlashdata('errors') as $error): ?><p><?= esc($error) ?></p><?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (session()->getFlashdata('success')): ?>
|
|
||||||
<div class="mb-4 p-3 rounded-lg bg-green-50 text-green-700 text-sm" role="alert"><?= esc(session()->getFlashdata('success')) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?= $this->renderSection('content') ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<footer class="bg-[#eef2f7] border-t border-[#dde4ec] px-4 py-1.5 text-xs text-gray-500 shrink-0 text-center">종량제 물류시스템</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,21 +1,71 @@
|
|||||||
<?= $this->extend('auth/_shell') ?>
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
<?= $this->section('heading') ?>로그인<?= $this->endSection() ?>
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
<?= $this->section('content') ?>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>로그인 - 종량제 시스템</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
||||||
|
colors: {
|
||||||
|
'system-header': '#ffffff',
|
||||||
|
'title-bar': '#2c3e50',
|
||||||
|
'control-panel': '#f8f9fa',
|
||||||
|
'btn-search': '#1c4e80',
|
||||||
|
'btn-exit': '#d9534f',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased">
|
||||||
|
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
|
||||||
|
<a href="<?= base_url() ?>" class="flex items-center gap-2 shrink-0 text-base font-semibold text-gray-800 tracking-tight hover:text-blue-600" title="종량제 시스템">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 text-blue-900 translate-y-[1px]" aria-hidden="true" focusable="false">
|
||||||
|
<path fill="currentColor" d="M9 3a1 1 0 00-1 1v1H5.75a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H16V4a1 1 0 00-1-1H9zm9 4H6v11a2 2 0 002 2h8a2 2 0 002-2V7zM10 9a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0110 9zm4 0a.75.75 0 01.75.75v6a.75.75 0 01-1.5 0v-6A.75.75 0 0114 9z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="whitespace-nowrap">종량제 시스템</span>
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||||
|
로그인
|
||||||
|
</div>
|
||||||
|
<?php if (session()->getFlashdata('success')): ?>
|
||||||
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-green-50 text-green-700 text-sm" role="alert"><?= esc(session()->getFlashdata('success')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (session()->getFlashdata('error')): ?>
|
||||||
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (session()->getFlashdata('errors')): ?>
|
||||||
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm space-y-1" role="alert">
|
||||||
|
<?php foreach (session()->getFlashdata('errors') as $error): ?><p><?= esc($error) ?></p><?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<main class="flex-grow bg-control-panel border-b border-gray-300 p-6 flex items-center justify-center">
|
||||||
|
<section class="w-full max-w-md bg-white border border-gray-300 rounded shadow-sm p-6">
|
||||||
<form action="<?= base_url('login') ?>" method="POST" class="space-y-4">
|
<form action="<?= base_url('login') ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="login_id">아이디</label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="login_id">아이디</label>
|
||||||
<input class="block w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-[#007bff]/40 focus:border-[#007bff]" id="login_id" name="login_id" type="text" placeholder="아이디" value="<?= esc(old('login_id')) ?>" autocomplete="username" autofocus/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="login_id" name="login_id" type="text" placeholder="아이디" value="<?= esc(old('login_id')) ?>" autocomplete="username" autofocus/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="password">비밀번호</label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="password">비밀번호</label>
|
||||||
<input class="block w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-[#007bff]/40 focus:border-[#007bff]" id="password" name="password" type="password" placeholder="비밀번호" autocomplete="current-password"/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="password" name="password" type="password" placeholder="비밀번호" autocomplete="current-password"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-2">
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-lg text-sm font-semibold shadow hover:brightness-110 transition border border-transparent">로그인</button>
|
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-sm text-sm font-medium shadow hover:opacity-90 transition border border-transparent">로그인</button>
|
||||||
<a href="<?= base_url('register') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-lg text-sm shadow-sm hover:bg-gray-50 transition">회원가입</a>
|
<a href="<?= base_url('register') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-sm text-sm shadow hover:bg-gray-50 transition">회원가입</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<?= $this->endSection() ?>
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">종량제 시스템</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,18 +1,59 @@
|
|||||||
<?= $this->extend('auth/_shell') ?>
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
<?= $this->section('heading') ?>2차 인증 (TOTP)<?= $this->endSection() ?>
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
<?= $this->section('content') ?>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<p class="text-sm text-gray-600 mb-4">계정 <strong class="text-gray-800"><?= esc($memberId) ?></strong> 에 대해 인증 앱의 6자리 코드를 입력해 주세요.</p>
|
<title>2차 인증 - 종량제 시스템</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
||||||
|
colors: {
|
||||||
|
'system-header': '#ffffff',
|
||||||
|
'title-bar': '#2c3e50',
|
||||||
|
'control-panel': '#f8f9fa',
|
||||||
|
'btn-search': '#1c4e80',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 text-gray-800 flex flex-col min-h-screen font-sans antialiased">
|
||||||
|
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
|
||||||
|
<?= view('components/header_brand') ?>
|
||||||
|
</header>
|
||||||
|
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||||
|
2차 인증 (TOTP)
|
||||||
|
</div>
|
||||||
|
<?php if (session()->getFlashdata('error')): ?>
|
||||||
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (session()->getFlashdata('errors')): ?>
|
||||||
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm space-y-1" role="alert">
|
||||||
|
<?php foreach (session()->getFlashdata('errors') as $error): ?><p><?= esc($error) ?></p><?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<main class="flex-grow bg-control-panel border-b border-gray-300 p-6 flex items-center justify-center">
|
||||||
|
<section class="w-full max-w-md bg-white border border-gray-300 rounded shadow-sm p-6 space-y-4">
|
||||||
|
<p class="text-sm text-gray-600">계정 <strong class="text-gray-800"><?= esc($memberId) ?></strong> 에 대해 인증 앱의 6자리 코드를 입력해 주세요.</p>
|
||||||
<form action="<?= base_url('login/two-factor') ?>" method="POST" class="space-y-4">
|
<form action="<?= base_url('login/two-factor') ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="totp_code">인증 코드</label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="totp_code">인증 코드</label>
|
||||||
<input class="block w-full border border-gray-300 rounded-lg px-3 py-2 text-sm tracking-widest focus:ring-2 focus:ring-[#007bff]/40 focus:border-[#007bff]" id="totp_code" name="totp_code" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" autofocus placeholder="000000" value="<?= esc(old('totp_code')) ?>"/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm tracking-widest focus:ring-blue-500 focus:border-blue-500" id="totp_code" name="totp_code" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" autofocus placeholder="000000" value="<?= esc(old('totp_code')) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-2">
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-lg text-sm font-semibold shadow hover:brightness-110 transition border border-transparent">확인</button>
|
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-sm text-sm font-medium shadow hover:opacity-90 transition border border-transparent">확인</button>
|
||||||
<a href="<?= base_url('login') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-lg text-sm shadow-sm hover:bg-gray-50 transition">처음으로</a>
|
<a href="<?= base_url('login') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-sm text-sm shadow hover:bg-gray-50 transition">처음으로</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<?= $this->endSection() ?>
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">종량제 시스템</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,38 +1,75 @@
|
|||||||
<?= $this->extend('auth/_shell') ?>
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
<?= $this->section('heading') ?>회원가입<?= $this->endSection() ?>
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
<?= $this->section('content') ?>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<?php $inputCls = 'block w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-[#007bff]/40 focus:border-[#007bff]'; ?>
|
<title>회원가입 - 종량제 시스템</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
||||||
|
colors: {
|
||||||
|
'system-header': '#ffffff',
|
||||||
|
'title-bar': '#2c3e50',
|
||||||
|
'control-panel': '#f8f9fa',
|
||||||
|
'btn-search': '#1c4e80',
|
||||||
|
'btn-exit': '#d9534f',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased">
|
||||||
|
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0">
|
||||||
|
<?= view('components/header_brand') ?>
|
||||||
|
</header>
|
||||||
|
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||||
|
회원가입
|
||||||
|
</div>
|
||||||
|
<?php if (session()->getFlashdata('error')): ?>
|
||||||
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (session()->getFlashdata('errors')): ?>
|
||||||
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm space-y-1" role="alert">
|
||||||
|
<?php foreach (session()->getFlashdata('errors') as $error): ?><p><?= esc($error) ?></p><?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<main class="flex-grow bg-control-panel border-b border-gray-300 p-6 overflow-auto">
|
||||||
|
<section class="w-full max-w-md mx-auto bg-white border border-gray-300 rounded shadow-sm p-6">
|
||||||
<form action="<?= base_url('register') ?>" method="POST" class="space-y-4">
|
<form action="<?= base_url('register') ?>" method="POST" class="space-y-4">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_id">아이디 <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_id">아이디 <span class="text-red-500">*</span></label>
|
||||||
<input class="<?= $inputCls ?>" id="mb_id" name="mb_id" type="text" value="<?= esc(old('mb_id')) ?>" autocomplete="username" autofocus/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="mb_id" name="mb_id" type="text" value="<?= esc(old('mb_id')) ?>" autocomplete="username" autofocus/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_passwd">비밀번호 <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_passwd">비밀번호 <span class="text-red-500">*</span></label>
|
||||||
<input class="<?= $inputCls ?>" id="mb_passwd" name="mb_passwd" type="password" autocomplete="new-password"/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="mb_passwd" name="mb_passwd" type="password" autocomplete="new-password"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_passwd_confirm">비밀번호 확인 <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_passwd_confirm">비밀번호 확인 <span class="text-red-500">*</span></label>
|
||||||
<input class="<?= $inputCls ?>" id="mb_passwd_confirm" name="mb_passwd_confirm" type="password" autocomplete="new-password"/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="mb_passwd_confirm" name="mb_passwd_confirm" type="password" autocomplete="new-password"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_name">이름 <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_name">이름 <span class="text-red-500">*</span></label>
|
||||||
<input class="<?= $inputCls ?>" id="mb_name" name="mb_name" type="text" value="<?= esc(old('mb_name')) ?>" autocomplete="name"/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="mb_name" name="mb_name" type="text" value="<?= esc(old('mb_name')) ?>" autocomplete="name"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_email">이메일</label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_email">이메일</label>
|
||||||
<input class="<?= $inputCls ?>" id="mb_email" name="mb_email" type="email" value="<?= esc(old('mb_email')) ?>"/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="mb_email" name="mb_email" type="email" value="<?= esc(old('mb_email')) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_phone">연락처</label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_phone">연락처</label>
|
||||||
<input class="<?= $inputCls ?>" id="mb_phone" name="mb_phone" type="tel" value="<?= esc(old('mb_phone')) ?>"/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="mb_phone" name="mb_phone" type="tel" value="<?= esc(old('mb_phone')) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_lg_idx">지자체</label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_lg_idx">지자체</label>
|
||||||
<select class="<?= $inputCls ?>" id="mb_lg_idx" name="mb_lg_idx">
|
<select class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="mb_lg_idx" name="mb_lg_idx">
|
||||||
<option value="">선택 안 함</option>
|
<option value="">선택 안 함</option>
|
||||||
<?php if (! empty($localGovernments)): ?>
|
<?php if (! empty($localGovernments)): ?>
|
||||||
<?php foreach ($localGovernments as $lg): ?>
|
<?php foreach ($localGovernments as $lg): ?>
|
||||||
@@ -43,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_level">사용자 역할 <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="mb_level">사용자 역할 <span class="text-red-500">*</span></label>
|
||||||
<select class="<?= $inputCls ?>" id="mb_level" name="mb_level">
|
<select class="block w-full border border-gray-300 rounded px-3 py-2 text-sm focus:ring-blue-500 focus:border-blue-500" id="mb_level" name="mb_level">
|
||||||
<?php foreach (config('Roles')->levelNames as $level => $name): ?>
|
<?php foreach (config('Roles')->levelNames as $level => $name): ?>
|
||||||
<?php if (\Config\Roles::isSuperAdminEquivalent((int) $level)) continue; ?>
|
<?php if (\Config\Roles::isSuperAdminEquivalent((int) $level)) continue; ?>
|
||||||
<option value="<?= $level ?>" <?= old('mb_level', config('Roles')->defaultLevelForSelfRegister) == $level ? 'selected' : '' ?>><?= esc($name) ?></option>
|
<option value="<?= $level ?>" <?= old('mb_level', config('Roles')->defaultLevelForSelfRegister) == $level ? 'selected' : '' ?>><?= esc($name) ?></option>
|
||||||
@@ -52,8 +89,12 @@
|
|||||||
<p class="text-xs text-gray-500 mt-1">가입 후 관리자 승인 완료 시 로그인할 수 있습니다.</p>
|
<p class="text-xs text-gray-500 mt-1">가입 후 관리자 승인 완료 시 로그인할 수 있습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-2">
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-lg text-sm font-semibold shadow hover:brightness-110 transition">가입하기</button>
|
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-sm text-sm font-medium shadow hover:opacity-90 transition">가입하기</button>
|
||||||
<a href="<?= base_url('login') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-lg text-sm shadow-sm hover:bg-gray-50 transition">로그인</a>
|
<a href="<?= base_url('login') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-sm text-sm shadow hover:bg-gray-50 transition">로그인</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<?= $this->endSection() ?>
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">종량제 시스템</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,29 +1,69 @@
|
|||||||
<?= $this->extend('auth/_shell') ?>
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
<?= $this->section('heading') ?>2차 인증 앱 등록<?= $this->endSection() ?>
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
<?= $this->section('content') ?>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<p class="text-sm text-gray-600 mb-4">관리자 계정 <strong class="text-gray-800"><?= esc($memberId) ?></strong> 에 Google Authenticator, Microsoft Authenticator 등으로 아래 시크릿 또는 QR을 등록한 뒤, 표시되는 6자리 코드를 입력해 주세요.</p>
|
<title>2차 인증 등록 - 종량제 시스템</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet"/>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: { sans: ['"Malgun Gothic"', '"Noto Sans KR"', 'sans-serif'] },
|
||||||
|
colors: {
|
||||||
|
'title-bar': '#2c3e50',
|
||||||
|
'control-panel': '#f8f9fa',
|
||||||
|
'btn-search': '#1c4e80',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 text-gray-800 flex flex-col min-h-screen font-sans antialiased">
|
||||||
|
<header class="bg-white border-b border-gray-300 h-12 flex items-center px-4 shrink-0">
|
||||||
|
<?= view('components/header_brand') ?>
|
||||||
|
</header>
|
||||||
|
<div class="bg-title-bar text-white px-4 py-2 text-sm font-medium shrink-0">
|
||||||
|
2차 인증 앱 등록
|
||||||
|
</div>
|
||||||
|
<?php if (session()->getFlashdata('error')): ?>
|
||||||
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm" role="alert"><?= esc(session()->getFlashdata('error')) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (session()->getFlashdata('errors')): ?>
|
||||||
|
<div class="mx-4 mt-2 p-3 rounded-lg bg-red-50 text-red-700 text-sm space-y-1" role="alert">
|
||||||
|
<?php foreach (session()->getFlashdata('errors') as $error): ?><p><?= esc($error) ?></p><?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<main class="flex-grow bg-control-panel border-b border-gray-300 p-6 flex items-center justify-center">
|
||||||
|
<section class="w-full max-w-lg bg-white border border-gray-300 rounded shadow-sm p-6 space-y-4">
|
||||||
|
<p class="text-sm text-gray-600">관리자 계정 <strong class="text-gray-800"><?= esc($memberId) ?></strong> 에 Google Authenticator, Microsoft Authenticator 등으로 아래 시크릿 또는 QR을 등록한 뒤, 표시되는 6자리 코드를 입력해 주세요.</p>
|
||||||
<?php if (! empty($qrDataUri)): ?>
|
<?php if (! empty($qrDataUri)): ?>
|
||||||
<div class="flex justify-center mb-4">
|
<div class="flex justify-center">
|
||||||
<img src="<?= esc($qrDataUri, 'attr') ?>" alt="TOTP QR 코드" class="border border-gray-200 rounded-lg max-w-[200px] h-auto"/>
|
<img src="<?= esc($qrDataUri, 'attr') ?>" alt="TOTP QR 코드" class="border border-gray-200 rounded max-w-[200px] h-auto"/>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<p class="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded p-2 mb-4">QR 이미지를 불러올 수 없습니다. 아래 시크릿을 앱에 직접 입력해 주세요.</p>
|
<p class="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded p-2">QR 이미지를 불러올 수 없습니다. 아래 시크릿을 앱에 직접 입력해 주세요.</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<span class="block text-xs font-semibold text-gray-500 mb-1">수동 입력용 시크릿</span>
|
<span class="block text-xs font-semibold text-gray-500 mb-1">수동 입력용 시크릿</span>
|
||||||
<code class="block text-sm bg-gray-100 border border-gray-200 rounded-lg px-3 py-2 break-all select-all"><?= esc($secret) ?></code>
|
<code class="block text-sm bg-gray-100 border border-gray-200 rounded px-3 py-2 break-all select-all"><?= esc($secret) ?></code>
|
||||||
</div>
|
</div>
|
||||||
<form action="<?= base_url('login/totp-setup') ?>" method="POST" class="space-y-4 pt-2 border-t border-gray-200">
|
<form action="<?= base_url('login/totp-setup') ?>" method="POST" class="space-y-4 pt-2 border-t border-gray-200">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-gray-700 mb-1" for="totp_code">확인용 인증 코드</label>
|
<label class="block text-sm font-bold text-gray-700 mb-1" for="totp_code">확인용 인증 코드</label>
|
||||||
<input class="block w-full border border-gray-300 rounded-lg px-3 py-2 text-sm tracking-widest focus:ring-2 focus:ring-[#007bff]/40 focus:border-[#007bff]" id="totp_code" name="totp_code" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" autofocus placeholder="000000" value="<?= esc(old('totp_code')) ?>"/>
|
<input class="block w-full border border-gray-300 rounded px-3 py-2 text-sm tracking-widest focus:ring-blue-500 focus:border-blue-500" id="totp_code" name="totp_code" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" autofocus placeholder="000000" value="<?= esc(old('totp_code')) ?>"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-lg text-sm font-semibold shadow hover:brightness-110 transition border border-transparent">등록 완료</button>
|
<button type="submit" class="bg-btn-search text-white px-4 py-2 rounded-sm text-sm font-medium shadow hover:opacity-90 transition border border-transparent">등록 완료</button>
|
||||||
<a href="<?= base_url('login') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-lg text-sm shadow-sm hover:bg-gray-50 transition">취소</a>
|
<a href="<?= base_url('login') ?>" class="bg-white text-gray-700 border border-gray-300 px-4 py-2 rounded-sm text-sm shadow hover:bg-gray-50 transition">취소</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<?= $this->endSection() ?>
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="bg-gray-200 border-t border-gray-300 px-4 py-1 text-xs text-gray-600 shrink-0">종량제 시스템</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* [개발용 임시] 판매관리·전화접수 화면 하단에 끼우는 "전체 처리 흐름" 표.
|
|
||||||
*
|
|
||||||
* - 통합 데이터 출처:
|
|
||||||
* · shop_order + shop_order_item (단계: order) — 전화/일반 주문 접수
|
|
||||||
* · bag_sale_scan_code state=sold (단계: sale) — 지정판매소 판매 처리
|
|
||||||
* · bag_sale_scan_code state=in_stock (단계: returned) — 반품으로 재고 복귀
|
|
||||||
* - 호출 API: GET /bag/sale/dev-all-sales-history
|
|
||||||
* - 운영 배포 시 본 파일과 라우트/컨트롤러(`Bag::devAllSalesHistory`) 함께 제거.
|
|
||||||
*
|
|
||||||
* 다수 페이지에 동일하게 include 되므로 ID 충돌 회피를 위해 dev-all-sales- 접두어 사용.
|
|
||||||
*/
|
|
||||||
?>
|
|
||||||
<section id="dev-all-sales-panel" class="border border-amber-400 bg-amber-50/50 p-3 mt-3 rounded-sm">
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
|
|
||||||
<p class="text-xs text-amber-900 leading-relaxed m-0">
|
|
||||||
<strong class="text-amber-950">[개발용 임시 — 전체 처리 흐름]</strong>
|
|
||||||
현재 지자체의 <strong>주문 접수(order) · 판매 처리(sale) · 반품 복귀(returned)</strong>를 모두 모아
|
|
||||||
<strong>일시 역순 최근 500건</strong>으로 표시합니다. (일시는 DB UTC 기준을 <strong>한국 표준시(KST)</strong>로 변환)
|
|
||||||
운영 배포 시 이 블록을 제거해 주세요.
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span id="dev-all-sales-count" class="text-[13px] text-amber-900"></span>
|
|
||||||
<button type="button" id="dev-all-sales-refresh" class="text-[13px] border border-amber-500 text-amber-800 bg-white rounded px-2 py-0.5 hover:bg-amber-100">새로고침</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="max-h-80 overflow-auto border border-amber-300 bg-white">
|
|
||||||
<table class="w-full data-table text-xs">
|
|
||||||
<thead class="sticky top-0 bg-amber-100 z-10">
|
|
||||||
<tr>
|
|
||||||
<th class="w-40">일시</th>
|
|
||||||
<th class="w-24">단계</th>
|
|
||||||
<th>지정판매소</th>
|
|
||||||
<th class="w-14">주문</th>
|
|
||||||
<th>봉투 종류</th>
|
|
||||||
<th class="w-44">봉투 바코드</th>
|
|
||||||
<th class="w-14">포장</th>
|
|
||||||
<th class="w-14">수량</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="dev-all-sales-tbody">
|
|
||||||
<tr><td colspan="8" class="text-center text-gray-400 py-4">불러오는 중…</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
// base_url() 은 .env 의 app.baseURL 기준 절대 URL을 만든다. 사용자가 다른 host(예: localhost)로
|
|
||||||
// 접속한 경우 cross-origin 으로 가 세션 쿠키가 빠진다. 페이지 origin과 동일하도록 path 부분만 사용.
|
|
||||||
const apiPath = '<?= parse_url(base_url('bag/sale/dev-all-sales-history'), PHP_URL_PATH) ?>';
|
|
||||||
const api = apiPath || '/bag/sale/dev-all-sales-history';
|
|
||||||
const tbody = document.getElementById('dev-all-sales-tbody');
|
|
||||||
const countEl = document.getElementById('dev-all-sales-count');
|
|
||||||
const refreshBtn = document.getElementById('dev-all-sales-refresh');
|
|
||||||
if (!tbody) return;
|
|
||||||
|
|
||||||
const nf = new Intl.NumberFormat('ko-KR');
|
|
||||||
const escapeHtml = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
||||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
||||||
}[c]));
|
|
||||||
|
|
||||||
function stageBadge(type) {
|
|
||||||
const t = String(type || '').toLowerCase();
|
|
||||||
if (t === 'order') {
|
|
||||||
return '<span class="text-[12px] px-1.5 py-0.5 rounded bg-sky-100 text-sky-800">주문 접수</span>';
|
|
||||||
}
|
|
||||||
if (t === 'sale') {
|
|
||||||
return '<span class="text-[12px] px-1.5 py-0.5 rounded bg-rose-100 text-rose-800">판매</span>';
|
|
||||||
}
|
|
||||||
if (t === 'returned') {
|
|
||||||
return '<span class="text-[12px] px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-800">반품 복귀</span>';
|
|
||||||
}
|
|
||||||
return `<span class="text-[12px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-700">${escapeHtml(type)}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rowClass(type) {
|
|
||||||
const t = String(type || '').toLowerCase();
|
|
||||||
if (t === 'returned') return 'text-gray-500';
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function dash(s) {
|
|
||||||
return (s == null || String(s) === '') ? '<span class="text-gray-300">-</span>' : escapeHtml(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRows(rows) {
|
|
||||||
if (!Array.isArray(rows) || rows.length === 0) {
|
|
||||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">처리 내역이 없습니다.</td></tr>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const html = rows.map((r) => {
|
|
||||||
const shopText = (r.ds_name && String(r.ds_name).trim() !== '')
|
|
||||||
? `${escapeHtml(r.ds_shop_no || '')} ${escapeHtml(r.ds_name)}`.trim()
|
|
||||||
: `판매소#${escapeHtml(r.ds_idx || '0')}`;
|
|
||||||
const bagText = (r.bag_code || r.bag_name)
|
|
||||||
? `${escapeHtml(r.bag_code || '')} ${escapeHtml(r.bag_name || '')}`.trim()
|
|
||||||
: '<span class="text-gray-300">-</span>';
|
|
||||||
return `
|
|
||||||
<tr class="${rowClass(r.event_type)}">
|
|
||||||
<td class="text-center whitespace-nowrap">${escapeHtml(r.event_time || '')}</td>
|
|
||||||
<td class="text-center">${stageBadge(r.event_type)}</td>
|
|
||||||
<td class="text-left pl-1">${shopText}</td>
|
|
||||||
<td class="text-center">${escapeHtml(r.so_idx || '')}</td>
|
|
||||||
<td class="text-left pl-1">${bagText}</td>
|
|
||||||
<td class="text-center font-mono">${dash(r.code)}</td>
|
|
||||||
<td class="text-center">${dash(r.unit)}</td>
|
|
||||||
<td class="text-right pr-1 tabular-nums">${Number(r.qty || 0) ? nf.format(Number(r.qty)) : '<span class="text-gray-300">-</span>'}</td>
|
|
||||||
</tr>`;
|
|
||||||
}).join('');
|
|
||||||
tbody.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-4">불러오는 중…</td></tr>';
|
|
||||||
countEl.textContent = '';
|
|
||||||
try {
|
|
||||||
const url = api + (api.indexOf('?') >= 0 ? '&' : '?') + '_=' + Date.now();
|
|
||||||
const res = await fetch(url, {
|
|
||||||
credentials: 'same-origin',
|
|
||||||
cache: 'no-store',
|
|
||||||
headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' },
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (!data || data.ok === false) {
|
|
||||||
const msg = (data && data.message) ? data.message : '내역을 불러오지 못했습니다.';
|
|
||||||
tbody.innerHTML = `<tr><td colspan="8" class="text-center text-red-500 py-4">${escapeHtml(msg)}</td></tr>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ok=false (예: 지자체 미선택)일 때는 진단을 표 자리에 띄운다.
|
|
||||||
if (data.ok === false) {
|
|
||||||
const sess = data.session || {};
|
|
||||||
const lines = [
|
|
||||||
(data.message || '지자체를 선택해 주세요.'),
|
|
||||||
`세션: mb_idx=${sess.mb_idx ?? '-'} · mb_level=${sess.mb_level ?? '-'} · admin_selected_lg_idx=${sess.admin_selected_lg_idx ?? '-'} · mb_lg_idx=${sess.mb_lg_idx ?? '-'}`,
|
|
||||||
];
|
|
||||||
tbody.innerHTML = `<tr><td colspan="8" class="text-left px-2 py-4 text-amber-900"><div class="mb-1">${escapeHtml(lines[0])}</div><div class="text-[12px] text-gray-500 font-mono">${escapeHtml(lines[1])}</div></td></tr>`;
|
|
||||||
countEl.textContent = `lg_idx=- · 주문 0 / 판매 0 / 반품복귀 0 · 표시 0건`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRows(data.rows || []);
|
|
||||||
const orders = Number(data.orders ?? 0);
|
|
||||||
const sold = Number(data.sold ?? 0);
|
|
||||||
const ret = Number(data.returned ?? 0);
|
|
||||||
const shown = Array.isArray(data.rows) ? data.rows.length : 0;
|
|
||||||
const lg = data.lg_idx == null ? '-' : data.lg_idx;
|
|
||||||
const sess = data.session || {};
|
|
||||||
const sessText = `[mb_level=${sess.mb_level ?? '-'}, admin=${sess.admin_selected_lg_idx ?? '-'}, mb_lg=${sess.mb_lg_idx ?? '-'}]`;
|
|
||||||
countEl.textContent = `lg_idx=${lg} ${sessText} · 주문 ${nf.format(orders)} / 판매 ${nf.format(sold)} / 반품복귀 ${nf.format(ret)} · 표시 ${nf.format(shown)}건`;
|
|
||||||
} catch (e) {
|
|
||||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-red-500 py-4">통신 오류로 불러오지 못했습니다.</td></tr>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshBtn?.addEventListener('click', () => load());
|
|
||||||
|
|
||||||
// 폼 제출(주문 저장·판매 저장 등) 직후 redirect 되어 새로 로드된 경우에도
|
|
||||||
// 항상 최신 상태를 보장한다. pageshow(BFCache 복원 포함)에서 한 번 더 호출.
|
|
||||||
window.addEventListener('pageshow', (ev) => {
|
|
||||||
if (ev.persisted) load();
|
|
||||||
});
|
|
||||||
|
|
||||||
load();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
<?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">(단위: 매) · <strong>판매(sale)</strong> 수량만 집계합니다(반품·취소 제외). <?= 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>
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
<?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> <strong>판매(sale)</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>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user