2096 lines
114 KiB
PHP
2096 lines
114 KiB
PHP
<?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 & 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 & 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 & 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 & 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>
|
||
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 (+) & 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>
|