goiintra/public_html/doc/report_region.php

2096 lines
114 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// =========================================================================
// 1. 프로젝트 공통 인클루드 및 권한 체크
// =========================================================================
include_once $_SERVER['DOCUMENT_ROOT'] . "/common/common.php";
// Level 9 권한 체크가 필요하다면 주석을 해제하세요.
// $func->checkLevelModal(9);
// =========================================================================
// 2. 날짜 파라미터 처리 및 보안 정제 (처음 진입 시 '해당 월 말일' 기본 지정)
// =========================================================================
$start_date = isset($_REQUEST['start_date']) && trim($_REQUEST['start_date']) !== ''
? trim($_REQUEST['start_date'])
: date('Y-m-01');
$end_date = isset($_REQUEST['end_date']) && trim($_REQUEST['end_date']) !== ''
? trim($_REQUEST['end_date'])
: date('Y-m-t');
$start_date_esc = preg_replace("/[^0-9-]/", "", $start_date);
$end_date_esc = preg_replace("/[^0-9-]/", "", $end_date);
$today_date = date('Y-m-d');
// =========================================================================
// 3-1. [쿼리 1] 지역별 통계 데이터 조회
// =========================================================================
$summary_sql = "
SELECT
A.c_regionuid AS region_uid,
CONCAT(
MAX(A.c_region),
'(',
MAX(COALESCE(
NULLIF(M.m_initial, ''),
CONCAT(UPPER(LEFT(M.m_firstname,1)), '.', UPPER(LEFT(M.m_lastname,1)))
)),
')'
) AS region_name,
COUNT(DISTINCT CASE WHEN A.c_contractdate IS NOT NULL
AND A.c_contractdate != ''
AND A.c_contractdate != '0000-00-00'
AND DATE_FORMAT(A.c_contractdate, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc'
THEN A.c_uid ELSE NULL END) AS plus_count,
COUNT(DISTINCT CASE WHEN A.c_inactivedate IS NOT NULL
AND A.c_inactivedate != ''
AND A.c_inactivedate != '0000-00-00'
AND DATE_FORMAT(A.c_inactivedate, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc'
THEN A.c_uid ELSE NULL END) AS minus_count,
COUNT(DISTINCT CASE
WHEN (A.c_contractdate IS NOT NULL AND A.c_contractdate != '' AND A.c_contractdate != '0000-00-00' AND DATE_FORMAT(A.c_contractdate, '%Y-%m-%d') <= '$end_date_esc')
AND (A.c_inactivedate IS NULL OR A.c_inactivedate = '' OR A.c_inactivedate = '0000-00-00' OR DATE_FORMAT(A.c_inactivedate, '%Y-%m-%d') > '$end_date_esc')
THEN A.c_uid ELSE NULL END) AS current_total,
SUM(CASE WHEN B.cc_install_date IS NOT NULL
AND B.cc_install_date != ''
AND B.cc_install_date != '0000-00-00'
AND DATE_FORMAT(B.cc_install_date, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc'
THEN 1 ELSE 0 END) AS container_plus_count,
SUM(CASE WHEN B.cc_pickup_date IS NOT NULL
AND B.cc_pickup_date != ''
AND B.cc_pickup_date != '0000-00-00'
AND DATE_FORMAT(B.cc_pickup_date, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc'
THEN 1 ELSE 0 END) AS container_minus_count,
SUM(CASE WHEN B.cc_customer_uid IS NOT NULL
AND (B.cc_install_date IS NOT NULL AND B.cc_install_date != '' AND B.cc_install_date != '0000-00-00' AND DATE_FORMAT(B.cc_install_date, '%Y-%m-%d') <= '$end_date_esc')
AND (B.cc_pickup_date IS NULL OR B.cc_pickup_date = '' OR B.cc_pickup_date = '0000-00-00' OR DATE_FORMAT(B.cc_pickup_date, '%Y-%m-%d') > '$end_date_esc')
THEN 1 ELSE 0 END) AS current_container_total
FROM tbl_customer A
LEFT JOIN tbl_customer_container B ON A.c_uid = B.cc_customer_uid
LEFT JOIN tbl_member M ON A.c_regionuid = M.m_regionuid
GROUP BY A.c_regionuid
ORDER BY
CASE WHEN A.c_regionuid IS NULL OR A.c_regionuid = '' OR A.c_regionuid = '0' THEN 1 ELSE 0 END ASC,
CAST(A.c_regionuid AS UNSIGNED) ASC
";
$summary_result = $jdb->nQuery($summary_sql, "summary query error");
// =========================================================================
// 3-1-2. [쿼리 1-2] 지역별 기름 통계 데이터 조회 (TBL_DAILY 연동)
// =========================================================================
$rd_start = str_replace('-', '', $start_date_esc);
$rd_end = str_replace('-', '', $end_date_esc);
$oil_sql = "
SELECT
M.m_regionuid AS region_uid,
SUM(RD.rd_quantity) AS total_pickup_qty,
SUM(RD.rd_visit) AS total_visit_cnt
FROM tbl_report_daily RD
INNER JOIN tbl_member M ON RD.rd_driveruid = M.m_uid
WHERE RD.rd_orderdate BETWEEN '{$rd_start}' AND '{$rd_end}'
GROUP BY M.m_regionuid
";
$oil_result = $jdb->nQuery($oil_sql, "oil daily query error");
$oil_data_map = [];
if ($oil_result) {
while ($row = mysqli_fetch_array($oil_result, MYSQLI_ASSOC)) {
$r_uid = !$row['region_uid'] || $row['region_uid'] == '0' ? '0' : $row['region_uid'];
$oil_data_map[$r_uid] = [
'pickup_qty' => (float)$row['total_pickup_qty'],
'visit_cnt' => (int)$row['total_visit_cnt']
];
}
}
// =========================================================================
// 3-1-3. [쿼리 1-3] 신규 고객 기반 오일 픽업량 & 방문수
// =========================================================================
$new_customer_oil_sql = "
SELECT
d.d_regionuid AS region_uid,
MAX(d.d_region) AS region_name,
COUNT(DISTINCT d.d_customeruid) AS new_customer_cnt,
SUM(d.d_quantity) AS oil_pickup_qty,
SUM(CASE WHEN d.d_quantity IS NOT NULL THEN 1 ELSE 0 END) AS visit_cnt
FROM tbl_daily d
WHERE d.d_customeruid IN (
SELECT c_uid
FROM tbl_customer
WHERE c_contractdate IS NOT NULL
AND c_contractdate != ''
AND c_contractdate != '0000-00-00'
AND DATE_FORMAT(c_contractdate, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc'
)
AND d.d_orderdate BETWEEN '{$rd_start}' AND '{$rd_end}'
GROUP BY d.d_regionuid
ORDER BY CAST(d.d_regionuid AS UNSIGNED)
";
$new_customer_oil_result = $jdb->nQuery($new_customer_oil_sql, "new customer oil query error");
$new_oil_data_map = [];
$total_new_customer_oil = 0;
$total_new_customer_visit = 0;
if ($new_customer_oil_result) {
while ($row = mysqli_fetch_array($new_customer_oil_result, MYSQLI_ASSOC)) {
$r_uid = !$row['region_uid'] || $row['region_uid'] == '0' ? '0' : $row['region_uid'];
$new_oil_data_map[$r_uid] = [
'oil_pickup_qty' => (float)$row['oil_pickup_qty'],
'visit_cnt' => (int)$row['visit_cnt'],
];
$total_new_customer_oil += (float)$row['oil_pickup_qty'];
$total_new_customer_visit += (int)$row['visit_cnt'];
}
}
// =========================================================================
// 3-2. 결제수단별 통계 데이터 조회
// =========================================================================
$pay_sql = "
SELECT
c_regionuid AS region_uid,
MAX(c_region) AS region_name,
c_paymenttype AS payment_code,
SUM(CASE WHEN c_contractdate IS NOT NULL
AND c_contractdate != ''
AND c_contractdate != '0000-00-00'
AND DATE_FORMAT(c_contractdate, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc'
THEN 1 ELSE 0 END) AS plus_count,
SUM(CASE WHEN c_inactivedate IS NOT NULL
AND c_inactivedate != ''
AND c_inactivedate != '0000-00-00'
AND DATE_FORMAT(c_inactivedate, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc'
THEN 1 ELSE 0 END) AS minus_count,
SUM(CASE WHEN (c_contractdate IS NOT NULL AND c_contractdate != '' AND c_contractdate != '0000-00-00' AND DATE_FORMAT(c_contractdate, '%Y-%m-%d') <= '$end_date_esc')
AND (c_inactivedate IS NULL OR c_inactivedate = '' OR c_inactivedate = '0000-00-00' OR DATE_FORMAT(c_inactivedate, '%Y-%m-%d') > '$end_date_esc')
THEN 1 ELSE 0 END) AS current_total
FROM tbl_customer
GROUP BY c_regionuid, c_paymenttype
ORDER BY
CASE WHEN c_regionuid IS NULL OR c_regionuid = '' OR c_regionuid = '0' THEN 1 ELSE 0 END ASC,
CAST(c_regionuid AS UNSIGNED) ASC
";
$pay_result = $jdb->nQuery($pay_sql, "payment query error");
// =========================================================================
// 3-3. 컨테이너 종류별 전체 통계 (B, D, ETC 3대 그룹)
// =========================================================================
$type_cap_sql = "
SELECT
CASE
WHEN cc_type LIKE '%BUCKET%' THEN 'ETC'
WHEN cc_type LIKE '%B%' THEN 'B'
WHEN cc_type LIKE '%D%' THEN 'D'
ELSE 'ETC'
END AS container_group,
SUM(CASE WHEN cc_install_date IS NOT NULL AND cc_install_date != '' AND cc_install_date != '0000-00-00' AND DATE_FORMAT(cc_install_date, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc' THEN 1 ELSE 0 END) AS plus_count,
SUM(CASE WHEN cc_pickup_date IS NOT NULL AND cc_pickup_date != '' AND cc_pickup_date != '0000-00-00' AND DATE_FORMAT(cc_pickup_date, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc' THEN 1 ELSE 0 END) AS minus_count,
SUM(CASE WHEN cc_customer_uid IS NOT NULL AND (cc_install_date IS NOT NULL AND cc_install_date != '' AND cc_install_date != '0000-00-00' AND DATE_FORMAT(cc_install_date, '%Y-%m-%d') <= '$end_date_esc') AND (cc_pickup_date IS NULL OR cc_pickup_date = '' OR cc_pickup_date = '0000-00-00' OR DATE_FORMAT(cc_pickup_date, '%Y-%m-%d') > '$end_date_esc') THEN 1 ELSE 0 END) AS current_total
FROM tbl_customer_container
WHERE cc_type IS NOT NULL AND cc_type != ''
AND (cc_pickup_date IS NULL OR cc_pickup_date = '' OR cc_pickup_date = '0000-00-00' OR DATE_FORMAT(cc_pickup_date, '%Y-%m-%d') >= '$start_date_esc')
GROUP BY
CASE
WHEN cc_type LIKE '%BUCKET%' THEN 'ETC'
WHEN cc_type LIKE '%B%' THEN 'B'
WHEN cc_type LIKE '%D%' THEN 'D'
ELSE 'ETC'
END
ORDER BY
CASE
WHEN container_group = 'B' THEN 1
WHEN container_group = 'D' THEN 2
ELSE 3
END ASC
";
$type_cap_result = $jdb->nQuery($type_cap_sql, "container type query error");
// =========================================================================
// 3-4. 지역별 X 컨테이너 대분류 교차 통계
// =========================================================================
$region_type_sql = "
SELECT
A.c_regionuid AS region_uid,
MAX(A.c_region) AS region_name,
CASE
WHEN B.cc_type LIKE '%BUCKET%' THEN 'ETC'
WHEN B.cc_type LIKE '%B%' THEN 'B'
WHEN B.cc_type LIKE '%D%' THEN 'D'
ELSE 'ETC'
END AS container_group,
SUM(CASE WHEN B.cc_install_date IS NOT NULL AND B.cc_install_date != '' AND B.cc_install_date != '0000-00-00' AND DATE_FORMAT(B.cc_install_date, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc' THEN 1 ELSE 0 END) AS plus_count,
SUM(CASE WHEN B.cc_pickup_date IS NOT NULL AND B.cc_pickup_date != '' AND B.cc_pickup_date != '0000-00-00' AND DATE_FORMAT(B.cc_pickup_date, '%Y-%m-%d') BETWEEN '$start_date_esc' AND '$end_date_esc' THEN 1 ELSE 0 END) AS minus_count,
SUM(CASE WHEN B.cc_customer_uid IS NOT NULL AND (B.cc_install_date IS NOT NULL AND B.cc_install_date != '' AND B.cc_install_date != '0000-00-00' AND DATE_FORMAT(B.cc_install_date, '%Y-%m-%d') <= '$end_date_esc') AND (B.cc_pickup_date IS NULL OR B.cc_pickup_date = '' OR B.cc_pickup_date = '0000-00-00' OR DATE_FORMAT(B.cc_pickup_date, '%Y-%m-%d') > '$end_date_esc') THEN 1 ELSE 0 END) AS current_total
FROM tbl_customer A
LEFT JOIN tbl_customer_container B ON A.c_uid = B.cc_customer_uid
AND (B.cc_pickup_date IS NULL OR B.cc_pickup_date = '' OR B.cc_pickup_date = '0000-00-00' OR DATE_FORMAT(B.cc_pickup_date, '%Y-%m-%d') >= '$start_date_esc')
GROUP BY A.c_regionuid,
CASE
WHEN B.cc_type LIKE '%BUCKET%' THEN 'ETC'
WHEN B.cc_type LIKE '%B%' THEN 'B'
WHEN B.cc_type LIKE '%D%' THEN 'D'
ELSE 'ETC'
END
ORDER BY
CASE WHEN A.c_regionuid IS NULL OR A.c_regionuid = '' OR A.c_regionuid = '0' THEN 1 ELSE 0 END ASC,
CAST(A.c_regionuid AS UNSIGNED) ASC,
CASE
WHEN container_group = 'B' THEN 1
WHEN container_group = 'D' THEN 2
ELSE 3
END ASC
";
$region_type_result = $jdb->nQuery($region_type_sql, "region type query error");
// =========================================================================
// 4-1. 지역별 통계 기본 구조 빌드
// =========================================================================
$region_data_list = [];
$total_plus = 0; $total_minus = 0; $total_net = 0; $total_current = 0;
$total_con_plus = 0; $total_con_minus = 0; $total_con_net = 0; $total_container = 0;
$total_oil_pickup = 0; $total_oil_visit = 0;
$chart_region_labels = [];
$chart_region_data = [];
$chart_container_data = [];
if ($summary_result) {
while ($row = mysqli_fetch_array($summary_result, MYSQLI_ASSOC)) {
if (!$row['region_uid'] || $row['region_uid'] == '0' || $row['region_name'] == 'EXT') {
continue;
}
$pureRegionName = $row['region_name'] ? $row['region_name'] : "NO NAME";
$regionDisplay = htmlspecialchars($pureRegionName);
$r_uid = $row['region_uid'];
$p_cnt = (int)$row['plus_count'];
$m_cnt = (int)$row['minus_count'];
$c_cnt = (int)$row['current_total'];
$con_p_cnt = (int)$row['container_plus_count'];
$con_m_cnt = (int)$row['container_minus_count'];
$con_cnt = (int)$row['current_container_total'];
$net_count = $p_cnt - $m_cnt;
$con_net_count = $con_p_cnt - $con_m_cnt;
$oil_pickup = isset($oil_data_map[$r_uid]) ? (float)$oil_data_map[$r_uid]['pickup_qty'] : 0;
$oil_visit = isset($oil_data_map[$r_uid]) ? (int)$oil_data_map[$r_uid]['visit_cnt'] : 0;
$region_data_list[] = [
'r_uid' => $r_uid,
'display_name' => $regionDisplay,
'p_cnt' => $p_cnt,
'm_cnt' => $m_cnt,
'net_count' => $net_count,
'c_cnt' => $c_cnt,
'con_p_cnt' => $con_p_cnt,
'con_m_cnt' => $con_m_cnt,
'con_net_count' => $con_net_count,
'con_cnt' => $con_cnt,
'oil_pickup' => $oil_pickup,
'oil_visit' => $oil_visit
];
$total_plus += $p_cnt;
$total_minus += $m_cnt;
$total_net += $net_count;
$total_current += $c_cnt;
$total_con_plus += $con_p_cnt;
$total_con_minus += $con_m_cnt;
$total_con_net += $con_net_count;
$total_container += $con_cnt;
$total_oil_pickup += $oil_pickup;
$total_oil_visit += $oil_visit;
$chart_region_labels[] = $pureRegionName;
$chart_region_data[] = $c_cnt;
$chart_container_data[] = $con_cnt;
}
}
// 결제수단 매트릭스
$chart_pay_labels = [];
$chart_pay_data = [];
$pay_matrix_methods = [];
$pay_matrix_data = [];
$pay_method_totals = [];
if ($pay_result) {
mysqli_data_seek($pay_result, 0);
while ($row = mysqli_fetch_array($pay_result, MYSQLI_ASSOC)) {
if (!$row['region_uid'] || $row['region_uid'] == '0' || $row['region_name'] == 'EXT' || $row['payment_code'] == 'EXT') {
continue;
}
$r_uid = $row['region_uid'];
$pay_name = htmlspecialchars($row['payment_code']);
if (!in_array($pay_name, $pay_matrix_methods)) {
$pay_matrix_methods[] = $pay_name;
}
$p_cnt = (int)$row['plus_count'];
$m_cnt = (int)$row['minus_count'];
$c_cnt = (int)$row['current_total'];
$net_cnt = $p_cnt - $m_cnt;
if (!isset($pay_matrix_data[$pay_name][$r_uid])) {
$pay_matrix_data[$pay_name][$r_uid] = ['p' => 0, 'm' => 0, 'net' => 0, 'c' => 0];
}
$pay_matrix_data[$pay_name][$r_uid]['p'] += $p_cnt;
$pay_matrix_data[$pay_name][$r_uid]['m'] += $m_cnt;
$pay_matrix_data[$pay_name][$r_uid]['net'] += $net_cnt;
$pay_matrix_data[$pay_name][$r_uid]['c'] += $c_cnt;
if (!isset($pay_method_totals[$pay_name])) $pay_method_totals[$pay_name] = 0;
$pay_method_totals[$pay_name] += $c_cnt;
}
usort($pay_matrix_methods, function($a, $b) use ($pay_method_totals) {
$ta = $pay_method_totals[$a] ?? 0;
$tb = $pay_method_totals[$b] ?? 0;
if ($ta === $tb) return 0;
return ($ta > $tb) ? -1 : 1;
});
}
// =========================================================================
// 4-3. B, D, ETC 3대 분류 데이터 수집
// =========================================================================
$tc_data_list = [];
$tc_total_plus = 0; $tc_total_minus = 0; $tc_total_net = 0; $tc_total_current = 0;
$temp_groups = [
'B' => ['p' => 0, 'm' => 0, 'c' => 0],
'D' => ['p' => 0, 'm' => 0, 'c' => 0],
'ETC' => ['p' => 0, 'm' => 0, 'c' => 0]
];
if ($type_cap_result) {
mysqli_data_seek($type_cap_result, 0);
while ($row = mysqli_fetch_array($type_cap_result, MYSQLI_ASSOC)) {
$group_type = $row['container_group'] ? $row['container_group'] : "ETC";
if (isset($temp_groups[$group_type])) {
$temp_groups[$group_type]['p'] += (int)$row['plus_count'];
$temp_groups[$group_type]['m'] += (int)$row['minus_count'];
$temp_groups[$group_type]['c'] += (int)$row['current_total'];
}
}
}
foreach (['B', 'D', 'ETC'] as $g_name) {
$p_cnt = $temp_groups[$g_name]['p'];
$m_cnt = $temp_groups[$g_name]['m'];
$c_cnt = $temp_groups[$g_name]['c'];
$net_count = $p_cnt - $m_cnt;
$tc_data_list[] = [
'display_name' => "<strong>" . $g_name . "</strong>",
'p_cnt' => $p_cnt,
'm_cnt' => $m_cnt,
'net_count' => $net_count,
'c_cnt' => $c_cnt
];
$tc_total_plus += $p_cnt;
$tc_total_minus += $m_cnt;
$tc_total_net += $net_count;
$tc_total_current += $c_cnt;
}
$chart_tc_labels = ['B', 'D', 'ETC'];
$chart_tc_data = [
(int)$temp_groups['B']['c'],
(int)$temp_groups['D']['c'],
(int)$temp_groups['ETC']['c']
];
// =========================================================================
// 4-4. 가로(지역) X 세로(B, D, ETC) 교차 매트릭스 빌드
// =========================================================================
$matrix_regions = [];
$matrix_types = ['B', 'D', 'ETC'];
$matrix_data = [];
if (!empty($region_data_list)) {
if ($summary_result) {
mysqli_data_seek($summary_result, 0);
while ($s_row = mysqli_fetch_array($summary_result, MYSQLI_ASSOC)) {
if (!$s_row['region_uid'] || $s_row['region_uid'] == '0' || $s_row['region_name'] == 'EXT') {
continue;
}
$r_uid = $s_row['region_uid'];
$r_name = $s_row['region_name'] ? $s_row['region_name'] : "NO NAME";
$matrix_regions[$r_uid] = htmlspecialchars($r_name);
}
}
}
foreach ($matrix_types as $g_name) {
foreach ($matrix_regions as $r_uid => $r_name) {
$matrix_data[$g_name][$r_uid] = ['p' => 0, 'm' => 0, 'net' => 0, 'c' => 0];
}
}
if ($region_type_result) {
mysqli_data_seek($region_type_result, 0);
while ($row = mysqli_fetch_array($region_type_result, MYSQLI_ASSOC)) {
if (!$row['region_uid'] || $row['region_uid'] == '0' || $row['region_name'] == 'EXT') {
continue;
}
$r_uid = $row['region_uid'];
$group_type = $row['container_group'] ? $row['container_group'] : 'ETC';
if (isset($matrix_data[$group_type][$r_uid])) {
$p_cnt = (int)$row['plus_count'];
$m_cnt = (int)$row['minus_count'];
$c_cnt = (int)$row['current_total'];
$matrix_data[$group_type][$r_uid]['p'] += $p_cnt;
$matrix_data[$group_type][$r_uid]['m'] += $m_cnt;
$matrix_data[$group_type][$r_uid]['net'] += ($p_cnt - $m_cnt);
$matrix_data[$group_type][$r_uid]['c'] += $c_cnt;
}
}
}
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
<style>
/* =========================================================================
테이블 공통
========================================================================= */
.tb-report th,
.tb-report td {
text-align: center !important;
vertical-align: middle !important;
white-space: nowrap;
padding: 8px 6px !important;
font-size: 13px;
}
.table-compact th, .table-compact td { min-width: 75px !important; }
.table-compact th.fixed-title-th, .table-compact td.fixed-title-th {
min-width: 150px !important;
white-space: normal;
}
/* =========================================================================
행 색상 클래스
========================================================================= */
.row-total-customer,
.row-total-customer td,
.tb-report tbody tr.row-total-customer td,
.tb-report tbody tr.row-total-customer:hover td { background-color: #173b451a !important; }
.row-total-container,
.row-total-container td,
.tb-report tbody tr.row-total-container td,
.tb-report tbody tr.row-total-container:hover td { background-color: #1b0c0c11 !important; }
.row-total-payment,
.row-total-payment td,
.tb-report tbody tr.row-total-payment td,
.tb-report tbody tr.row-total-payment:hover td { background-color: #ECEBDE !important; }
.row-total-payment-1,
.row-total-payment-1 td,
.tb-report tbody tr.row-total-payment-1 td,
.tb-report tbody tr.row-total-payment-1:hover td { background-color: #D7D3BF !important; }
.row-total-payment-2,
.row-total-payment-2 td,
.tb-report tbody tr.row-total-payment-2 td,
.tb-report tbody tr.row-total-payment-2:hover td { background-color: #C1BAA1 !important; }
.row-total-tc,
.row-total-tc td,
.tb-report tbody tr.row-total-tc td,
.tb-report tbody tr.row-total-tc:hover td { background-color: #173b4511 !important; }
.row-total-rt,
.row-total-rt td,
.tb-report tbody tr.row-total-rt td,
.tb-report tbody tr.row-total-rt:hover td {
background-color: #6c434315 !important;
color: #271d1d !important;
}
.row-total-oil,
.row-total-oil td,
.tb-report tbody tr.row-total-oil td,
.tb-report tbody tr.row-total-oil:hover td { background-color: #f7e6cfa6 !important; }
/* =========================================================================
상단 가로 네비게이션 (스크롤 시 fixed로 전환되어 항상 따라다님)
========================================================================= */
#report-quicknav {
position: relative;
z-index: 1;
isolation: auto;
margin: 16px 0 24px;
padding: 10px 14px;
background: #ffffff;
border: 1px solid #e8dfc0;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
/* 스크롤이 일정 위치를 지나면 상단에 고정되어 따라다님 */
#report-quicknav.is-fixed {
position: fixed;
top: var(--quicknav-top-offset, 0px);
left: 0;
right: 0;
margin: 0;
border-radius: 0;
border-left: none;
border-right: none;
border-top: none;
box-shadow: 0 4px 14px rgba(0,0,0,0.10);
padding: 10px 24px;
background: #fffefacc;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
#report-quicknav .qn-title {
font-size: 10px;
letter-spacing: .14em;
text-transform: uppercase;
color: #9a8560;
font-weight: 700;
padding: 0 12px 0 4px;
border-right: 1px solid #e8dfc0;
margin-right: 6px;
white-space: nowrap;
}
#report-quicknav a {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
border-radius: 8px;
font-size: 12.5px;
color: #5a4a2a;
text-decoration: none;
white-space: nowrap;
transition: background .15s ease, color .15s ease;
line-height: 1.3;
}
#report-quicknav a i { font-size: 14px; opacity: .65; }
#report-quicknav a:hover {
background: #f7f2e2;
color: #2a2017;
}
#report-quicknav a:hover i { opacity: 1; }
#report-quicknav a.qn-active {
background: linear-gradient(180deg, #C9A84C22, #C9A84C18);
color: #7a5c10;
font-weight: 600;
box-shadow: inset 0 0 0 1px #C9A84C44;
}
#report-quicknav a.qn-active i { opacity: 1; color: #8B6914; }
/* 모바일/좁은 화면: 가로 스크롤로 전환 */
@media (max-width: 768px) {
#report-quicknav,
#report-quicknav.is-fixed {
flex-wrap: nowrap;
overflow-x: auto;
padding: 8px 10px;
gap: 4px;
-webkit-overflow-scrolling: touch;
}
#report-quicknav .qn-title { display: none; }
#report-quicknav a { padding: 6px 12px; font-size: 12px; flex-shrink: 0; }
}
/* 캘린더(jQuery UI datepicker)가 열려 있는 동안 quicknav를 잠시 숨김 */
#report-quicknav.qn-hidden {
opacity: 0;
pointer-events: none;
transition: opacity .15s ease;
}
/* 보조 안전장치: datepicker z-index를 quicknav(1050)보다 위로 강제 */
.ui-datepicker { z-index: 1200 !important; }
</style>
<script type="text/javascript">
$(document).ready(function(){
// 날짜 선택기 — 캘린더가 열려있는 동안 상단 quicknav를 잠시 숨김
$('.custom-date-picker').datepicker({
dateFormat: "yy-mm-dd",
changeMonth: true,
changeYear: true,
beforeShow: function(input, inst) {
$('#report-quicknav').addClass('qn-hidden');
setTimeout(function() {
if (inst && inst.dpDiv) inst.dpDiv.css('z-index', 1200);
}, 0);
},
onSelect: function() {
// 날짜 선택 시 change 이벤트로 인한 자동 검색 방지
// — 버튼(🔍)을 눌러야만 검색됩니다
$(this).off('change.datepicker_autosearch');
},
onClose: function() {
$('#report-quicknav').removeClass('qn-hidden');
// change 이벤트가 datepicker 선택 직후 발생하는 것을 한 틱 차단
var $input = $(this);
$input.off('change');
setTimeout(function() { $input.off('change'); }, 100);
}
});
// common.php 등 외부에서 .custom-date-picker에 바인딩된
// change 핸들러(goSearch 포함)를 페이지 로드 후 완전히 제거
$(document).ready(function() {
setTimeout(function() {
$('.custom-date-picker').off('change');
}, 500);
});
Chart.register(ChartDataLabels);
// -------------------------------------------------------------------------
// 공통 데이터 바인딩
// -------------------------------------------------------------------------
var regionLabels = <?=json_encode($chart_region_labels)?>;
var customerTotals = <?=json_encode($chart_region_data)?>;
var containerTotals = <?=json_encode($chart_container_data)?>;
var customerPlus = [<?php foreach($region_data_list as $r) echo ($r['p_cnt'] ?? 0).','; ?>];
var customerMinus = [<?php foreach($region_data_list as $r) echo (-1 * ($r['m_cnt'] ?? 0)).','; ?>];
var containerPlus = [<?php foreach($region_data_list as $r) echo ($r['con_p_cnt'] ?? 0).','; ?>];
var containerMinus = [<?php foreach($region_data_list as $r) echo (-1 * ($r['con_m_cnt'] ?? 0)).','; ?>];
var sumTotalCust = customerTotals.reduce((a, b) => a + Number(b), 0);
var sumTotalCont = containerTotals.reduce((a, b) => a + Number(b), 0);
// -------------------------------------------------------------------------
// [Customer Trend 차트 전용] GH · EXT 지역을 차트에서만 제외
// → 위쪽 표의 원본 배열은 그대로 유지됩니다.
// -------------------------------------------------------------------------
var custExclude = ['GH', 'EXT'];
var custKeepIdx = regionLabels
.map(function(name, idx) {
return { name: String(name).trim().toUpperCase(), idx: idx };
})
.filter(function(o) { return custExclude.indexOf(o.name) === -1; })
.map(function(o) { return o.idx; });
function custPick(arr) {
return custKeepIdx.map(function(i) { return arr[i]; });
}
var custChartLabels = custPick(regionLabels);
var custChartTotals = custPick(customerTotals);
var custChartPlus = custPick(customerPlus);
var custChartMinus = custPick(customerMinus);
var sumTotalCustChart = custChartTotals.reduce((a, b) => a + Number(b), 0);
// -------------------------------------------------------------------------
// [차트 1] Customer Trend — GH 제외된 데이터로 렌더링
// -------------------------------------------------------------------------
var ctxRegCust = document.getElementById('regionCustomerChart').getContext('2d');
var regCustChart = new Chart(ctxRegCust, {
data: {
labels: custChartLabels,
datasets: [
{
type: 'bar',
label: 'Active Total',
data: custChartTotals,
backgroundColor: '#ffe04293',
borderColor: '#FFDE42',
borderWidth: { top: 2.5, bottom: 0, left: 0, right: 0 },
grouped: false,
barPercentage: 1.0,
categoryPercentage: 1.0,
order: 1,
yAxisID: 'y_total'
},
{
type: 'bar',
label: 'New (+)',
data: custChartPlus,
backgroundColor: '#4c5c2da4',
borderColor: '#4C5C2D',
borderWidth: 1,
stack: 'CustStack',
order: 2,
yAxisID: 'y_bars'
},
{
type: 'bar',
label: 'Inact (-)',
data: custChartMinus,
backgroundColor: '#1b0c0cb6',
borderColor: '#1B0C0C',
borderWidth: 1,
stack: 'CustStack',
order: 2,
yAxisID: 'y_bars'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { grid: { drawOnChartArea: true } },
y_bars: {
type: 'linear', display: true, position: 'left',
beginAtZero: false, ticks: { stepSize: 1 }, grace: '10%',
stack: 'split_layout', stackWeight: 1,
title: { display: true, text: 'New / Inact', font: { size: 11, weight: 'bold' } }
},
y_total: {
type: 'linear', display: true, position: 'left',
beginAtZero: false, ticks: { stepSize: 1 }, grace: '10%',
stack: 'split_layout', stackWeight: 1,
title: { display: true, text: 'Active Total', font: { size: 11, weight: 'bold' } },
border: { color: '#ccc', width: 1 }
}
},
plugins: {
legend: { display: true, position: 'top', labels: { boxWidth: 10 } },
datalabels: {
anchor: function(context) { return context.dataset.data[context.dataIndex] < 0 ? 'start' : 'end'; },
align: function(context) { return context.dataset.data[context.dataIndex] < 0 ? 'bottom' : 'top'; },
textColor: '#333',
font: { weight: 'bold', size: 10 },
formatter: function(value, context) {
if (value === 0) return '';
if (context.dataset.label === 'Active Total') {
var pct = sumTotalCustChart === 0 ? '0%' : ((value / sumTotalCustChart) * 100).toFixed(1) + '%';
return value.toLocaleString() + '(' + pct + ')';
}
return Math.abs(value).toLocaleString();
}
}
}
}
});
// -------------------------------------------------------------------------
// 결제수단 바 그래프
// -------------------------------------------------------------------------
var ctxPayElement = document.getElementById('payChart');
if (ctxPayElement) {
var ctxPay = ctxPayElement.getContext('2d');
var payData = <?=json_encode($chart_pay_data ?? [])?>;
var payLabels = <?=json_encode($chart_pay_labels ?? [])?>;
var totalPaySum = payData.reduce((a, b) => a + Number(b), 0);
new Chart(ctxPay, {
type: 'bar',
plugins: [ChartDataLabels],
data: {
labels: payLabels,
datasets: [{
label: '현재 전체 고객수',
data: payData,
backgroundColor: '#36b9cc',
borderColor: '#2c9faf',
borderWidth: 1
}]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 }, grace: '15%' } },
plugins: {
legend: { display: false },
datalabels: {
anchor: 'end', align: 'top', textColor: '#444',
font: { weight: 'bold', size: 11 },
formatter: function(value) {
if (value === 0 || totalPaySum === 0) return '';
return value.toLocaleString() + ' (' + ((value / totalPaySum) * 100).toFixed(1) + '%)';
}
}
}
}
});
}
// -------------------------------------------------------------------------
// 기름 픽업 & 방문 그룹 바 차트
// -------------------------------------------------------------------------
var oilPickups = [<?php foreach($region_data_list as $r) echo ($r['oil_pickup'] ?? 0).','; ?>];
var oilVisits = [<?php foreach($region_data_list as $r) echo ($r['oil_visit'] ?? 0).','; ?>];
var ctxOil = document.getElementById('oilPickupChart').getContext('2d');
new Chart(ctxOil, {
type: 'bar',
data: {
labels: regionLabels,
datasets: [
{
label: 'Oil Pickup Volume (L)',
data: oilPickups,
backgroundColor: '#7F7F7F', borderColor: '#7F7F7F', borderWidth: 1,
yAxisID: 'yPickups', barPercentage: 0.7, categoryPercentage: 0.6
},
{
label: 'Visits',
data: oilVisits,
backgroundColor: '#D4B285', borderColor: '#D4B285', borderWidth: 1,
yAxisID: 'yVisits', barPercentage: 0.7, categoryPercentage: 0.6
}
]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
yPickups: { type: 'linear', position: 'left', beginAtZero: true, title: { display: true, text: 'Oil Pickup Qty', font: { weight: 'bold' } } },
yVisits: { type: 'linear', position: 'right', beginAtZero: true, title: { display: true, text: 'Visits', font: { weight: 'bold' } }, grid: { drawOnChartArea: false } }
},
plugins: {
legend: { display: true, position: 'top' },
datalabels: {
anchor: 'end', align: 'top', font: { weight: 'bold', size: 10 },
formatter: function(value) { return value > 0 ? value.toLocaleString() : ''; }
}
}
}
});
});
function goSearch(){
// 버튼 클릭 시에만 검색되도록 이 함수는 아무것도 하지 않습니다.
// document.form_customer.submit();
}
</script>
<main id="main" class="main">
<div class="breadcrumbs">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<h2>CUSTOMER &amp; CONTAINER STATS REPORT</h2>
<ol>
<li><a href="index_intranet.php">HOME</a></li>
<li>REPORT</li>
<li>CUSTOMER REPORT</li>
</ol>
</div>
</div>
</div>
<section class="page">
<div class="container" data-aos="fade-up">
<div class="wrap-border">
<FORM CLASS="form-report" METHOD="POST" NAME="form_customer" ACTION="<?=$_SERVER["PHP_SELF"]?>">
<INPUT TYPE="hidden" NAME="view" VALUE="report_region">
<table class="table-search-report col-float-right" style="margin-bottom: 0;">
<tr>
<td style="padding: 0 6px; vertical-align: middle;">
<label style="margin: 0; white-space: nowrap;">Date Range : </label>
</td>
<td style="padding: 0 6px; vertical-align: middle;">
<input type="text" name="start_date" class="input-date-custom custom-date-picker custom-select" value="<?=$start_date?>" readonly>
</td>
<td style="padding: 0 4px; vertical-align: middle;"><span>~</span></td>
<td style="padding: 0 6px; vertical-align: middle;">
<input type="text" name="end_date" class="input-date-custom custom-date-picker custom-select" value="<?=$end_date?>" readonly>
</td>
<td style="padding: 0 0 0 6px; vertical-align: middle;">
<button class="btn btn-search" type="button" onclick="document.form_customer.submit();"><i class="bi-search"></i></button>
</td>
</tr>
</table>
<div style="clear: both;"></div>
</FORM>
</div>
</div>
<!-- ===================================================================
상단 가로 섹션 네비게이션 (스크롤 시 상단에 고정됨)
==================================================================== -->
<div class="container" data-aos="fade-up">
<nav id="report-quicknav" aria-label="Report quick navigation">
<div class="qn-title"><i class="bi bi-list"></i> Quick Jump</div>
<a href="#sec-customer-stats"><i class="bi bi-table"></i> Customer &amp; Oil Stats</a>
<a href="#sec-customer-trend"><i class="bi bi-graph-up"></i> Customer Trend</a>
<a href="#sec-new-customer"><i class="bi bi-person-plus"></i> New Customer Oil</a>
<a href="#sec-container-cross"><i class="bi bi-box-seam"></i> Container Cross</a>
<a href="#sec-payment-cross"><i class="bi bi-credit-card"></i> Payment Cross</a>
</nav>
</div>
<script>
/* 섹션 네비게이션 — 스크롤 시 fixed로 전환되어 따라다님 + 부드러운 스크롤 + 현재 섹션 하이라이트 */
(function() {
document.addEventListener("DOMContentLoaded", function() {
var nav = document.getElementById("report-quicknav");
if (!nav) return;
var links = Array.prototype.slice.call(nav.querySelectorAll("a"));
// fixed 전환 시 자리 유지용 placeholder (점프 방지)
var placeholder = document.createElement('div');
placeholder.style.display = 'none';
placeholder.setAttribute('aria-hidden', 'true');
nav.parentNode.insertBefore(placeholder, nav);
var navOffsetTop = 0;
var navHeight = 0;
// ───── 사이트 상단 fixed/sticky 헤더 높이 자동 감지 ─────
// 페이지에 떠 있는 헤더(또는 그 비슷한 요소)를 찾아 그 아래로 quicknav를 배치
var headerSelectors = [
'#header', 'header.header', 'header[role="banner"]',
'.site-header', '#topbar', '.topbar',
'.navbar.fixed-top', '.fixed-top', '.header-fixed',
'#main-header', '.main-header'
];
function detectSiteHeaderHeight() {
var maxBottom = 0;
for (var i = 0; i < headerSelectors.length; i++) {
var els = document.querySelectorAll(headerSelectors[i]);
for (var j = 0; j < els.length; j++) {
var el = els[j];
if (el === nav || nav.contains(el) || el.contains(nav)) continue;
var cs = window.getComputedStyle(el);
if (cs.position === 'fixed' || cs.position === 'sticky') {
var rect = el.getBoundingClientRect();
// 화면 상단 근처에 떠 있는 헤더만 (스크롤로 사라진 헤더 제외)
if (rect.top <= 5 && rect.bottom > maxBottom) {
maxBottom = rect.bottom;
}
}
}
}
return Math.max(0, Math.round(maxBottom));
}
function updateTopOffset() {
var offset = detectSiteHeaderHeight();
document.documentElement.style.setProperty('--quicknav-top-offset', offset + 'px');
return offset;
}
// ────────────────────────────────────────────────
function measure() {
if (nav.classList.contains('is-fixed')) return;
navOffsetTop = nav.getBoundingClientRect().top + window.pageYOffset;
navHeight = nav.offsetHeight;
}
function checkSticky() {
var headerH = updateTopOffset();
// 사이트 헤더 높이만큼 일찍 fixed로 전환되어야 가려지지 않음
if (window.pageYOffset >= (navOffsetTop - headerH)) {
if (!nav.classList.contains('is-fixed')) {
placeholder.style.height = navHeight + 'px';
placeholder.style.display = 'block';
nav.classList.add('is-fixed');
}
} else {
if (nav.classList.contains('is-fixed')) {
nav.classList.remove('is-fixed');
placeholder.style.display = 'none';
}
}
}
// 클릭 시 부드러운 스크롤 (상단 사이트 헤더 + quicknav 높이만큼 자동 보정)
links.forEach(function(a) {
a.addEventListener("click", function(e) {
var id = a.getAttribute("href").replace('#', '');
var target = document.getElementById(id);
if (target) {
e.preventDefault();
var headerH = detectSiteHeaderHeight();
var navH = nav.classList.contains('is-fixed') ? nav.offsetHeight : (navHeight || 56);
var offset = headerH + navH + 16;
window.scrollTo({
top: target.getBoundingClientRect().top + window.pageYOffset - offset,
behavior: "smooth"
});
}
});
});
// 스크롤에 따라 현재 섹션 하이라이트
var sections = links.map(function(a) {
return document.getElementById(a.getAttribute("href").replace('#', ''));
});
function onScroll() {
checkSticky();
var headerH = detectSiteHeaderHeight();
var pos = window.pageYOffset + headerH + (nav.offsetHeight || 56) + 40;
var current = -1;
sections.forEach(function(sec, i) {
if (sec && sec.offsetTop <= pos) current = i;
});
links.forEach(function(a, i) {
a.classList.toggle("qn-active", i === current);
});
}
// 초기 측정 + 이벤트 등록
updateTopOffset();
measure();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", function() {
var wasFixed = nav.classList.contains('is-fixed');
if (wasFixed) {
nav.classList.remove('is-fixed');
placeholder.style.display = 'none';
}
updateTopOffset();
measure();
onScroll();
});
// 이미지/폰트 로드 후 위치 재측정
window.addEventListener("load", function() {
updateTopOffset();
measure();
onScroll();
});
onScroll();
});
})();
</script>
<!-- ===== /상단 섹션 네비게이션 ===== -->
<!-- ================================================================
[섹션 1] Region-wise Customer, Container & Oil Stats
================================================================= -->
<div class="container margin-top-25" id="sec-customer-stats" data-aos="fade-up">
<hr><h4 class="text-center">Region-wise Customer, Container &amp; Oil Stats</h4><hr>
<div class="row">
<div class="col-lg-12">
<div class="wrap-overflow forecast-info report-wrap">
<table class="tb-list tb-report table-compact" style="cursor: default !important;">
<thead>
<tr style="cursor: default !important;">
<th class="fixed-title-th" style="cursor: default !important;">Classification</th>
<?php if(!empty($region_data_list)): ?>
<?php foreach($region_data_list as $r_data): ?>
<th style="text-align:center; cursor: default !important;"><?=$r_data['display_name']?></th>
<?php endforeach; ?>
<th class="bg-total" style="min-width:85px !important; cursor: default !important;">TOTAL</th>
<?php else: ?>
<th style="cursor: default !important;">No Data</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php if(!empty($region_data_list)): ?>
<tr style="cursor: default !important;">
<td class="fixed-title-th header-customer-group" style="cursor: default !important;">Customer New (+)</td>
<?php foreach($region_data_list as $r_data): ?>
<td class="txt-plus" style="cursor: default !important;">+<?=number_format($r_data['p_cnt'])?></td>
<?php endforeach; ?>
<td class="txt-plus bg-total" style="cursor: default !important;">+<?=number_format($total_plus)?></td>
</tr>
<tr style="cursor: default !important;">
<td class="fixed-title-th header-customer-group" style="cursor: default !important;">Customer Inact (-)</td>
<?php foreach($region_data_list as $r_data): ?>
<td class="txt-minus" style="cursor: default !important;">-<?=number_format($r_data['m_cnt'])?></td>
<?php endforeach; ?>
<td class="txt-minus bg-total" style="cursor: default !important;">-<?=number_format($total_minus)?></td>
</tr>
<tr style="cursor: default !important;">
<td class="fixed-title-th header-customer-group" style="cursor: default !important;">Customer Net</td>
<?php foreach($region_data_list as $r_data):
$net_class = $r_data['net_count'] > 0 ? "txt-plus" : ($r_data['net_count'] < 0 ? "txt-minus" : "");
$net_sign = $r_data['net_count'] > 0 ? "+" : "";
?>
<td class="<?=$net_class?>" style="cursor: default !important;"><?=$net_sign.number_format($r_data['net_count'])?></td>
<?php endforeach; ?>
<?php
$t_net_class = $total_net > 0 ? "txt-plus" : ($total_net < 0 ? "txt-minus" : "");
$t_net_sign = $total_net > 0 ? "+" : "";
?>
<td class="<?=$t_net_class?> bg-total" style="cursor: default !important;"><?=$t_net_sign.number_format($total_net)?></td>
</tr>
<tr class="row-total-customer" style="cursor: default !important;">
<td class="fixed-title-th header-customer-group" style="cursor: default !important;">Customer Active Total</td>
<?php foreach($region_data_list as $r_data): ?>
<td class="txt-current" style="cursor: default !important;"><?=number_format($r_data['c_cnt'])?></td>
<?php endforeach; ?>
<td class="txt-current-total bg-total" style="cursor: default !important;"><?=number_format($total_current)?></td>
</tr>
<tr style="height: 20px; background-color: #fff !important; cursor: default !important;">
<td colspan="<?=count($region_data_list) + 2?>" style="border-left: none !important; border-right: none !important; background-color: #fff !important; cursor: default !important;"></td>
</tr>
<tr style="cursor: default !important;">
<td class="fixed-title-th header-container-group" style="cursor: default !important;">Container New (+)</td>
<?php foreach($region_data_list as $r_data): ?>
<td class="txt-plus" style="cursor: default !important;">+<?=number_format($r_data['con_p_cnt'])?></td>
<?php endforeach; ?>
<td class="txt-plus bg-total" style="cursor: default !important;">+<?=number_format($total_con_plus)?></td>
</tr>
<tr style="cursor: default !important;">
<td class="fixed-title-th header-container-group" style="cursor: default !important;">Container Pickup (-)</td>
<?php foreach($region_data_list as $r_data): ?>
<td class="txt-minus" style="cursor: default !important;">-<?=number_format($r_data['con_m_cnt'])?></td>
<?php endforeach; ?>
<td class="txt-minus bg-total" style="cursor: default !important;">-<?=number_format($total_con_minus)?></td>
</tr>
<tr style="cursor: default !important;">
<td class="fixed-title-th header-container-group" style="cursor: default !important;">Container Net</td>
<?php foreach($region_data_list as $r_data):
$con_net_class = $r_data['con_net_count'] > 0 ? "txt-plus" : ($r_data['con_net_count'] < 0 ? "txt-minus" : "");
$con_net_sign = $r_data['con_net_count'] > 0 ? "+" : "";
?>
<td class="<?=$con_net_class?>" style="cursor: default !important;"><?=$con_net_sign.number_format($r_data['con_net_count'])?></td>
<?php endforeach; ?>
<?php
$t_con_net_class = $total_con_net > 0 ? "txt-plus" : ($total_con_net < 0 ? "txt-minus" : "");
$t_con_net_sign = $total_con_net > 0 ? "+" : "";
?>
<td class="<?=$t_con_net_class?> bg-total" style="cursor: default !important;"><?=$t_con_net_sign.number_format($total_con_net)?></td>
</tr>
<tr class="row-total-container" style="cursor: default !important;">
<td class="fixed-title-th header-container-group" style="cursor: default !important;">Container Active Total</td>
<?php foreach($region_data_list as $r_data): ?>
<td style="cursor: default !important;"><?=number_format($r_data['con_cnt'])?></td>
<?php endforeach; ?>
<td class="bg-total" style="cursor: default !important;"><?=number_format($total_container)?></td>
</tr>
<tr style="height: 20px; background-color: #fff !important; cursor: default !important;">
<td colspan="<?=count($region_data_list) + 2?>" style="border-left: none !important; border-right: none !important; background-color: #fff !important; cursor: default !important;"></td>
</tr>
<tr style="cursor: default !important;">
<td class="fixed-title-th" style="text-align: center; cursor: default !important;">Oil Pickup Quantity (L)</td>
<?php foreach($region_data_list as $r_data): ?>
<td style="cursor: default !important;"><?=number_format(floatval($r_data['oil_pickup']))?></td>
<?php endforeach; ?>
<td class="bg-total" style="cursor: default !important;"><?=number_format(floatval($total_oil_pickup))?></td>
</tr>
<tr style="cursor: default !important;">
<td class="fixed-title-th" style="text-align: center; cursor: default !important;">Total Visit Count</td>
<?php foreach($region_data_list as $r_data): ?>
<td style="cursor: default !important;"><?=number_format($r_data['oil_visit'])?></td>
<?php endforeach; ?>
<td class="bg-total" style="cursor: default !important;"><?=number_format($total_oil_visit)?></td>
</tr>
<tr class="row-total-oil" style="cursor: default !important;">
<td class="fixed-title-th" style="text-align: center !important; padding-left: 15px !important; cursor: default !important;">Avg. Qty per Visit</td>
<?php foreach($region_data_list as $r_data):
$avg = $r_data['oil_visit'] > 0 ? round($r_data['oil_pickup'] / $r_data['oil_visit'], 1) : 0;
?>
<td style="cursor: default !important;"><?=number_format($avg, 1)?></td>
<?php endforeach; ?>
<?php $total_avg = $total_oil_visit > 0 ? round($total_oil_pickup / $total_oil_visit, 1) : 0; ?>
<td class="bg-total" style="cursor: default !important;"><?=number_format($total_avg, 1)?></td>
</tr>
<?php else: ?>
<tr><td colspan="2" class="text-center no-data-cell">No records found for the selected period.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ================================================================
[섹션 2] Customer Trend 차트 — GH 지역 제외
================================================================= -->
<div class="container" id="sec-customer-trend" style="margin-top: 40px; margin-bottom: 50px;" data-aos="fade-up">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:6px;">
<div style="width:4px; height:32px; background:linear-gradient(180deg,#C9A84C,#8B6914); border-radius:2px;"></div>
<div>
<div style="font-size:16px; color:#2a2017; letter-spacing:-0.01em;">Region-wise Customer Trend Breakdowns</div>
</div>
</div>
<div style="height:1px; background:linear-gradient(90deg,#C9A84C33,#C9A84C,#C9A84C33); margin-bottom:28px;"></div>
<div style="background:#fffdf7; border:1px solid #e8dfc0; border-radius:12px; padding:20px;">
<div style="font-size:11px; letter-spacing:0.12em; color:#9a8560; text-transform:uppercase; margin-bottom:4px; font-weight:600;">Customer Trend Analysis</div>
<div style="font-size:11px; color:#bbb; margin-bottom:14px;">Customer Growth and Trend Analysis by Region <span style="color:#c9a84c;">(GH excluded)</span></div>
<div style="position: relative; height: 320px;">
<canvas id="regionCustomerChart"></canvas>
</div>
</div>
</div>
<!-- ================================================================
[섹션 3] New Customer — Oil Pickup & Visit Stats
================================================================= -->
<div class="container" id="sec-new-customer" style="margin-top: 40px; margin-bottom: 50px;" data-aos="fade-up">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:6px;">
<div style="width:4px; height:32px; background:linear-gradient(180deg,#4C8FC9,#145A8B); border-radius:2px;"></div>
<div>
<div style="font-size:16px; color:#17253a; letter-spacing:-0.01em;">New Customer — Oil Pickup &amp; Visit Stats</div>
</div>
</div>
<div style="height:1px; background:linear-gradient(90deg,#4C8FC933,#4C8FC9,#4C8FC933); margin-bottom:28px;"></div>
<!-- 표 -->
<div class="wrap-overflow forecast-info report-wrap" style="margin-bottom:32px;">
<table class="tb-list tb-report table-compact" style="cursor:default;">
<thead>
<tr style="background-color:#f1f6fb;">
<th class="fixed-title-th" style="cursor:default;">Classification</th>
<?php foreach($region_data_list as $r_data): ?>
<th style="text-align:center; cursor:default;"><?=$r_data['display_name']?></th>
<?php endforeach; ?>
<th class="bg-total" style="min-width:85px; cursor:default;">TOTAL</th>
</tr>
</thead>
<tbody>
<tr style="cursor:default;">
<td class="fixed-title-th" style="cursor:default;">New Customer (#)</td>
<?php foreach($region_data_list as $r_data): ?>
<td class="txt-plus" style="cursor:default;">+<?=number_format($r_data['p_cnt'])?></td>
<?php endforeach; ?>
<td class="txt-plus bg-total" style="cursor:default;">+<?=number_format($total_plus)?></td>
</tr>
<tr style="cursor:default;">
<td class="fixed-title-th" style="cursor:default;">Oil Pickup Qty (L)</td>
<?php foreach($region_data_list as $r_data):
$oil_qty = isset($new_oil_data_map[$r_data['r_uid']]) ? $new_oil_data_map[$r_data['r_uid']]['oil_pickup_qty'] : 0;
?>
<td style="cursor:default;"><?=number_format($oil_qty, 1)?></td>
<?php endforeach; ?>
<td class="bg-total" style="cursor:default;"><?=number_format($total_new_customer_oil, 1)?></td>
</tr>
<tr style="cursor:default;">
<td class="fixed-title-th" style="cursor:default;">Visit Count</td>
<?php foreach($region_data_list as $r_data):
$v_cnt = isset($new_oil_data_map[$r_data['r_uid']]) ? $new_oil_data_map[$r_data['r_uid']]['visit_cnt'] : 0;
?>
<td style="cursor:default;"><?=number_format($v_cnt)?></td>
<?php endforeach; ?>
<td class="bg-total" style="cursor:default;"><?=number_format($total_new_customer_visit)?></td>
</tr>
<tr class="row-total-oil" style="cursor:default;">
<td class="fixed-title-th" style="cursor:default;">Avg Oil / New Customer (L)</td>
<?php foreach($region_data_list as $r_data):
$o = isset($new_oil_data_map[$r_data['r_uid']]) ? $new_oil_data_map[$r_data['r_uid']]['oil_pickup_qty'] : 0;
$n = $r_data['p_cnt'];
$avg = $n > 0 ? round($o / $n, 1) : 0;
?>
<td style="cursor:default;"><?=number_format($avg, 1)?></td>
<?php endforeach; ?>
<?php $t_avg = $total_plus > 0 ? round($total_new_customer_oil / $total_plus, 1) : 0; ?>
<td class="bg-total" style="cursor:default;"><?=number_format($t_avg, 1)?></td>
</tr>
</tbody>
</table>
</div>
<!-- 카드 그리드 -->
<?php
$nc_total_new = 0; $nc_total_oil = 0;
$nc_max_new = 0; $nc_max_oil = 0;
foreach($region_data_list as $r_data) {
$nc_total_new += (int)$r_data['p_cnt'];
$nc_total_oil += isset($new_oil_data_map[$r_data['r_uid']]) ? (float)$new_oil_data_map[$r_data['r_uid']]['oil_pickup_qty'] : 0;
if ((int)$r_data['p_cnt'] > $nc_max_new) $nc_max_new = (int)$r_data['p_cnt'];
$oil_v = isset($new_oil_data_map[$r_data['r_uid']]) ? (float)$new_oil_data_map[$r_data['r_uid']]['oil_pickup_qty'] : 0;
if ($oil_v > $nc_max_oil) $nc_max_oil = $oil_v;
}
?>
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
<div style="display:flex; gap:6px; font-size:11px;">
<span style="display:flex;align-items:center;gap:4px;"><span style="width:9px;height:9px;border-radius:2px;background:#639922;display:inline-block;"></span>New customers</span>
<span style="margin:0 4px; color:#ddd;">|</span>
<span style="display:flex;align-items:center;gap:4px;"><span style="width:9px;height:9px;border-radius:2px;background:#185FA5;display:inline-block;"></span>Oil qty (L)</span>
</div>
<div style="font-size:11px; color:#888;">
Total new: <strong style="color:#3B6D11;"><?=number_format($nc_total_new)?></strong>
&nbsp;&nbsp;Total oil: <strong style="color:#185FA5;"><?=number_format($nc_total_oil, 0)?> L</strong>
</div>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(110px,1fr)); gap:10px;">
<?php foreach($region_data_list as $r_data):
$n = (int)$r_data['p_cnt'];
$oil = isset($new_oil_data_map[$r_data['r_uid']]) ? (float)$new_oil_data_map[$r_data['r_uid']]['oil_pickup_qty'] : 0;
$hasOil = $oil > 0;
$nPct = $nc_max_new > 0 ? round($n / $nc_max_new * 100) : 0;
$oPct = $nc_max_oil > 0 ? round($oil / $nc_max_oil * 100) : 0;
$oilText = $hasOil ? number_format($oil, 0) . ' L' : '—';
?>
<div style="background:#fff; border:0.5px solid #e0e8f0; border-radius:12px; padding:14px 12px; display:flex; flex-direction:column; gap:10px;">
<div style="font-size:13px; font-weight:500; color:#17253a;"><?=$r_data['display_name']?></div>
<div>
<div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:5px;">
<span style="font-size:10px; color:#888;">New</span>
<span style="font-size:14px; font-weight:500; color:#3B6D11;"><?=$n?></span>
</div>
<div style="height:5px; background:#eef2e8; border-radius:3px; overflow:hidden;">
<div style="height:100%; width:<?=$nPct?>%; background:#639922; border-radius:3px;"></div>
</div>
</div>
<div>
<div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:5px;">
<span style="font-size:10px; color:#888;">Oil</span>
<span style="font-size:11px; font-weight:500; color:<?=$hasOil ? '#185FA5' : '#bbb'?>;"><?=$oilText?></span>
</div>
<div style="height:5px; background:#e8f0f8; border-radius:3px; overflow:hidden;">
<div style="height:100%; width:<?=$oPct?>%; background:<?=$hasOil ? '#378ADD' : 'transparent'?>; border-radius:3px;"></div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<br>
<!-- ================================================================
[섹션 4] Region × Container Specification Cross Analysis
================================================================= -->
<div class="container margin-top-50" id="sec-container-cross" data-aos="fade-up">
<hr><h4 class="text-center">Region × Container Specification Cross Analysis</h4><hr>
<div class="row">
<div class="col-lg-12">
<div class="wrap-overflow forecast-info report-wrap">
<table class="tb-list tb-report table-compact" style="cursor: default;">
<thead>
<tr style="background-color: #f1f3f9;">
<th style="cursor: default;">Container Specification</th>
<th style="cursor: default;">Classification</th>
<?php if(!empty($matrix_regions)): ?>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<th style="text-align:center; min-width:95px; cursor: default;"><?=$r_name?></th>
<?php endforeach; ?>
<th class="bg-total" style="min-width:110px; cursor: default;">TOTAL</th>
<?php else: ?>
<th>No Data</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php if(!empty($matrix_types) && !empty($matrix_regions)): ?>
<?php
$grand_col_p = []; $grand_col_m = []; $grand_col_net = []; $grand_col_c = [];
$total_of_all_p = 0; $total_of_all_m = 0; $total_of_all_net = 0; $total_of_all_c = 0;
?>
<?php foreach($matrix_types as $type_key): ?>
<?php $row_total_p = 0; $row_total_m = 0; $row_total_net = 0; $row_total_c = 0; ?>
<tr>
<td class="fixed-title-th" rowspan="4" style="text-align:center !important; background-color:#f8f9fc !important; vertical-align:middle !important; border-bottom: 2px solid #ccc; cursor: default !important;"><?=$type_key?></td>
<td style="text-align: left !important; background-color: #fafafa; font-size: 12px; cursor: default;">Installed (+)</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<?php $val = isset($matrix_data[$type_key][$r_uid]['p']) ? $matrix_data[$type_key][$r_uid]['p'] : 0;
$row_total_p += $val;
$grand_col_p[$r_uid] = isset($grand_col_p[$r_uid]) ? $grand_col_p[$r_uid] + $val : $val;
?>
<td class="txt-plus" style="cursor: default;"><?=($val > 0 ? "+".number_format($val) : "0")?></td>
<?php endforeach; ?>
<td class="txt-plus bg-total" style="cursor: default;">+<?=number_format($row_total_p)?></td>
</tr>
<tr>
<td style="text-align: left !important; background-color: #fafafa; font-size: 12px; cursor: default;">Picked up (-)</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<?php $val = isset($matrix_data[$type_key][$r_uid]['m']) ? $matrix_data[$type_key][$r_uid]['m'] : 0;
$row_total_m += $val;
$grand_col_m[$r_uid] = isset($grand_col_m[$r_uid]) ? $grand_col_m[$r_uid] + $val : $val;
?>
<td class="txt-minus" style="cursor: default;"><?=($val > 0 ? "-".number_format($val) : "0")?></td>
<?php endforeach; ?>
<td class="txt-minus bg-total" style="cursor: default;">-<?=number_format($row_total_m)?></td>
</tr>
<tr>
<td style="text-align: left !important; background-color: #fafafa; font-size: 12px; cursor: default;">Net Change</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<?php $val = isset($matrix_data[$type_key][$r_uid]['net']) ? $matrix_data[$type_key][$r_uid]['net'] : 0;
$row_total_net += $val;
$grand_col_net[$r_uid] = isset($grand_col_net[$r_uid]) ? $grand_col_net[$r_uid] + $val : $val;
$nc_class = $val > 0 ? "txt-plus" : ($val < 0 ? "txt-minus" : "");
$nc_sign = $val > 0 ? "+" : "";
?>
<td class="<?=$nc_class?>" style="cursor: default;"><?=$nc_sign.number_format($val)?></td>
<?php endforeach; ?>
<?php $rt_nc_class = $row_total_net > 0 ? "txt-plus" : ($row_total_net < 0 ? "txt-minus" : "");
$rt_nc_sign = $row_total_net > 0 ? "+" : "";
?>
<td class="<?=$rt_nc_class?> bg-total" style="cursor: default;"><?=$rt_nc_sign.number_format($row_total_net)?></td>
</tr>
<tr class="row-total-tc" style="border-bottom: 2px solid #ccc;">
<td style="text-align: left !important; font-size: 12px; cursor: default;">Active Total</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<?php $val = isset($matrix_data[$type_key][$r_uid]['c']) ? $matrix_data[$type_key][$r_uid]['c'] : 0;
$row_total_c += $val;
$grand_col_c[$r_uid] = isset($grand_col_c[$r_uid]) ? $grand_col_c[$r_uid] + $val : $val;
?>
<td class="txt-current" style="cursor: default;"><?=number_format($val)?></td>
<?php endforeach; ?>
<td class="txt-current-total bg-total" style="cursor: default;"><?=number_format($row_total_c)?></td>
</tr>
<?php $total_of_all_p += $row_total_p; $total_of_all_m += $row_total_m; $total_of_all_net += $row_total_net; $total_of_all_c += $row_total_c; ?>
<?php endforeach; ?>
<tr class="row-total-rt" style="border-top: 2px solid #aaa;">
<td class="fixed-title-th" rowspan="4" style="vertical-align:middle !important; cursor: default !important; background-color: #6C4343 !important; color: #ffffff !important;">TOTAL MATRIX MAP</td>
<td style="text-align: left !important; font-size:12px; cursor: default;">Total Installed (+)</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<td style="cursor: default;">+<?=number_format($grand_col_p[$r_uid] ?? 0)?></td>
<?php endforeach; ?>
<td class="bg-total" style="cursor: default;">+<?=number_format($total_of_all_p)?></td>
</tr>
<tr class="row-total-rt">
<td style="text-align: left !important; font-size:12px; cursor: default;">Total Picked up (-)</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<td style="cursor: default;">-<?=number_format($grand_col_m[$r_uid] ?? 0)?></td>
<?php endforeach; ?>
<td class="bg-total" style="cursor: default;">-<?=number_format($total_of_all_m)?></td>
</tr>
<tr class="row-total-rt">
<td style="text-align: left !important; font-size:12px; cursor: default;">Total Net Change</td>
<?php foreach($matrix_regions as $r_uid => $r_name):
$g_net = $grand_col_net[$r_uid] ?? 0;
$gn_class = $g_net > 0 ? "txt-plus" : ($g_net < 0 ? "txt-minus" : "");
$gn_sign = $g_net > 0 ? "+" : "";
?>
<td class="<?=$gn_class?>" style="cursor: default;"><?=$gn_sign.number_format($g_net)?></td>
<?php endforeach; ?>
<?php $tot_gn_class = $total_of_all_net > 0 ? "txt-plus" : ($total_of_all_net < 0 ? "txt-minus" : "");
$tot_gn_sign = $total_of_all_net > 0 ? "+" : "";
?>
<td class="<?=$tot_gn_class?> bg-total" style="cursor: default;"><?=$tot_gn_sign.number_format($total_of_all_net)?></td>
</tr>
<tr class="row-total-rt">
<td style="text-align: left !important; font-size:12px; cursor: default;">Total Active Volume</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<td style="cursor: default;"><?=number_format($grand_col_c[$r_uid] ?? 0)?></td>
<?php endforeach; ?>
<td class="bg-total" style="font-size:14px; cursor: default;"><?=number_format($total_of_all_c)?></td>
</tr>
<?php else: ?>
<tr><td colspan="<?=!empty($matrix_regions)?count($matrix_regions)+3:3?>" class="text-center no-data-cell">Insufficient data to generate the cross-analysis matrix.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php
$chart2_regions = [];
if (!empty($matrix_regions)) {
foreach ($matrix_regions as $r_uid => $r_name) {
$chart2_regions[$r_uid] = $r_name;
}
}
$chart2_types = ['B', 'D', 'ETC'];
$chart2_combined_data = [];
if (!empty($matrix_data)) {
foreach ($chart2_types as $g_name) {
if (!isset($matrix_data[$g_name])) continue;
foreach ($matrix_data[$g_name] as $r_uid => $val) {
$chart2_combined_data[$g_name][$r_uid] = [
'c' => isset($val['c']) ? (int)$val['c'] : 0,
'p' => isset($val['p']) ? (int)$val['p'] : 0,
'm' => isset($val['m']) ? (int)$val['m'] : 0
];
}
}
}
?>
<style>
.chart-wrapper { position: relative; width: 100%; height: 100%; }
.chart-wrapper canvas { position: absolute; left: 0; top: 0; width: 100% !important; height: 100% !important; }
</style>
<div class="container" style="margin-top: 40px; margin-bottom: 50px;" data-aos="fade-up">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:6px;">
<div style="width:4px; height:32px; background:linear-gradient(180deg,#C9A84C,#8B6914); border-radius:2px;"></div>
<div>
<div style="font-size:16px; color:#2a2017; letter-spacing:-0.01em;">Region × Container Specification Independent Analysis</div>
</div>
</div>
<div style="height:1px; background:linear-gradient(90deg,#C9A84C33,#C9A84C,#C9A84C33); margin-bottom:28px;"></div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:20px;">
<div style="background:#fffdf7; border:1px solid #e8dfc0; border-radius:12px; padding:20px;">
<div style="font-size:11px; letter-spacing:0.12em; color:#9a8560; text-transform:uppercase; margin-bottom:4px; font-weight:600;">Active Container</div>
<div style="font-size:11px; color:#bbb; margin-bottom:14px;">Current Count by Container Specification</div>
<div style="position:relative; height:260px;"><canvas id="splitTopActiveChart"></canvas></div>
</div>
<div style="background:#fffdf7; border:1px solid #e8dfc0; border-radius:12px; padding:20px;">
<div style="font-size:11px; letter-spacing:0.12em; color:#9a8560; text-transform:uppercase; margin-bottom:4px; font-weight:600;">Install / Pickup Trend</div>
<div style="font-size:11px; color:#bbb; margin-bottom:14px;">Installation and Collection Status During the Period</div>
<div style="position:relative; height:260px;"><canvas id="splitBottomTrendChart"></canvas></div>
</div>
</div>
</div>
<script>
(function() {
window.addEventListener('load', function() {
if (!window.Chart) return;
var regions = <?php echo json_encode($chart2_regions); ?>;
var types = <?php echo json_encode($chart2_types); ?>;
var combinedData = <?php echo json_encode($chart2_combined_data); ?>;
var regionUids = Object.keys(regions).sort(function(a, b) {
if (a === '0') return 1;
if (b === '0') return -1;
return parseInt(a) - parseInt(b);
});
var regionNames = regionUids.map(function(uid) { return regions[uid]; });
var colorMapping = {
'B': { active: '#6C4343', plus: '#6c4343a4', minus: '#6c43435d' },
'D': { active: '#364968', plus: '#364968a6', minus: '#36496844' },
'ETC': { active: '#E09664', plus: '#e09564bb', minus: '#e0956457' }
};
var topDatasets = [];
var bottomDatasets = [];
types.forEach(function(gName) {
var activePoints = [];
var plusPoints = [];
var minusPoints = [];
regionUids.forEach(function(rUid) {
var cVal = 0, pVal = 0, mVal = 0;
if (combinedData[gName] && combinedData[gName][rUid] !== undefined) {
cVal = combinedData[gName][rUid]['c'];
pVal = combinedData[gName][rUid]['p'];
mVal = combinedData[gName][rUid]['m'];
}
activePoints.push(cVal);
plusPoints.push(pVal);
minusPoints.push(-mVal);
});
topDatasets.push({
label: gName + ' (Active)',
data: activePoints,
backgroundColor: colorMapping[gName].active,
borderColor: colorMapping[gName].active,
borderWidth: 1
});
bottomDatasets.push({
label: gName + ' (Install)',
data: plusPoints,
backgroundColor: colorMapping[gName].plus,
borderColor: colorMapping[gName].active,
borderWidth: 1,
stack: gName,
barPercentage: 0.85, categoryPercentage: 0.85
});
bottomDatasets.push({
label: gName + ' (Pickup)',
data: minusPoints,
backgroundColor: colorMapping[gName].minus,
borderColor: colorMapping[gName].active,
borderWidth: 1,
stack: gName,
barPercentage: 0.85, categoryPercentage: 0.85
});
});
var ctxTop = document.getElementById('splitTopActiveChart').getContext('2d');
var topChart = new window.Chart(ctxTop, {
type: 'bar',
data: { labels: regionNames, datasets: topDatasets },
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { grid: { display: false }, ticks: { font: { size: 11, weight: 'bold' } } },
y: { type: 'linear', beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' },
ticks: { font: { size: 10 }, callback: function(v) { return v.toLocaleString(); } } }
},
plugins: {
legend: { position: 'top', labels: { boxWidth: 10, font: { size: 11 } } },
datalabels: {
display: function(ctx) { return ctx.dataset.data[ctx.dataIndex] !== 0; },
color: '#333333', font: { weight: 'bold', size: 9 },
formatter: function(v) { return Math.abs(v).toLocaleString(); },
anchor: 'end', align: 'top'
}
}
},
plugins: [window.ChartDataLabels || {}]
});
var ctxBottom = document.getElementById('splitBottomTrendChart').getContext('2d');
var bottomChart = new window.Chart(ctxBottom, {
type: 'bar',
data: { labels: regionNames, datasets: bottomDatasets },
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { grid: { display: false }, ticks: { font: { size: 11, weight: 'bold' } } },
y: {
type: 'linear',
grid: {
color: function(ctx) { return ctx.tick.value === 0 ? '#000000' : 'rgba(0,0,0,0.04)'; },
lineWidth: function(ctx) { return ctx.tick.value === 0 ? 2 : 1; }
},
ticks: { font: { size: 10 }, callback: function(v) { return Math.abs(v).toLocaleString(); } }
}
},
plugins: {
legend: { position: 'top', labels: { boxWidth: 10, font: { size: 11 } } },
datalabels: {
display: function(ctx) { return ctx.dataset.data[ctx.dataIndex] !== 0; },
color: '#222222', font: { weight: 'bold', size: 8.5 },
formatter: function(v) { return Math.abs(v).toLocaleString(); },
anchor: function(ctx) { return ctx.dataset.data[ctx.dataIndex] >= 0 ? 'end' : 'start'; },
align: function(ctx) { return ctx.dataset.data[ctx.dataIndex] >= 0 ? 'top' : 'bottom'; },
offset: 2
},
tooltip: {
callbacks: {
label: function(ctx) {
return ' ' + ctx.dataset.label + ': ' + Math.abs(ctx.parsed.y).toLocaleString() + ' 개';
}
}
}
}
},
plugins: [window.ChartDataLabels || {}]
});
setTimeout(function() { topChart.resize(); bottomChart.resize(); }, 50);
});
})();
</script>
<br><br>
<!-- ================================================================
[섹션 5] Region × Payment Method Cross Analysis
================================================================= -->
<div class="container margin-top-50" id="sec-payment-cross" data-aos="fade-up">
<hr><h4 class="text-center">Region × Payment Method Cross Analysis</h4><hr>
<div class="row">
<div class="col-lg-12">
<div class="wrap-overflow forecast-info report-wrap">
<table class="tb-list tb-report table-compact" style="cursor: default;">
<thead>
<tr style="background-color: #fcf8e3;">
<th style="min-width:160px; cursor: default;">Payment Method</th>
<th style="min-width:110px; cursor: default;">Classification</th>
<?php if(!empty($matrix_regions)): ?>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<th style="text-align:center; min-width:95px; cursor: default;"><?=$r_name?></th>
<?php endforeach; ?>
<th class="bg-total" style="min-width:110px; cursor: default;">TOTAL</th>
<?php else: ?>
<th>No Region Registered</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php if(!empty($pay_matrix_methods) && !empty($matrix_regions)): ?>
<?php
$grand_pay_col_p = []; $grand_pay_col_m = [];
$grand_pay_col_net = []; $grand_pay_col_c = [];
$total_of_pay_all_p = 0; $total_of_pay_all_m = 0;
$total_of_pay_all_net = 0; $total_of_pay_all_c = 0;
$method_counter = 0;
?>
<?php foreach($pay_matrix_methods as $method_name): ?>
<?php
$row_pay_p = 0; $row_pay_m = 0; $row_pay_net = 0; $row_pay_c = 0;
$method_counter++;
$row_hidden_class = ($method_counter > 3) ? 'hidden-method-row' : '';
$row_hidden_style = ($method_counter > 3) ? 'display: none;' : '';
?>
<tr class="<?=$row_hidden_class?>" style="<?=$row_hidden_style?>">
<td class="fixed-title-th" rowspan="4" style="text-align:center !important; background-color:#fffdf7 !important; vertical-align:middle !important; border-bottom: 2px solid #ddd; cursor: default !important;"><?=$method_name?></td>
<td style="text-align: left !important; background-color: #fafafa; font-size: 12px; padding-left:12px; cursor: default;">New (+)</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<?php $val = $pay_matrix_data[$method_name][$r_uid]['p'] ?? 0;
$row_pay_p += $val;
$grand_pay_col_p[$r_uid] = ($grand_pay_col_p[$r_uid] ?? 0) + $val;
?>
<td class="txt-plus" style="cursor: default;"><?=($val > 0 ? "+".number_format($val) : "0")?></td>
<?php endforeach; ?>
<td class="txt-plus bg-total" style="cursor: default;">+<?=number_format($row_pay_p)?></td>
</tr>
<tr class="<?=$row_hidden_class?>" style="<?=$row_hidden_style?>">
<td style="text-align: left !important; background-color: #fafafa; font-size: 12px; padding-left:12px; cursor: default;">Inact (-)</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<?php $val = $pay_matrix_data[$method_name][$r_uid]['m'] ?? 0;
$row_pay_m += $val;
$grand_pay_col_m[$r_uid] = ($grand_pay_col_m[$r_uid] ?? 0) + $val;
?>
<td class="txt-minus" style="cursor: default;"><?=($val > 0 ? "-".number_format($val) : "0")?></td>
<?php endforeach; ?>
<td class="txt-minus bg-total" style="cursor: default;">-<?=number_format($row_pay_m)?></td>
</tr>
<tr class="<?=$row_hidden_class?>" style="<?=$row_hidden_style?>">
<td style="text-align: left !important; background-color: #fafafa; font-size: 12px; padding-left:12px; cursor: default;">Net Change</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<?php $val = $pay_matrix_data[$method_name][$r_uid]['net'] ?? 0;
$row_pay_net += $val;
$grand_pay_col_net[$r_uid] = ($grand_pay_col_net[$r_uid] ?? 0) + $val;
$nc_class = $val > 0 ? "txt-plus" : ($val < 0 ? "txt-minus" : "");
$nc_sign = $val > 0 ? "+" : "";
?>
<td class="<?=$nc_class?>" style="cursor: default;"><?=$nc_sign.number_format($val)?></td>
<?php endforeach; ?>
<?php $r_nc_class = $row_pay_net > 0 ? "txt-plus" : ($row_pay_net < 0 ? "txt-minus" : "");
$r_nc_sign = $row_pay_net > 0 ? "+" : "";
?>
<td class="<?=$r_nc_class?> bg-total" style="cursor: default;"><?=$r_nc_sign.number_format($row_pay_net)?></td>
</tr>
<tr class="row-total-payment <?=$row_hidden_class?>" style="border-bottom: 2px solid #CCC; <?=$row_hidden_style?>">
<td style="text-align: left !important; font-size: 12px; padding-left:12px; cursor: default;">Active Total</td>
<?php foreach($matrix_regions as $r_uid => $r_name): ?>
<?php $val = $pay_matrix_data[$method_name][$r_uid]['c'] ?? 0;
$row_pay_c += $val;
$grand_pay_col_c[$r_uid] = ($grand_pay_col_c[$r_uid] ?? 0) + $val;
?>
<td class="txt-current" style="cursor: default;"><?=number_format($val)?></td>
<?php endforeach; ?>
<td class="txt-current-total bg-total" style="font-size:13px; cursor: default;"><?=number_format($row_pay_c)?></td>
</tr>
<?php $total_of_pay_all_p += $row_pay_p; $total_of_pay_all_m += $row_pay_m; $total_of_pay_all_net += $row_pay_net; $total_of_pay_all_c += $row_pay_c; ?>
<?php endforeach; ?>
<tr class="row-total-payment-1">
<td class="fixed-title-th" rowspan="4" style="text-align:center !important; vertical-align:middle !important; background-color:#A59D84 !important; border-bottom: 1px solid #CCC; cursor: default !important;">TOTAL PAYMENT MAP</td>
<td style="text-align: left !important; font-size:12px; padding-left:12px; background-color: #fffaf0; cursor: default;">Total New (+)</td>
<?php foreach($matrix_regions as $r_uid => $r_name): $v = $grand_pay_col_p[$r_uid] ?? 0; ?>
<td style="cursor: default;">+<?=number_format($v)?></td>
<?php endforeach; ?>
<td class="bg-total" style="cursor: default;">+<?=number_format($total_of_pay_all_p)?></td>
</tr>
<tr class="row-total-payment-1">
<td style="text-align: left !important; font-size:12px; padding-left:12px; background-color: #fffaf0; cursor: default;">Total Inact (-)</td>
<?php foreach($matrix_regions as $r_uid => $r_name): $v = $grand_pay_col_m[$r_uid] ?? 0; ?>
<td style="cursor: default;">-<?=number_format($v)?></td>
<?php endforeach; ?>
<td class="bg-total" style="cursor: default;">-<?=number_format($total_of_pay_all_m)?></td>
</tr>
<tr class="row-total-payment-1">
<td style="text-align: left !important; font-size:12px; padding-left:12px; background-color: #fffaf0; cursor: default;">Total Net Change</td>
<?php foreach($matrix_regions as $r_uid => $r_name):
$v = $grand_pay_col_net[$r_uid] ?? 0;
$g_nc_class = $v > 0 ? "txt-plus" : ($v < 0 ? "txt-minus" : "");
$g_nc_sign = $v > 0 ? "+" : "";
?>
<td class="<?=$g_nc_class?>" style="cursor: default;"><?=$g_nc_sign.number_format($v)?></td>
<?php endforeach; ?>
<?php $m_tot_nc_class = $total_of_pay_all_net > 0 ? "txt-plus" : ($total_of_pay_all_net < 0 ? "txt-minus" : "");
$m_tot_nc_sign = $total_of_pay_all_net > 0 ? "+" : "";
?>
<td class="<?=$m_tot_nc_class?> bg-total" style="cursor: default;"><?=$m_tot_nc_sign.number_format($total_of_pay_all_net)?></td>
</tr>
<tr class="row-total-payment-2">
<td style="text-align: left !important; font-size:12px; padding-left:12px; cursor: default;">Total Active Volume</td>
<?php foreach($matrix_regions as $r_uid => $r_name): $v = $grand_pay_col_c[$r_uid] ?? 0; ?>
<td style="cursor: default;"><?=number_format($v)?></td>
<?php endforeach; ?>
<td class="bg-total" style="font-size:14px; cursor: default;"><?=number_format($total_of_pay_all_c)?></td>
</tr>
<?php else: ?>
<tr><td colspan="<?=!empty($matrix_regions)?count($matrix_regions)+3:3?>" class="text-center no-data-cell">The payment method data could not be parsed.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if(!empty($pay_matrix_methods) && count($pay_matrix_methods) > 3): ?>
<div class="text-center" style="margin-top: 15px;">
<span id="btn-toggle-payment" style="cursor:pointer; font-size:13px; color:#333; user-select:none;">
See More (+<?=count($pay_matrix_methods) - 3?>) <span id="toggle-arrow">▼</span>
</span>
</div>
<?php endif; ?>
</div>
</div>
<?php
$pay_chart_methods = [];
foreach ($pay_matrix_methods as $mname) {
$sum_c = 0; $sum_p = 0; $sum_m = 0;
foreach ($matrix_regions as $r_uid => $r_name) {
$sum_c += $pay_matrix_data[$mname][$r_uid]['c'] ?? 0;
$sum_p += $pay_matrix_data[$mname][$r_uid]['p'] ?? 0;
$sum_m += $pay_matrix_data[$mname][$r_uid]['m'] ?? 0;
}
$pay_chart_methods[] = ['method' => $mname, 'c' => $sum_c, 'p' => $sum_p, 'm' => $sum_m];
}
$pay_chart_regions_json = json_encode(array_values($matrix_regions));
$pay_chart_methods_json = json_encode($pay_matrix_methods);
$pay_chart_matrix_json = json_encode($pay_matrix_data);
$pay_chart_regions_keys = json_encode(array_keys($matrix_regions));
?>
<!-- Payment Method Charts -->
<div style="margin-top: 40px; margin-bottom: 50px;">
<div style="display:flex; align-items:center; gap:12px; margin-bottom:6px;">
<div style="width:4px; height:32px; background:linear-gradient(180deg,#C9A84C,#8B6914); border-radius:2px;"></div>
<div>
<div style="font-size:16px; color:#2a2017; letter-spacing:-0.01em;">Region × Payment Method</div>
</div>
</div>
<div style="height:1px; background:linear-gradient(90deg,#C9A84C33,#C9A84C,#C9A84C33); margin-bottom:28px;"></div>
<div style="display:grid; grid-template-columns: 280px 1fr; gap:20px; align-items: start;">
<div style="background:#fffdf7; border:1px solid #e8dfc0; border-radius:12px; padding:20px 16px; position:relative;">
<div style="font-size:11px; letter-spacing:0.12em; color:#9a8560; text-transform:uppercase; margin-bottom:14px; font-weight:600;">Active Total Share</div>
<div style="position:relative; height:200px;">
<canvas id="payDonutChart"></canvas>
<div id="payDonutCenter" style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); text-align:center; pointer-events:none;">
<div style="font-size:10px; color:#9a8560; letter-spacing:0.08em;">TOTAL</div>
<div id="payDonutTotal" style="font-size:20px; font-weight:700; color:#2a2017; line-height:1.1;"></div>
</div>
</div>
<div id="payDonutLegend" style="margin-top:14px; display:flex; flex-direction:column; gap:5px;"></div>
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:20px;">
<div style="background:#fffdf7; border:1px solid #e8dfc0; border-radius:12px; padding:20px;">
<div style="font-size:11px; letter-spacing:0.12em; color:#9a8560; text-transform:uppercase; margin-bottom:4px; font-weight:600;">Active Total by Region</div>
<div style="font-size:11px; color:#bbb; margin-bottom:14px;">Current Number of Accounts Managed by Payment Method</div>
<div style="position:relative; height:260px;"><canvas id="payRegionStackedChart"></canvas></div>
</div>
<div style="background:#fffdf7; border:1px solid #e8dfc0; border-radius:12px; padding:20px;">
<div style="font-size:11px; letter-spacing:0.12em; color:#9a8560; text-transform:uppercase; margin-bottom:4px; font-weight:600;">New (+) &amp; Inact () by Method</div>
<div style="font-size:11px; color:#bbb; margin-bottom:14px;">Change by Payment Method During the Period</div>
<div style="position:relative; height:260px;"><canvas id="payNetChart"></canvas></div>
</div>
</div>
</div>
<div id="payHorizBars" style="display:none;"></div>
</div>
</div>
<script>
(function() {
window.addEventListener('load', function() {
if (!window.Chart) return;
var regionNames = <?= $pay_chart_regions_json ?>;
var regionKeys = <?= $pay_chart_regions_keys ?>;
var methods = <?= $pay_chart_methods_json ?>;
var matrixData = <?= $pay_chart_matrix_json ?>;
var methodStats = <?= json_encode($pay_chart_methods) ?>;
var palette = [
{ solid: '#B5813B', mid: 'rgba(181,129,59,0.72)', soft: 'rgba(181,129,59,0.22)' },
{ solid: '#4A6FA5', mid: 'rgba(74,111,165,0.72)', soft: 'rgba(74,111,165,0.22)' },
{ solid: '#6B8C6B', mid: 'rgba(107,140,107,0.72)', soft: 'rgba(107,140,107,0.22)' },
{ solid: '#9B6B9B', mid: 'rgba(155,107,155,0.72)', soft: 'rgba(155,107,155,0.22)' },
{ solid: '#C47A5A', mid: 'rgba(196,122,90,0.72)', soft: 'rgba(196,122,90,0.22)' },
{ solid: '#5A8A8A', mid: 'rgba(90,138,138,0.72)', soft: 'rgba(90,138,138,0.22)' },
{ solid: '#8A7A5A', mid: 'rgba(138,122,90,0.72)', soft: 'rgba(138,122,90,0.22)' },
{ solid: '#7A5A8A', mid: 'rgba(122,90,138,0.72)', soft: 'rgba(122,90,138,0.22)' },
];
function pc(i) { return palette[i % palette.length]; }
var pluginsDL = window.ChartDataLabels || {};
// Donut
var donutTotal = methodStats.reduce(function(a, s) { return a + s.c; }, 0);
document.getElementById('payDonutTotal').textContent = donutTotal.toLocaleString();
var donutCtx = document.getElementById('payDonutChart').getContext('2d');
new window.Chart(donutCtx, {
type: 'doughnut',
data: {
labels: methodStats.map(function(s) { return s.method; }),
datasets: [{
data: methodStats.map(function(s) { return s.c; }),
backgroundColor: methodStats.map(function(_, i) { return pc(i).solid; }),
borderColor: '#fffdf7', borderWidth: 3, hoverOffset: 8
}]
},
options: {
responsive: true, maintainAspectRatio: false, cutout: '65%',
plugins: {
legend: { display: false },
datalabels: {
display: function(ctx) { return ctx.dataset.data[ctx.dataIndex] > 0; },
color: '#fff', font: { size: 10, weight: 'bold' },
formatter: function(val) {
return donutTotal === 0 ? '' : ((val / donutTotal) * 100).toFixed(1) + '%';
}
},
tooltip: {
callbacks: {
label: function(ctx) {
var pct = donutTotal > 0 ? ((ctx.parsed / donutTotal) * 100).toFixed(1) : 0;
return ' ' + ctx.label + ': ' + ctx.parsed.toLocaleString() + ' (' + pct + '%)';
}
}
}
}
},
plugins: [pluginsDL]
});
var legendEl = document.getElementById('payDonutLegend');
methodStats.forEach(function(s, i) {
var row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:7px;font-size:11px;color:#4a3a20;';
row.innerHTML = '<span style="width:10px;height:10px;border-radius:2px;flex-shrink:0;background:' + pc(i).solid + ';display:inline-block;"></span>'
+ '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + s.method + '</span>'
+ '<span style="font-weight:600;color:#2a2017;">' + s.c.toLocaleString() + '</span>';
legendEl.appendChild(row);
});
// Stacked bar — 지역별 Active Total
var stackedDatasets = methods.map(function(mname, i) {
var data = regionKeys.map(function(ruid) {
return (matrixData[mname] && matrixData[mname][ruid]) ? (matrixData[mname][ruid]['c'] || 0) : 0;
});
return {
label: mname, data: data,
backgroundColor: pc(i).mid, borderColor: pc(i).solid,
borderWidth: 1, borderRadius: 2, stack: 'total'
};
});
var ctxStacked = document.getElementById('payRegionStackedChart').getContext('2d');
new window.Chart(ctxStacked, {
type: 'bar',
data: { labels: regionNames, datasets: stackedDatasets },
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { stacked: true, grid: { display: false }, ticks: { font: { size: 10 }, color: '#5a4a2a' } },
y: { stacked: true, beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' },
ticks: { font: { size: 10 }, callback: function(v) { return v.toLocaleString(); } } }
},
plugins: {
legend: { position: 'top', labels: { boxWidth: 9, font: { size: 10 }, color: '#4a3a20' } },
datalabels: {
display: function(ctx) { return ctx.dataset.data[ctx.dataIndex] > 0; },
color: '#fff', font: { size: 8.5, weight: 'bold' },
formatter: function(v) { return v.toLocaleString(); }
}
}
},
plugins: [pluginsDL]
});
// Net 차트 — 지역별 결제수단 New/Inact
var netDatasets = [];
methods.forEach(function(mname, i) {
var colorInfo = pc(i);
var newData = regionKeys.map(function(ruid) {
return (matrixData[mname] && matrixData[mname][ruid]) ? (matrixData[mname][ruid]['p'] || 0) : 0;
});
var inactData = regionKeys.map(function(ruid) {
var mVal = (matrixData[mname] && matrixData[mname][ruid]) ? (matrixData[mname][ruid]['m'] || 0) : 0;
return -mVal;
});
netDatasets.push({
label: mname + ' (+)', data: newData,
backgroundColor: colorInfo.mid, borderColor: colorInfo.solid, borderWidth: 1, borderRadius: 2,
stack: mname, categoryPercentage: 0.8, barPercentage: 0.9
});
netDatasets.push({
label: mname + ' ()', data: inactData,
backgroundColor: 'rgba(181,80,80,0.4)', borderColor: '#B55050', borderWidth: 1, borderRadius: 2,
stack: mname, categoryPercentage: 0.8, barPercentage: 0.9
});
});
var netCtx = document.getElementById('payNetChart').getContext('2d');
new window.Chart(netCtx, {
type: 'bar',
data: { labels: regionNames, datasets: netDatasets },
options: {
responsive: true, maintainAspectRatio: false,
scales: {
x: { stacked: false, grid: { display: false }, ticks: { font: { size: 10 }, color: '#5a4a2a' } },
y: {
beginAtZero: true,
grid: {
color: function(ctx) { return ctx.tick.value === 0 ? 'rgba(0,0,0,0.25)' : 'rgba(0,0,0,0.05)'; },
lineWidth: function(ctx) { return ctx.tick.value === 0 ? 1.5 : 1; }
},
ticks: { font: { size: 10 }, callback: function(v) { return (v >= 0 ? '+' : '') + v.toLocaleString(); } }
}
},
plugins: {
legend: {
position: 'top',
labels: {
boxWidth: 8, font: { size: 9 }, color: '#4a3a20',
filter: function(item) { return item.text.indexOf('(+)') !== -1; }
}
},
datalabels: {
display: function(ctx) { return ctx.dataset.data[ctx.dataIndex] !== 0; },
color: function(ctx) { return ctx.dataset.data[ctx.dataIndex] >= 0 ? '#3a6a3a' : '#8B2020'; },
font: { size: 8, weight: 'bold' },
anchor: function(ctx) { return ctx.dataset.data[ctx.dataIndex] >= 0 ? 'end' : 'start'; },
align: function(ctx) { return ctx.dataset.data[ctx.dataIndex] >= 0 ? 'top' : 'bottom'; },
formatter: function(v) { return Math.abs(v).toLocaleString(); },
offset: 1
},
tooltip: {
callbacks: {
label: function(ctx) {
return ' ' + ctx.dataset.label + ': ' + Math.abs(ctx.parsed.y).toLocaleString();
}
}
}
}
},
plugins: [pluginsDL]
});
});
})();
</script>
<script>
/* Payment 더보기 토글 */
document.addEventListener("DOMContentLoaded", function() {
var toggleBtn = document.getElementById("btn-toggle-payment");
if (toggleBtn) {
toggleBtn.addEventListener("click", function() {
var hiddenRows = document.querySelectorAll(".hidden-method-row");
var isHidden = hiddenRows[0] && hiddenRows[0].style.display === "none";
hiddenRows.forEach(function(row) {
row.style.display = isHidden ? "table-row" : "none";
});
toggleBtn.innerHTML = isHidden
? 'Collapse ▲'
: 'See More (+' + Math.floor(hiddenRows.length / 4) + ') ▼';
});
}
});
</script>
</section>
</main>