Files
confirms/app/Views/pages/results/m416/stats.php
2025-12-18 14:50:48 +09:00

534 lines
18 KiB
PHP

<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<style>
th {
font-size: 11px;
}
#resultList tbody tr {
cursor: pointer;
}
.blockUI {
z-index: 1500 !important;
}
.swal2-cancel {
background-color: #ff0000 !important;
color: #fff !important;
}
</style>
<h1>개인별이동거리</h1>
<div class="row">
<div class="col-md-12 col-xl-12">
<div class="main-card mb-3 card">
<div class="card-body">
<form class="row align-items-end" id="frm_srch_info" onsubmit="return false;">
<div class="row mb-3">
<!-- 관할조직 -->
<div class="col-md-2">
<label class="form-label mb-1">관할조직</label>
<div class="row g-2">
<div class="col-6">
<select class="form-control" name="bonbu" id="bonbu">
<option value="">선택</option>
<?php foreach ($bonbu as $d): ?>
<option value="<?= $d['dept_sq'] ?>"><?= $d['dept_nm'] ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-6">
<select class="form-control" name="dept_sq" id="dept_sq">
<option value="">선택</option>
</select>
</div>
</div>
</div>
<!-- 기준일자 -->
<div class="col-md-3">
<label class="form-label mb-1">기준일자</label>
<div class="row g-2">
<div class="col-5">
<input type="date" class="form-control" id="sdate" name="sdate">
</div>
<div class="col-2 d-flex align-items-center justify-content-center">~</div>
<div class="col-5">
<input type="date" class="form-control" id="edate" name="edate">
</div>
</div>
</div>
<div class="col-md-1">
<label class="form-label mb-1">검색유형</label>
<select class="form-control" name="srchType">
<option value="">선택</option>
<option value="1">아이디</option>
<option value="2">이름</option>
</select>
</div>
<!-- 검색어 -->
<div class="col-md-2">
<label class="form-label mb-1">검색어</label>
<input type="text" class="form-control" name="srchTxt" placeholder="검색어 입력">
</div>
<!-- 검색버튼 -->
<div class="col-md-1 d-grid align-items-end">
<button type="button" class="btn btn-primary" id="btnSearch">검색</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-12 col-xl-12">
<div class="main-card mb-3 card">
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0">사용자 목록</h3>
<div class="ms-auto d-flex align-items-center gap-3">
<button class="mb-2 me-2 border-0 btn-transition btn btn-shadow btn-outline-success"
id="excel-download">엑셀다운로드</button>
</div>
</div>
<div class="card-body">
<table id="resultList" class="table table-hover table-striped table-bordered">
<thead></thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css" />
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script defer src="/architectui/assets/js/datatable.kor.js"></script>
<style>
table.dataTable thead th {
text-align: center;
}
</style>
<script type="text/javascript">
// const tpl = document.querySelector('.my-loader-template');
var teamArr = <?= json_encode($team, JSON_UNESCAPED_UNICODE); ?>;
let date = new Date();
$(function () {
setTimeout(() => {
const edate = $('#edate').val();
const days = getDaysByEndDate(edate);
initTable(days);
}, 0);
$("#bonbu").on("change", function (e) {
var dept_sq = e.target.value
$("#dept_sq").empty()
var str = "<option value=''>선택</option>"
if (teamArr != null) {
for (var i = 0; i < teamArr.length; i++) {
if (dept_sq === teamArr[i].pdept_sq) {
str += "<option value='" + teamArr[i].dept_sq + "'>" + teamArr[i].dept_nm + "</option>"
}
}
}
$("#dept_sq").append(str)
});
$("#srcSido, #srcGugun").on("change", function (e) {
const targetId = this.id;
$.ajax({
url: "/manage/areas/getAreaList",
method: "POST",
dataType: "json",
data: {
'srcSido': $("#frm_srch_info [name=srcSido]").val(),
'srcGugun': $("#frm_srch_info [name=srcGugun]").val(),
},
beforeSend: function () {
blockUI.blockPage({
message: tpl
})
},
complete: function () {
blockUI.unblockPage()
},
success: function (result) {
switch (targetId) {
case "srcSido":
$("#srcGugun").empty()
var str = "";
str += "<option value=''>시/군/구</option>";
if (result.length > 0) {
for (var i = 0; i < result.length; i++) {
str += "<option value='" + result[i]['region_cd'] + "'>" + result[i].region_nm + "</option>";
}
}
$("#srcGugun").append(str);
break;
case "srcGugun":
$("#srcDong").empty()
var str = "";
str += "<option value=''>읍/면/동</option>";
if (result.length > 0) {
for (var i = 0; i < result.length; i++) {
str += "<option value='" + result[i]['region_cd'] + "'>" + result[i].region_nm + "</option>";
}
}
$("#srcDong").append(str);
break;
}
}
});
});
// [검색] 버튼 눌렀을 때 다시 조회
$('#btnSearch').on('click', function () {
const srchDate = $('#edate').val();
const srchDay = getDaysByEndDate(srchDate);
initTable(srchDay)
// table.ajax.reload()
});
// 엑셀다운 click
$("#excel-download").on("click", function () {
$.ajax({
url: "/m416/m416a/excel",
method: "GET",
dataType: "json",
data: $("#frm_srch_info").serialize(),
beforeSend: function () {
blockUI.blockPage({
message: tpl
})
},
complete: function () {
blockUI.unblockPage()
},
success: function (result) {
const days = getDaysByEndDate($("#edate").val());
downloadExcelWithHeader(result.data, days);
}
});
});
});
// datatable render
function initTable(days) {
// 1) thead 생성
renderThead(days, days.length);
// 2) 기존 테이블 제거
if ($.fn.DataTable.isDataTable('#resultList')) {
table.destroy();
$('#resultList tbody').empty(); // 잔여 DOM 정리
}
// 3) columns 생성 후 초기화
const columns = buildColumns(days);
table = $('#resultList').DataTable({
language: lang_kor,
processing: true,
serverSide: true, // 필요 시
searching: false,
ordering: false,
paging: false,
ajax: {
url: '/m416/m416a/getResultList',
type: 'GET',
data: function (d) {
d.sdate = $('#sdate').val();
d.edate = $('#edate').val();
d.bonbu = $("#frm_srch_info [name=bonbu]").val();
d.dep_sq = $("#frm_srch_info [name=dep_sq]").val();
d.srchType = $("#frm_srch_info [name=srchType]").val();
d.srchTxt = $("#frm_srch_info [name=srchTxt]").val();
}
},
columns,
columnDefs: [
{ targets: '_all', defaultContent: '', className: 'text-center' }
]
});
}
// table head render
function renderThead(days, cnt) {
// cnt는 days.length 라고 보면 됨
const $thead = $('#resultList thead');
const tr1 = `
<tr>
<th rowspan="2">순번</th>
<th rowspan="2">아이디</th>
<th rowspan="2">이름</th>
<th rowspan="2">촬영</th>
<th colspan="${cnt}">일자별 이동거리(km)</th>
<th rowspan="2">합계</th>
</tr>`;
const tr2 = `
<tr>
${days.map(d => `<th>${parseInt(d, 10)}</th>`).join('')}
</tr>`;
$thead.html(tr1 + tr2);
}
// data column render
function buildColumns(days) {
const fixed = [
{
"data": null,
"width": "30px",
"render": function (data, type, row, meta) {
return meta.row + meta.settings._iDisplayStart + 1;
}
},
{ data: 'usr_id', "width": "80px" },
{ data: 'usr_nm', "width": "80px" },
{ data: 'shoot_cnt' },
];
const dynamic = days.map(day => ({
data: `d${day}`,
name: `d${day}`,
defaultContent: ''
}));
const total = [
{ data: 'tot_distance' },
];
return [...fixed, ...dynamic, ...total];
}
// 엑셀 다운로드
function downloadExcelWithHeader(dataRows, days) {
// days: ["01","02",...]
const fixedCols = ["순번", "아이디", "이름", "촬영"];
const totalCol = "합계(km)";
const COLS = fixedCols.length + (days?.length || 0) + 1; // +1 = 합계
const fitCols = (arr) => {
const a = (arr || []).slice(0, COLS);
while (a.length < COLS) a.push("");
return a;
};
const safe = (v) => (v === undefined || v === null) ? "" : v;
// 숫자형 안전 처리 (거리)
const num = (v) => (v === undefined || v === null || v === "") ? 0 : Number(v);
/* =======================
* 1) 헤더(2줄)
* - 1줄: 고정 + 그룹(일자별) + 합계(rowspan)
* - 2줄: 일자(1..말일)
* ======================= */
const header1 = fitCols([
...fixedCols,
"일자별 이동거리(km)",
...Array(Math.max((days?.length || 0) - 1, 0)).fill(""),
totalCol
]);
const header2 = fitCols([
...Array(fixedCols.length).fill(""),
...(days || []).map(d => String(parseInt(d, 10))), // "01" -> "1"
""
]);
/* =======================
* 2) 바디
* - 서버가 d01,d02... 형태로 내려준다는 전제
* - 합계는 tot_distance (없으면 일자 합으로 계산)
* ======================= */
const body = (dataRows || []).map((r, idx) => {
const distByDay = (days || []).map(d => num(r[`d${d}`]));
// 서버 합계 컬럼이 있으면 그걸 우선 사용, 없으면 일자합
const total = (r.tot_distance !== undefined && r.tot_distance !== null)
? num(r.tot_distance)
: distByDay.reduce((a, b) => a + b, 0);
return fitCols([
idx + 1, // 순번(엑셀은 재계산)
safe(r.usr_id),
safe(r.usr_nm),
safe(r.shoot_cnt),
...distByDay,
total
]);
});
const aoa = [header1, header2, ...body];
const ws = XLSX.utils.aoa_to_sheet(aoa);
/* =======================
* 3) 병합(merge)
* ======================= */
const dayStart = fixedCols.length; // 일자 시작 컬럼 index
const dayEnd = dayStart + (days.length - 1); // 일자 끝 컬럼 index
const totalColIdx = fixedCols.length + days.length; // 합계 컬럼 index
const merges = [
// 고정 4개 rowspan 2
{ s: { r: 0, c: 0 }, e: { r: 1, c: 0 } }, // 순번
{ s: { r: 0, c: 1 }, e: { r: 1, c: 1 } }, // 아이디
{ s: { r: 0, c: 2 }, e: { r: 1, c: 2 } }, // 이름
{ s: { r: 0, c: 3 }, e: { r: 1, c: 3 } }, // 촬영
// 합계 rowspan 2
{ s: { r: 0, c: totalColIdx }, e: { r: 1, c: totalColIdx } },
];
// 일자별 그룹 colspan (days가 1개 이상일 때만)
if (days.length > 0) {
merges.push({ s: { r: 0, c: dayStart }, e: { r: 0, c: dayEnd } });
}
ws["!merges"] = merges;
/* =======================
* 4) 컬럼 너비
* ======================= */
const cols = [];
cols.push({ wpx: 50 }); // 순번
cols.push({ wpx: 90 }); // 아이디
cols.push({ wpx: 90 }); // 이름
cols.push({ wpx: 60 }); // 촬영
// 일자 컬럼들
for (let i = 0; i < days.length; i++) cols.push({ wpx: 55 });
cols.push({ wpx: 80 }); // 합계
ws["!cols"] = cols;
/* =======================
* 5) (선택) 헤더 스타일/테두리 (xlsx-js-style 사용 시)
* ======================= */
const lastRow = aoa.length - 1;
const lastCol = COLS - 1;
const headerStyle = {
font: { bold: true },
alignment: { horizontal: "center", vertical: "center" },
fill: { patternType: "solid", fgColor: { rgb: "F2F2F2" } },
border: {
top: { style: "thin", color: { rgb: "D9D9D9" } },
bottom: { style: "thin", color: { rgb: "D9D9D9" } },
left: { style: "thin", color: { rgb: "D9D9D9" } },
right: { style: "thin", color: { rgb: "D9D9D9" } },
}
};
const cellBorder = {
border: {
top: { style: "thin", color: { rgb: "E0E0E0" } },
bottom: { style: "thin", color: { rgb: "E0E0E0" } },
left: { style: "thin", color: { rgb: "E0E0E0" } },
right: { style: "thin", color: { rgb: "E0E0E0" } },
}
};
for (let r = 0; r <= lastRow; r++) {
for (let c = 0; c <= lastCol; c++) {
const addr = XLSX.utils.encode_cell({ r, c });
if (!ws[addr]) ws[addr] = { t: "s", v: "" };
ws[addr].s = Object.assign({}, ws[addr].s || {}, cellBorder);
if (r <= 1) ws[addr].s = Object.assign({}, ws[addr].s, headerStyle);
}
}
ws["!rows"] = [{ hpx: 24 }, { hpx: 24 }];
/* =======================
* 6) 저장
* ======================= */
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "sheet1");
XLSX.writeFile(wb, "개인별_이동거리_" + getDateTimeString() + ".xlsx");
}
// 말일 계산
function getDaysByEndDate(edate) {
if (!edate) return [];
const endDay = Number(edate.substr(8, 2)); // 말일 (ex: 31)
const days = [];
for (let d = 1; d <= endDay; d++) {
days.push(String(d).padStart(2, '0')); // "01" ~ "31"
}
return days;
}
function getDateTimeString() {
const d = new Date();
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
const hh = String(d.getHours()).padStart(2, '0');
const mi = String(d.getMinutes()).padStart(2, '0');
const ss = String(d.getSeconds()).padStart(2, '0');
return `${yyyy}${mm}${dd}${hh}${mi}${ss}`;
}
window.addEventListener("DOMContentLoaded", function () {
// 오늘 날짜
const today = new Date();
// 이번 달 1일
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
// 이번 달 말일 (다음달 0일 = 이번달 말일)
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
// yyyy-mm-dd 형태로 변환
const toDate = (d) => d.toISOString().split("T")[0];
document.getElementById("sdate").value = toDate(firstDay);
document.getElementById("edate").value = toDate(lastDay);
});
</script>
<?= $this->endSection() ?>