536 lines
18 KiB
PHP
536 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">
|
|
<i class="fa fa-fw" aria-hidden="true" title="file-excel-o"></i>엑셀다운로드
|
|
</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() ?>
|