diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 64b1ab0..a2e8f6a 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -123,6 +123,12 @@ $routes->group('admin', ['filter' => 'adminAuth'], static function ($routes): vo $routes->post('packaging-units/delete/(:num)', 'Admin\PackagingUnit::delete/$1'); $routes->get('packaging-units/history/(:num)', 'Admin\PackagingUnit::history/$1'); + // 현황/리포트 (Phase 5) + $routes->get('reports/sales-ledger', 'Admin\SalesReport::salesLedger'); + $routes->get('reports/daily-summary', 'Admin\SalesReport::dailySummary'); + $routes->get('reports/period-sales', 'Admin\SalesReport::periodSales'); + $routes->get('reports/supply-demand', 'Admin\SalesReport::supplyDemand'); + // 판매 대행소 관리 (P2-07/08) $routes->get('sales-agencies', 'Admin\SalesAgency::index'); $routes->get('sales-agencies/create', 'Admin\SalesAgency::create'); diff --git a/app/Controllers/Admin/SalesReport.php b/app/Controllers/Admin/SalesReport.php new file mode 100644 index 0000000..77a1389 --- /dev/null +++ b/app/Controllers/Admin/SalesReport.php @@ -0,0 +1,180 @@ +to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); + + $startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); + $endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); + $mode = $this->request->getGet('mode') ?? 'daily'; // daily or period + + $saleModel = model(BagSaleModel::class); + $db = \Config\Database::connect(); + + if ($mode === 'daily') { + $result = $db->query(" + SELECT bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type, + SUM(ABS(bs_qty)) as total_qty, + SUM(bs_amount) as total_amount + FROM bag_sale + WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? AND bs_type IN('sale','return') + GROUP BY bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type + ORDER BY bs_sale_date DESC, bs_ds_name, bs_bag_code + ", [$lgIdx, $startDate, $endDate])->getResult(); + } else { + $result = $db->query(" + SELECT bs_ds_name, bs_bag_code, bs_bag_name, + SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty, + SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount, + SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty, + SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount + FROM bag_sale + WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? + GROUP BY bs_ds_name, bs_bag_code, bs_bag_name + ORDER BY bs_ds_name, bs_bag_code + ", [$lgIdx, $startDate, $endDate])->getResult(); + } + + return view('admin/layout', [ + 'title' => '판매 대장', + 'content' => view('admin/sales_report/sales_ledger', compact('result', 'startDate', 'endDate', 'mode')), + ]); + } + + /** + * P5-02: 일계표 + */ + public function dailySummary() + { + helper('admin'); + $lgIdx = admin_effective_lg_idx(); + if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); + + $date = $this->request->getGet('date') ?? date('Y-m-d'); + $db = \Config\Database::connect(); + + // 당일 판매 + $daily = $db->query(" + SELECT bs_bag_code, bs_bag_name, + SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty, + SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount + FROM bag_sale + WHERE bs_lg_idx = ? AND bs_sale_date = ? + GROUP BY bs_bag_code, bs_bag_name + ORDER BY bs_bag_code + ", [$lgIdx, $date])->getResult(); + + // 당월 누계 + $monthStart = date('Y-m-01', strtotime($date)); + $monthly = $db->query(" + SELECT bs_bag_code, bs_bag_name, + SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty, + SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount + FROM bag_sale + WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? + GROUP BY bs_bag_code, bs_bag_name + ORDER BY bs_bag_code + ", [$lgIdx, $monthStart, $date])->getResult(); + + return view('admin/layout', [ + 'title' => '일계표', + 'content' => view('admin/sales_report/daily_summary', compact('daily', 'monthly', 'date')), + ]); + } + + /** + * P5-03: 기간별 판매현황 + */ + public function periodSales() + { + helper('admin'); + $lgIdx = admin_effective_lg_idx(); + if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); + + $startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); + $endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); + $db = \Config\Database::connect(); + + $result = $db->query(" + SELECT bs_bag_code, bs_bag_name, + SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty, + SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount, + SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty, + SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount + FROM bag_sale + WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? + GROUP BY bs_bag_code, bs_bag_name + ORDER BY bs_bag_code + ", [$lgIdx, $startDate, $endDate])->getResult(); + + return view('admin/layout', [ + 'title' => '기간별 판매현황', + 'content' => view('admin/sales_report/period_sales', compact('result', 'startDate', 'endDate')), + ]); + } + + /** + * P5-07: 봉투 수불 현황 + */ + public function supplyDemand() + { + helper('admin'); + $lgIdx = admin_effective_lg_idx(); + if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.'); + + $startDate = $this->request->getGet('start_date') ?? date('Y-m-01'); + $endDate = $this->request->getGet('end_date') ?? date('Y-m-d'); + $db = \Config\Database::connect(); + + // 입고 합계 + $receiving = $db->query(" + SELECT br_bag_code, br_bag_name, + SUM(br_qty_sheet) as recv_qty + FROM bag_receiving + WHERE br_lg_idx = ? AND br_receive_date BETWEEN ? AND ? + GROUP BY br_bag_code, br_bag_name + ", [$lgIdx, $startDate, $endDate])->getResult(); + + // 판매 합계 + $sales = $db->query(" + SELECT bs_bag_code, bs_bag_name, + SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty, + SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty + FROM bag_sale + WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? + GROUP BY bs_bag_code, bs_bag_name + ", [$lgIdx, $startDate, $endDate])->getResult(); + + // 불출 합계 + $issues = $db->query(" + SELECT bi2_bag_code, bi2_bag_name, + SUM(bi2_qty) as issue_qty + FROM bag_issue + WHERE bi2_lg_idx = ? AND bi2_issue_date BETWEEN ? AND ? AND bi2_status = 'normal' + GROUP BY bi2_bag_code, bi2_bag_name + ", [$lgIdx, $startDate, $endDate])->getResult(); + + // 현재 재고 + $inventory = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll(); + + return view('admin/layout', [ + 'title' => '봉투 수불 현황', + 'content' => view('admin/sales_report/supply_demand', compact('receiving', 'sales', 'issues', 'inventory', 'startDate', 'endDate')), + ]); + } +} diff --git a/app/Views/admin/sales_report/daily_summary.php b/app/Views/admin/sales_report/daily_summary.php new file mode 100644 index 0000000..0539b58 --- /dev/null +++ b/app/Views/admin/sales_report/daily_summary.php @@ -0,0 +1,102 @@ +
+ 일계표 +
+
+
+ + + +
+
+ +
+ +
+
+ 당일 () +
+ + + + + + + + + + + + + sale_qty; + $dailySaleAmountTotal += (int) $row->sale_amount; + ?> + + + + + + + + + + + + + + + + + + +
봉투코드봉투명판매수량판매금액
bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>sale_amount) ?>
조회된 데이터가 없습니다.
합계
+
+ + +
+
+ 당월 누계 ( ~ ) +
+ + + + + + + + + + + + + sale_qty; + $monthlySaleAmountTotal += (int) $row->sale_amount; + ?> + + + + + + + + + + + + + + + + + + +
봉투코드봉투명판매수량판매금액
bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>sale_amount) ?>
조회된 데이터가 없습니다.
합계
+
+
diff --git a/app/Views/admin/sales_report/period_sales.php b/app/Views/admin/sales_report/period_sales.php new file mode 100644 index 0000000..ebe307b --- /dev/null +++ b/app/Views/admin/sales_report/period_sales.php @@ -0,0 +1,69 @@ +
+ 기간별 판매현황 +
+
+
+ + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + sale_qty; + $grandSaleAmount += (int) $row->sale_amount; + $grandReturnQty += (int) $row->return_qty; + $grandReturnAmount += (int) $row->return_amount; + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명판매수량판매금액반품수량반품금액합계수량합계금액
bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>sale_amount) ?>return_qty) ?>return_amount) ?>sale_qty - (int) $row->return_qty) ?>sale_amount - (int) $row->return_amount) ?>
조회된 데이터가 없습니다.
합계
+
diff --git a/app/Views/admin/sales_report/sales_ledger.php b/app/Views/admin/sales_report/sales_ledger.php new file mode 100644 index 0000000..38d6ba1 --- /dev/null +++ b/app/Views/admin/sales_report/sales_ledger.php @@ -0,0 +1,93 @@ +
+ 판매 대장 +
+
+
+ + + + + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
판매일판매소봉투코드봉투명구분수량금액
bs_sale_date) ?>bs_ds_name) ?>bs_bag_code) ?>bs_bag_name) ?> + '판매', 'return' => '반품']; + echo esc($typeMap[$row->bs_type] ?? $row->bs_type); + ?> + total_qty) ?>total_amount) ?>
조회된 데이터가 없습니다.
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
판매소봉투코드봉투명판매수량판매금액반품수량반품금액계(수량)계(금액)
bs_ds_name) ?>bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>sale_amount) ?>return_qty) ?>return_amount) ?>sale_qty - (int) $row->return_qty) ?>sale_amount - (int) $row->return_amount) ?>
조회된 데이터가 없습니다.
+
+ diff --git a/app/Views/admin/sales_report/supply_demand.php b/app/Views/admin/sales_report/supply_demand.php new file mode 100644 index 0000000..f79e526 --- /dev/null +++ b/app/Views/admin/sales_report/supply_demand.php @@ -0,0 +1,130 @@ +
+ 봉투 수불 현황 +
+
+
+ + + + + +
+
+ +
+ +
+
+ 현재 재고 +
+ + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명재고수량
bi_bag_code) ?>bi_bag_name) ?>bi_qty) ?>
데이터가 없습니다.
+
+ + +
+
+ 기간 입고 +
+ + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명입고수량
br_bag_code) ?>br_bag_name) ?>recv_qty) ?>
데이터가 없습니다.
+
+ + +
+
+ 기간 판매 +
+ + + + + + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명판매수량반품수량순판매
bs_bag_code) ?>bs_bag_name) ?>sale_qty) ?>return_qty) ?>sale_qty - (int) $row->return_qty) ?>
데이터가 없습니다.
+
+ + +
+
+ 기간 불출 +
+ + + + + + + + + + + + + + + + + + + + +
봉투코드봉투명불출수량
bi2_bag_code) ?>bi2_bag_name) ?>issue_qty) ?>
데이터가 없습니다.
+
+
diff --git a/e2e/phase5-reports.spec.js b/e2e/phase5-reports.spec.js new file mode 100644 index 0000000..80d848d --- /dev/null +++ b/e2e/phase5-reports.spec.js @@ -0,0 +1,57 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const { login } = require('./helpers/auth'); + +async function loginAsAdmin(page) { + await login(page, 'admin'); + await page.locator('input[name="lg_idx"]').first().check(); + await page.click('button[type="submit"]'); + await page.waitForURL(url => !url.pathname.includes('select-local-government'), { timeout: 15000 }); +} + +test.describe('P5: 판매 대장', () => { + test('일자별 판매 대장 접근', async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/admin/reports/sales-ledger'); + await expect(page).toHaveURL(/\/admin\/reports\/sales-ledger/); + }); + test('기간별 판매 대장', async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/admin/reports/sales-ledger?mode=period&start_date=2026-01-01&end_date=2026-12-31'); + await expect(page).toHaveURL(/mode=period/); + }); +}); + +test.describe('P5: 일계표', () => { + test('일계표 접근', async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/admin/reports/daily-summary'); + await expect(page).toHaveURL(/\/admin\/reports\/daily-summary/); + }); +}); + +test.describe('P5: 기간별 판매현황', () => { + test('기간별 판매현황 접근', async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/admin/reports/period-sales'); + await expect(page).toHaveURL(/\/admin\/reports\/period-sales/); + }); +}); + +test.describe('P5: 봉투 수불 현황', () => { + test('수불 현황 접근', async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/admin/reports/supply-demand'); + await expect(page).toHaveURL(/\/admin\/reports\/supply-demand/); + }); +}); + +test.describe('P5: 지자체관리자 접근', () => { + test('리포트 접근 가능', async ({ page }) => { + await login(page, 'local'); + await page.goto('/admin/reports/sales-ledger'); + await expect(page).toHaveURL(/\/admin\/reports\/sales-ledger/); + await page.goto('/admin/reports/daily-summary'); + await expect(page).toHaveURL(/\/admin\/reports\/daily-summary/); + }); +});