2154 lines
58 KiB
PHP
2154 lines
58 KiB
PHP
<?php
|
|
$selectedDate = $_GET['date'] ?? date('Y-m-d');
|
|
$canViewAllDrivers = ($_SESSION['ss_LEVEL'] <= 5 || $_SESSION['ss_GROUP'] == 1);
|
|
$selectedDriver = $_GET['driver_uid'] ?? ($canViewAllDrivers ? 0 : $_SESSION['ss_DRUID']);
|
|
|
|
$isAdmin = $canViewAllDrivers;
|
|
|
|
// driver filter
|
|
$whereDriver = "";
|
|
if(!$isAdmin){
|
|
$driver_uid = $_SESSION['ss_DRUID'];
|
|
$whereDriver = " AND di_driver_uid = '{$driver_uid}'";
|
|
}else{
|
|
$driver_uid = (int)($_GET['driver_uid'] ?? 0);
|
|
|
|
if($driver_uid > 0){
|
|
$whereDriver = " AND di_driver_uid = '{$driver_uid}'";
|
|
}
|
|
}
|
|
|
|
// driver list
|
|
$driverList = [];
|
|
$sql_member = "SELECT dr_uid, dr_initial FROM tbl_driver WHERE dr_type = 'INSTALL' ORDER BY dr_initial";
|
|
$res_member = $jdb->nQuery($sql_member, "Load driver list error");
|
|
while($row = mysqli_fetch_assoc($res_member)){
|
|
$driverList[] = $row;
|
|
}
|
|
|
|
$driverInitialMap = [];
|
|
foreach($driverList as $d){
|
|
$driverInitialMap[(int)$d['dr_uid']] = $d['dr_initial'];
|
|
}
|
|
|
|
// daily install data
|
|
$installData = [];
|
|
$sql_order = "
|
|
SELECT di.*,
|
|
n.n_note AS di_additional_note,
|
|
EXISTS(
|
|
SELECT 1
|
|
FROM tbl_customer_image ci
|
|
WHERE ci.i_sourceuid = di.di_uid
|
|
LIMIT 1
|
|
) AS has_image
|
|
FROM tbl_daily_install di
|
|
LEFT JOIN tbl_note n
|
|
ON n.n_dailyuid = CONCAT('i_', di.di_uid)
|
|
WHERE di_install_date = '{$selectedDate}'
|
|
{$whereDriver}
|
|
ORDER BY di_driver_uid, di_order_seq ASC
|
|
";
|
|
$res_order = $jdb->nQuery($sql_order, "Load daily install error");
|
|
|
|
// install container
|
|
while($row = mysqli_fetch_assoc($res_order)){
|
|
|
|
// 컨테이너 불러오기
|
|
$containers = [];
|
|
$sql2 = "
|
|
SELECT *
|
|
FROM tbl_daily_install_container
|
|
WHERE dic_di_uid = {$row['di_uid']}
|
|
";
|
|
$r2 = $jdb->nQuery($sql2,"Load container error");
|
|
|
|
while($c = mysqli_fetch_assoc($r2)){
|
|
$containers[] = $c;
|
|
}
|
|
|
|
$row['containers'] = $containers;
|
|
$installData[] = $row;
|
|
}
|
|
|
|
// image
|
|
if (isset($_GET['ajax']) && $_GET['ajax'] === 'work_images') {
|
|
if (ob_get_length()) ob_clean();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
$type = $_GET['type'] ?? ''; // 'install' or 'install_order'
|
|
$sourceuid = (int)($_GET['sourceuid'] ?? 0);
|
|
|
|
// type 화이트리스트
|
|
if (!in_array($type, ['install', 'install_order', 'clean_before', 'clean_after'], true)) {
|
|
echo json_encode(['ok' => false, 'msg' => 'Invalid type']);
|
|
exit;
|
|
}
|
|
|
|
$qry = "
|
|
SELECT tci.*
|
|
FROM tbl_customer_image tci
|
|
WHERE tci.i_type = '".addslashes($type)."'
|
|
AND tci.i_sourceuid = {$sourceuid}
|
|
AND tci.i_status = 'A'
|
|
ORDER BY tci.i_uid DESC
|
|
LIMIT 1
|
|
";
|
|
$rt = $jdb->nQuery($qry, "work image list error");
|
|
$img = mysqli_fetch_assoc($rt);
|
|
|
|
// 없으면 빈
|
|
if (!$img) {
|
|
echo json_encode(['ok' => true, 'data' => null]);
|
|
exit;
|
|
}
|
|
|
|
// filepath + filename split
|
|
$files = array_filter(array_map('trim', explode(',', $img['i_filename'] ?? '')));
|
|
$first = $files ? $files[0] : '';
|
|
|
|
$imgPath = '';
|
|
if ($first !== '') {
|
|
$imgPath = ($img['i_filepath'] ?? '') . $first;
|
|
}
|
|
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'data' => [
|
|
'i_uid' => $img['i_uid'],
|
|
'i_type' => $img['i_type'],
|
|
'i_sourceuid' => $img['i_sourceuid'],
|
|
'i_note' => $img['i_note'],
|
|
'i_createddate' => $img['i_createddate'],
|
|
'imgPath' => $imgPath,
|
|
'filename' => $first
|
|
]
|
|
]);
|
|
exit;
|
|
}
|
|
?>
|
|
|
|
<main id="main" class="main" style="padding-bottom:25px;">
|
|
|
|
<!-- ======= Breadcrumbs ======= -->
|
|
<div class="breadcrumbs">
|
|
<div class="container">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h2>INSTALL ORDER</h2>
|
|
<ol>
|
|
<li><a href="index_intranet.php">HOME</a></li>
|
|
<li>ORDER</li>
|
|
<li>INSTALL ORDER</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="installOrderContext"
|
|
data-member-uid="<?=$_SESSION['ss_UID']?>"
|
|
data-driver-uid="<?=$_SESSION['ss_DRUID']?>">
|
|
</div>
|
|
|
|
<section class="page">
|
|
<div class="container-fluid">
|
|
|
|
<div class="row">
|
|
|
|
<!-- ===== LEFT PANEL ===== -->
|
|
<div class="col-md-2">
|
|
|
|
<!-- CONTROL -->
|
|
<div class="card mb-3">
|
|
<div class="card-body d-flex flex-column align-items-center">
|
|
|
|
<!-- DATE -->
|
|
<div class="mb-3 w-100 d-flex flex-column align-items-center">
|
|
|
|
<input type="text" id="installDate"
|
|
class="form-control form-control-sm text-center"
|
|
style="width:100%; padding:0px; margin:0px;"
|
|
value="<?=$selectedDate?>" readonly>
|
|
|
|
<div class="d-flex mt-1" style="width:100%;">
|
|
<button class="btn btn-sm btn-outline-secondary w-50" id="prevDay">
|
|
<i class="bi bi-chevron-left"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary w-50" id="nextDay">
|
|
<i class="bi bi-chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- DRIVER -->
|
|
<div class="mb-3 w-100 d-flex justify-content-center">
|
|
<select id="driverSelect"
|
|
class="form-select form-select-sm text-center"
|
|
style="width:100%;"
|
|
<?=(!$isAdmin ? 'disabled' : '')?>>
|
|
|
|
<?php if($isAdmin): ?>
|
|
<option value="0">ALL</option>
|
|
<?php endif; ?>
|
|
|
|
<?php foreach($driverList as $d): ?>
|
|
<option value="<?=$d['dr_uid']?>"
|
|
<?=($selectedDriver == $d['dr_uid'])?'selected':''?>>
|
|
<?=$d['dr_initial']?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
|
|
</select>
|
|
</div>
|
|
|
|
<!-- TOGGLE -->
|
|
<div class="radio-segment d-flex" style="width:100%;">
|
|
<label class="w-50 text-center">
|
|
<input type="radio" name="view_mode" value="list" checked>
|
|
<span>LIST</span>
|
|
</label>
|
|
<label class="w-50 text-center">
|
|
<input type="radio" name="view_mode" value="map">
|
|
<span>MAP</span>
|
|
</label>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LOAD BEFORE -->
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<h6 class="mb-2">Load Before Start</h6>
|
|
<div id="summaryInstall"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LOAD AFTER -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h6 class="mb-2">Expected Return</h6>
|
|
<div id="summaryPickup"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ===== RIGHT PANEL ===== -->
|
|
<div class="col-md-10">
|
|
|
|
<!-- LIST -->
|
|
<div id="listSection">
|
|
<div id="installList"></div>
|
|
</div>
|
|
|
|
<!-- MAP -->
|
|
<div id="mapSection" class="card d-none">
|
|
<div class="card-body p-0">
|
|
<div id="installMap" style="height:80vh;"></div>
|
|
|
|
<div id="installMapLegend" class="install-map-legend"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- =============================== -->
|
|
<!-- WORK MODAL (TR CLICK) -->
|
|
<!-- =============================== -->
|
|
<div class="modal fade" id="workModal" tabindex="-1" aria-labelledby="workModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
|
|
<div class="modal-header">
|
|
<div>
|
|
<h5 class="modal-title" id="workModalLabel">Work</h5>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
|
|
<div class="modal-body" style="padding: 1.25rem;">
|
|
|
|
<!-- Summary -->
|
|
<div class="mb-3">
|
|
<div class="fw-bold" id="wm_customer"></div>
|
|
<div class="small text-muted" id="wm_address"></div>
|
|
<div class="small text-muted" id="wm_phone"></div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-start justify-content-between mb-2" id="wm_note_row">
|
|
<div id="wm_note_area" class="small text-muted flex-grow-1"></div>
|
|
|
|
<div class="ms-2 flex-shrink-0">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="wm_instruction_photos_btn">
|
|
<i class="bi bi-images"></i>
|
|
Instruction
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="wm_action_area"></div>
|
|
|
|
<hr>
|
|
|
|
<div class="row g-3">
|
|
<!-- Left: Comment -->
|
|
<div class="col-md-7">
|
|
<table class="tb-info-box">
|
|
<colgroup>
|
|
<col style="width:20%">
|
|
<col style="width:80%">
|
|
</colgroup>
|
|
<tr>
|
|
<td class="td-title-info">Result</td>
|
|
<td class="td-text-info">
|
|
<input type="radio" class="btn-check" name="wm_result" id="wm_result_asplanned" value="AS_PLANNED" autocomplete="off" checked>
|
|
<label class="btn" for="wm_result_asplanned">As planned</label>
|
|
|
|
<input type="radio" class="btn-check" name="wm_result" id="wm_result_modified" value="MODIFIED" autocomplete="off">
|
|
<label class="btn" for="wm_result_modified">Modified</label>
|
|
|
|
<input type="radio" class="btn-check" name="wm_result" id="wm_result_cancelled" value="CANCELLED" autocomplete="off">
|
|
<label class="btn" for="wm_result_cancelled">Cancelled</label>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="td-title-info">Install Note</td>
|
|
<td class="td-text-info"><textarea class="form-control" id="wm_comment" rows="2" style="margin-bottom:0px;"></textarea></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="td-title-info">Additional Note</td>
|
|
<td class="td-text-info"><textarea class="form-control" id="wm_add_comment" rows="2" style="margin-bottom:0px;"></textarea></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="td-title-info">Oil Quantity</td>
|
|
<td class="td-text-info"><input type="number" id="wm_quantity" minlength="1" maxlength="10" style="width:100%;"></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="td-title-info">Sludge</td>
|
|
<td class="td-text-info"><input type="number" id="wm_sludge" minlength="1" maxlength="10" style="width:100%;"></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Right: After Photos -->
|
|
<div class="col-md-5">
|
|
<div id="wm_photo_section" class="row g-2">
|
|
|
|
<!-- BEFORE -->
|
|
<div id="wm_before_wrap" class="col-6" style="display:none;">
|
|
<div class="photo-card before">
|
|
<div class="photo-header">Before</div>
|
|
|
|
<div id="wm_before_photo_list" class="photo-body"></div>
|
|
|
|
<input type="file" class="form-control form-control-sm mt-2" id="wm_photos_before" accept="image/*">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AFTER -->
|
|
<div id="wm_after_wrap" class="col-12">
|
|
<div class="photo-card after">
|
|
<div class="photo-header">After</div>
|
|
|
|
<div id="wm_after_photo_list" class="photo-body"></div>
|
|
|
|
<input type="file" class="form-control form-control-sm mt-2" id="wm_photos_after" accept="image/*">
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-primary" id="wm_save">Save</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- =============================== -->
|
|
<!-- IMAGE MODAL -->
|
|
<!-- =============================== -->
|
|
<div class="modal fade" id="customerImageModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-image"></i> Customer Images
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
|
|
<div class="modal-body" id="customerImageModalBody">
|
|
<div class="text-center text-muted py-5">
|
|
Loading images...
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- =============================== -->
|
|
<!-- MESSAGE MODAL -->
|
|
<!-- =============================== -->
|
|
<div class="modal fade" id="myModalPopup" tabindex="-1" role="dialog" aria-labelledby="myModalPopup" style="opacity: 0.5; padding-right: 0px !important;">
|
|
<div class="modal-dialog modal-dialog-centered modal-xl" role="document">
|
|
<div class="modal-content" style="background-color:#2A9B56 !important; max-width:260px; margin: 0 auto;">
|
|
<div class="myModalPopup-body" style="text-align: center; border-radius: 5px; max-height:60px; font-size:18px; background-color:#2A9B56; color: #FFFFFF; font-weight: bold;padding:0.5em 1em; ">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- =============================== -->
|
|
<!-- JAVASCRIPT -->
|
|
<!-- =============================== -->
|
|
<script>
|
|
let installData = <?=json_encode($installData)?>;
|
|
let driverInitialMap = <?=json_encode($driverInitialMap)?>;
|
|
let reorderModeDriverUid = null;
|
|
let reorderSelectedRow = null; // currently selected for reorder
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
renderList();
|
|
renderSummary();
|
|
initWorkModal();
|
|
|
|
$('#installDate').datepicker({
|
|
dateFormat: 'yy-mm-dd',
|
|
changeMonth: true,
|
|
changeYear: true,
|
|
onSelect: function(dateText) {
|
|
reloadPage();
|
|
}
|
|
});
|
|
|
|
// installList 안의 tr 클릭
|
|
document.getElementById('installList').addEventListener('click', function(e){
|
|
|
|
// Reorder 버튼
|
|
const btn = e.target.closest('.btn-reorder');
|
|
if(btn){
|
|
const driverUid = btn.dataset.driverUid;
|
|
toggleReorderMode(driverUid, btn);
|
|
return; // 여기서 끝
|
|
}
|
|
|
|
// ▲ 이동
|
|
const btnUp = e.target.closest('.btn-move-up');
|
|
if(btnUp){
|
|
moveUp();
|
|
return;
|
|
}
|
|
|
|
// ▼ 이동
|
|
const btnDown = e.target.closest('.btn-move-down');
|
|
if(btnDown){
|
|
moveDown();
|
|
return;
|
|
}
|
|
|
|
const btnRoute = e.target.closest('.btn-route');
|
|
if(btnRoute){
|
|
openRoute(btnRoute.dataset.driverUid);
|
|
return;
|
|
}
|
|
|
|
// Row 클릭
|
|
const tr = e.target.closest('tr[data-uid]');
|
|
if(!tr) return;
|
|
|
|
// =========================
|
|
// reorder 모드일 때
|
|
// =========================
|
|
if (reorderModeDriverUid) {
|
|
|
|
const tbody = tr.closest('tbody[id^="tbody_"]');
|
|
if (!tbody || tbody.id !== `tbody_${reorderModeDriverUid}`) {
|
|
return;
|
|
}
|
|
|
|
// status-A만 선택 가능
|
|
if (!tr.classList.contains('status-A')) return;
|
|
|
|
selectReorderRow(tr);
|
|
updateMoveButtons(reorderModeDriverUid);
|
|
|
|
return; // 모달 열지 않음
|
|
}
|
|
|
|
// =========================
|
|
// 일반 모드
|
|
// =========================
|
|
openWorkModal(tr.dataset.uid);
|
|
});
|
|
});
|
|
|
|
function selectReorderRow(tr){
|
|
|
|
// 기존 선택 제거
|
|
document.querySelectorAll('.reorder-selected-row').forEach(r=>{
|
|
r.classList.remove('reorder-selected-row');
|
|
});
|
|
|
|
// 현재 선택
|
|
tr.classList.add('reorder-selected-row');
|
|
reorderSelectedRow = tr;
|
|
}
|
|
|
|
function updateMoveButtons(driverUid){
|
|
|
|
// 해당 드라이버 카드의 버튼 영역 가져오기
|
|
const controls = document.querySelector(`.reorder-controls[data-driver-uid="${driverUid}"]`);
|
|
if(!controls) return; // 없으면 종료
|
|
|
|
// 위/아래 버튼 가져오기
|
|
const upBtn = controls.querySelector('.btn-move-up');
|
|
const downBtn = controls.querySelector('.btn-move-down');
|
|
|
|
// 선택된 row가 없으면 둘 다 비활성
|
|
if(!reorderSelectedRow){
|
|
upBtn.disabled = true;
|
|
downBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
// 이전 형제가 없으면 (맨 위) 비활성
|
|
upBtn.disabled = !reorderSelectedRow.previousElementSibling;
|
|
|
|
// 다음 형제가 없으면 (맨 아래) 비활성
|
|
downBtn.disabled = !reorderSelectedRow.nextElementSibling;
|
|
}
|
|
|
|
function moveUp(){
|
|
|
|
// 선택된 row가 없으면 아무것도 안 함
|
|
if(!reorderSelectedRow) return;
|
|
|
|
// 현재 row의 바로 위 row 가져오기
|
|
const prev = reorderSelectedRow.previousElementSibling;
|
|
|
|
// 이미 맨 위면 이동 불가
|
|
if(!prev) return;
|
|
|
|
// 부모 tbody 가져오기
|
|
const tbody = reorderSelectedRow.parentNode;
|
|
|
|
// 현재 row를 prev 앞에 넣는다 (한 칸 위로 이동)
|
|
tbody.insertBefore(reorderSelectedRow, prev);
|
|
|
|
// 이동 후 순서 다시 계산
|
|
renumberActiveRowsFromDOM(tbody);
|
|
|
|
// 버튼 상태 갱신 (맨 위/아래 체크)
|
|
updateMoveButtons(reorderModeDriverUid);
|
|
}
|
|
|
|
function moveDown(){
|
|
|
|
// 선택된 row 없으면 종료
|
|
if(!reorderSelectedRow) return;
|
|
|
|
// 현재 row의 바로 아래 row 가져오기
|
|
const next = reorderSelectedRow.nextElementSibling;
|
|
|
|
// 이미 맨 아래면 이동 불가
|
|
if(!next) return;
|
|
|
|
// 부모 tbody 가져오기
|
|
const tbody = reorderSelectedRow.parentNode;
|
|
|
|
// next를 현재 row 앞으로 이동시킨다 (한 칸 아래로 이동)
|
|
tbody.insertBefore(next, reorderSelectedRow);
|
|
|
|
// 순번 다시 계산
|
|
renumberActiveRowsFromDOM(tbody);
|
|
|
|
// 버튼 상태 갱신
|
|
updateMoveButtons(reorderModeDriverUid);
|
|
}
|
|
|
|
function renumberActiveRowsFromDOM(tbody){
|
|
|
|
let maxSeq = 0;
|
|
|
|
// 기존 완료된 row들(P/M/X)에서 최대 seq 찾기
|
|
tbody.querySelectorAll('tr').forEach(tr => {
|
|
|
|
if(tr.classList.contains('status-A')) return; // A는 제외
|
|
|
|
const firstCell = tr.children[0];
|
|
if(!firstCell) return;
|
|
|
|
const val = parseInt(firstCell.textContent, 10);
|
|
if(!isNaN(val) && val > maxSeq){
|
|
maxSeq = val;
|
|
}
|
|
});
|
|
|
|
// A row는 maxSeq 다음부터 순번 부여
|
|
let seq = maxSeq + 1;
|
|
|
|
tbody.querySelectorAll('tr.status-A').forEach(tr => {
|
|
|
|
const firstCell = tr.children[0];
|
|
if(firstCell){
|
|
firstCell.textContent = seq;
|
|
}
|
|
|
|
seq++;
|
|
});
|
|
}
|
|
|
|
/* ===============================
|
|
Modal
|
|
=================================*/
|
|
let workModal; // bootstrap modal instance
|
|
let selectedRow = null; // currently opened row data
|
|
|
|
function initWorkModal(){
|
|
const el = document.getElementById('workModal');
|
|
if(!el) return;
|
|
workModal = new bootstrap.Modal(el, {
|
|
backdrop: true,
|
|
keyboard: true
|
|
});
|
|
|
|
bindPhotoInput('wm_photos_after', 'wm_after_photo_list');
|
|
bindPhotoInput('wm_photos_before', 'wm_before_photo_list');
|
|
|
|
// Save 버튼
|
|
document.getElementById('wm_save').addEventListener('click', function(){
|
|
|
|
if(!selectedRow) return;
|
|
const ctx = document.getElementById('installOrderContext');
|
|
const memberUid = ctx ? (ctx.dataset.memberUid || '') : '';
|
|
const result = document.querySelector('input[name="wm_result"]:checked')?.value || 'AS_PLANNED';
|
|
const status = mapResultToStatus(result);
|
|
const install_note = (document.getElementById('wm_comment').value || '').trim();
|
|
const additional_note = (document.getElementById('wm_add_comment').value || '').trim();
|
|
const oil_qty = parseInt(document.getElementById('wm_quantity').value || '0', 10) || 0;
|
|
const sludge = parseInt(document.getElementById('wm_sludge').value || '0', 10) || 0;
|
|
|
|
if(result !== 'AS_PLANNED' && !install_note){
|
|
alert('Please enter a comment.');
|
|
return;
|
|
}
|
|
|
|
|
|
api('daily_install_update_status', {
|
|
di_uid: selectedRow.di_uid,
|
|
member_uid: memberUid,
|
|
status: status,
|
|
install_note,
|
|
additional_note,
|
|
oil_qty,
|
|
sludge
|
|
}, (res) => {
|
|
|
|
if(!res || res.ok != 1){
|
|
//alert(res?.msg || 'Save failed');
|
|
showPopupMessage(res?.msg || 'Save failed', 'error', 1500);
|
|
return;
|
|
}
|
|
|
|
const workType = selectedRow.di_work_type;
|
|
const afterFileInput = document.getElementById('wm_photos_after');
|
|
const beforeFileInput = document.getElementById('wm_photos_before');
|
|
const afterFile = afterFileInput?.files?.[0] || null;
|
|
const beforeFile = beforeFileInput?.files?.[0] || null;
|
|
|
|
let uploads = [];
|
|
|
|
// AFTER
|
|
if(afterFile){
|
|
const type = (workType === 'CLEAN') ? 'clean_after' : 'install';
|
|
uploads.push(uploadPhotoPromise(selectedRow, afterFile, type));
|
|
}
|
|
|
|
// BEFORE (CLEAN만)
|
|
if(workType === 'CLEAN' && beforeFile){
|
|
uploads.push(uploadPhotoPromise(selectedRow, beforeFile, 'clean_before'));
|
|
}
|
|
|
|
if(uploads.length > 0){
|
|
Promise.all(uploads).then(results => {
|
|
|
|
const hasError = results.some(r => !r || r.ok != 1);
|
|
|
|
if(hasError){
|
|
showPopupMessage('Saved (photo partial fail)', 'error', 1500);
|
|
}else{
|
|
showPopupMessage('Successfully Saved', 'success', 1000);
|
|
}
|
|
|
|
location.reload();
|
|
});
|
|
}else{
|
|
showPopupMessage('Successfully Saved', 'success', 1000);
|
|
location.reload();
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
function mapResultToStatus(result){
|
|
if(result === 'AS_PLANNED') return 'P';
|
|
if(result === 'MODIFIED') return 'M';
|
|
if(result === 'CANCELLED') return 'X';
|
|
return 'P';
|
|
}
|
|
|
|
const btn = document.getElementById('wm_instruction_photos_btn');
|
|
if(btn){
|
|
btn.addEventListener('click', function(){
|
|
if(!selectedRow) return;
|
|
|
|
const customerUid = selectedRow.di_customer_uid;
|
|
openCustomerImages(customerUid, 'install_order');
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('input[name="wm_result"]').forEach(radio => {
|
|
radio.addEventListener('change', updateCommentReadonlyByResult);
|
|
});
|
|
|
|
// 모달이 닫힐 때 입력값 초기화
|
|
el.addEventListener('hidden.bs.modal', function(){
|
|
selectedRow = null;
|
|
document.getElementById('wm_comment').value = '';
|
|
document.getElementById('wm_add_comment').value = '';
|
|
document.getElementById('wm_note_area').innerHTML = '';
|
|
resetPhotoBlocks();
|
|
});
|
|
}
|
|
|
|
function findRowByUid(di_uid){
|
|
di_uid = String(di_uid);
|
|
return (installData || []).find(r => String(r.di_uid) === di_uid) || null;
|
|
}
|
|
|
|
async function openWorkModal(di_uid){
|
|
const row = findRowByUid(di_uid);
|
|
if(!row){
|
|
//alert('Row not found: ' + di_uid);
|
|
showPopupMessage('Row not found: ' + di_uid, 'error', 1500);
|
|
return;
|
|
}
|
|
selectedRow = row;
|
|
|
|
// ready
|
|
const address = `${row.di_address || ''}, ${row.di_city || ''}, ${row.di_postal || ''}`;
|
|
const phone = formatPhone(row.di_phone || '');
|
|
|
|
// basic info
|
|
document.getElementById('workModalLabel').textContent = `#${row.di_order_seq || ''} - ${row.di_work_type || 'WORK'}`;
|
|
document.getElementById('wm_customer').textContent = `${row.di_customer_name || ''} (${row.di_accountno || ''})`;
|
|
document.getElementById('wm_address').innerHTML = `${escapeHtml(address)} ${phone ? ` <span class="text-muted">· ${escapeHtml(phone)}</span>` : ''}`;
|
|
document.getElementById('wm_comment').value = row.di_install_note || '';
|
|
document.getElementById('wm_add_comment').value = row.di_additional_note || '';
|
|
document.getElementById('wm_quantity').value = row.di_oil_pickup || '';
|
|
document.getElementById('wm_sludge').value = row.di_sludge || '';
|
|
|
|
// 상태 세팅
|
|
setResultRadioByStatus(row.di_status);
|
|
updateCommentReadonlyByResult();
|
|
|
|
// note
|
|
renderWorkNotes(row);
|
|
|
|
// container
|
|
renderWorkActionBoxes();
|
|
fillWorkModalContainers(row);
|
|
|
|
// photos
|
|
togglePhotoSection(row.di_work_type);
|
|
await loadAfterPhoto(row);
|
|
if(row.di_work_type === 'CLEAN'){
|
|
await loadBeforePhoto(row);
|
|
}
|
|
|
|
// open
|
|
workModal.show();
|
|
}
|
|
|
|
function renderWorkActionBoxes(){
|
|
|
|
const area = document.getElementById('wm_action_area');
|
|
area.innerHTML = `
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
${renderWorkActionBox('PICKUP')}
|
|
</div>
|
|
<div class="col-md-6">
|
|
${renderWorkActionBox('INSTALL')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderWorkActionBox(action){
|
|
|
|
const icon = {
|
|
PICKUP: 'bi-dash-circle',
|
|
INSTALL: 'bi-plus-circle'
|
|
}[action] || 'bi-box';
|
|
|
|
const colorClass =
|
|
action === 'PICKUP' ? 'pickup'
|
|
: action === 'INSTALL' ? 'install'
|
|
: '';
|
|
|
|
return `
|
|
<div class="mb-3 task-action-box" data-action="${action}">
|
|
<div class="task-action-title ${colorClass}">
|
|
<i class="bi ${icon}"></i> ${action}
|
|
</div>
|
|
|
|
<div class="mt-2 container-chips" data-action="${action}"></div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderContainerChipsForAction(containers, action){
|
|
|
|
const list = (containers || []).filter(c => String(c.dic_action || '').toUpperCase() === action);
|
|
if(list.length === 0){
|
|
return `<div class="text-muted small">No ${action.toLowerCase()} containers</div>`;
|
|
}
|
|
|
|
return list.map(c => {
|
|
|
|
// chip class
|
|
const isRestaurantOwned = (c.dic_owner_type === 'O');
|
|
const chipClass = isRestaurantOwned ? 'chip chip-restaurant' : 'chip chip-company';
|
|
|
|
// icon
|
|
const icon = getContainerIcon(c.dic_type);
|
|
|
|
return `
|
|
<span class="${chipClass}">
|
|
<i class="bi ${icon}"></i>
|
|
${isRestaurantOwned ? '<span class="ms-1 fw-bold">O</span>' : ''}
|
|
${c.dic_is_used === 'Y' ? '<span class="ms-1">Used</span>' : ''}
|
|
<span class="ms-1">${escapeHtml(c.dic_type || '')}</span>
|
|
${c.dic_has_lock === 'Y' ? '<i class="bi bi-lock-fill ms-1"></i>' : ''}
|
|
${c.dic_has_wheel === 'Y' ? '<i class="bi bi-gear-fill ms-1"></i>' : ''}
|
|
${c.dic_has_woodframe === 'Y' ? '<i class="bi bi-bounding-box-circles ms-1"></i>' : ''}
|
|
</span>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function fillWorkModalContainers(row){
|
|
|
|
const area = document.getElementById('wm_action_area');
|
|
if(!area) return;
|
|
|
|
const containers = row?.containers || [];
|
|
|
|
const pickupWrap = area.querySelector('.container-chips[data-action="PICKUP"]');
|
|
if(pickupWrap){
|
|
pickupWrap.innerHTML = renderContainerChipsForAction(containers, 'PICKUP');
|
|
}
|
|
|
|
const installWrap = area.querySelector('.container-chips[data-action="INSTALL"]');
|
|
if(installWrap){
|
|
installWrap.innerHTML = renderContainerChipsForAction(containers, 'INSTALL');
|
|
}
|
|
}
|
|
|
|
function renderWorkNotes(row){
|
|
|
|
const area = document.getElementById('wm_note_area');
|
|
if(!area) return;
|
|
|
|
const req = (row.di_request_note || '').trim();
|
|
const work = (row.di_work_note || '').trim();
|
|
|
|
let html = '';
|
|
|
|
if(req){
|
|
html += `
|
|
<div class="small text-muted mb-1">
|
|
<strong>Request:</strong>
|
|
${escapeHtml(req)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if(work){
|
|
html += `
|
|
<div class="small text-muted mb-1">
|
|
<strong>Work:</strong>
|
|
${escapeHtml(work)}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
area.innerHTML = html;
|
|
}
|
|
|
|
function setResultRadioByStatus(status){
|
|
|
|
// 기본 초기화
|
|
document.querySelectorAll('input[name="wm_result"]').forEach(r => r.checked = false);
|
|
|
|
if(status === 'P') document.getElementById('wm_result_asplanned').checked = true;
|
|
else if(status === 'M') document.getElementById('wm_result_modified').checked = true;
|
|
else if(status === 'X') document.getElementById('wm_result_cancelled').checked = true;
|
|
else document.getElementById('wm_result_asplanned').checked = true;
|
|
}
|
|
|
|
function updateCommentReadonlyByResult(){
|
|
|
|
const result = document.querySelector('input[name="wm_result"]:checked')?.value;
|
|
const textarea = document.getElementById('wm_comment');
|
|
|
|
if(!textarea) return;
|
|
|
|
if(result === 'AS_PLANNED'){
|
|
textarea.setAttribute('readonly', 'readonly');
|
|
textarea.classList.add('bg-light');
|
|
|
|
// 이미 저장된 노트가 있으면 그걸 우선 사용
|
|
if (selectedRow && (selectedRow.di_install_note || '').trim()) {
|
|
textarea.value = selectedRow.di_install_note;
|
|
return;
|
|
}
|
|
|
|
// 저장된 노트가 없을 때만 자동 생성
|
|
if(selectedRow){
|
|
const mapped = (selectedRow.containers || []).map(c => ({
|
|
action: (c.dic_action || '').toUpperCase(),
|
|
type: c.dic_type,
|
|
used: c.dic_is_used === 'Y',
|
|
wheel: c.dic_has_wheel === 'Y',
|
|
lock: c.dic_has_lock === 'Y',
|
|
frame: c.dic_has_woodframe === 'Y'
|
|
}));
|
|
|
|
textarea.value = generateAutoComment(mapped);
|
|
}
|
|
|
|
}else{
|
|
textarea.removeAttribute('readonly');
|
|
textarea.classList.remove('bg-light');
|
|
}
|
|
}
|
|
|
|
function generateAutoComment(containers){
|
|
|
|
let install = {};
|
|
let pickup = {};
|
|
|
|
containers.forEach(c=>{
|
|
|
|
const key = buildContainerLabel(c);
|
|
|
|
if(c.action === 'INSTALL'){
|
|
install[key] = (install[key] || 0) + 1;
|
|
}
|
|
|
|
if(c.action === 'PICKUP'){
|
|
pickup[key] = (pickup[key] || 0) + 1;
|
|
}
|
|
});
|
|
|
|
let parts = [];
|
|
|
|
Object.entries(install).forEach(([k,v])=>{
|
|
parts.push(`Installed ${v} ${k}`);
|
|
});
|
|
|
|
Object.entries(pickup).forEach(([k,v])=>{
|
|
parts.push(`Picked up ${v} ${k}`);
|
|
});
|
|
|
|
if(parts.length === 0){
|
|
return 'Completed as planned.';
|
|
}
|
|
|
|
return parts.join(', ') + ' as planned.';
|
|
}
|
|
|
|
function buildContainerLabel(c){
|
|
|
|
let label = '';
|
|
|
|
if(c.used){
|
|
label += 'Used ';
|
|
}
|
|
|
|
// 기본 타입
|
|
label += c.type;
|
|
|
|
const suffix = [];
|
|
|
|
if(c.wheel) suffix.push('W');
|
|
if(c.lock) suffix.push('L');
|
|
if(c.frame) suffix.push('F');
|
|
|
|
if(suffix.length > 0){
|
|
label += '/' + suffix.join('/');
|
|
}
|
|
|
|
return label;
|
|
}
|
|
|
|
/* ===============================
|
|
Image
|
|
=================================*/
|
|
async function fetchWorkImage(type, sourceuid){
|
|
const qs = new URLSearchParams({
|
|
view: 'install_order',
|
|
ajax: 'work_images',
|
|
type,
|
|
sourceuid
|
|
});
|
|
|
|
const res = await fetch(`index_intranet.php?${qs.toString()}`);
|
|
const json = await res.json();
|
|
if(!json || json.ok !== true) return null;
|
|
return json.data || null;
|
|
}
|
|
|
|
async function loadAfterPhoto(row){
|
|
const workType = row.di_work_type;
|
|
let type = 'install';
|
|
if(workType === 'CLEAN'){
|
|
type = 'clean_after';
|
|
}
|
|
|
|
const data = await fetchWorkImage(type, row.di_uid);
|
|
row._afterPhoto = data;
|
|
renderAfterPhoto(data);
|
|
}
|
|
|
|
async function loadBeforePhoto(row){
|
|
|
|
if(row.di_work_type !== 'CLEAN') return;
|
|
|
|
const data = await fetchWorkImage('clean_before', row.di_uid);
|
|
row._beforePhoto = data;
|
|
renderBeforePhoto(data);
|
|
}
|
|
|
|
function renderAfterPhoto(photo){
|
|
|
|
const wrap = document.getElementById('wm_after_photo_list');
|
|
if(!wrap) return;
|
|
|
|
if(!photo || !photo.imgPath){
|
|
wrap.innerHTML = `<div class="small text-muted">No after photo.</div>`;
|
|
return;
|
|
}
|
|
|
|
const url = photo.imgPath;
|
|
|
|
wrap.innerHTML = `
|
|
<div style="width:100%;">
|
|
<img src="${escapeHtml(url)}"
|
|
style="width:100%; height:160px; object-fit:cover; border-radius:8px; border:1px solid #ddd; cursor:pointer;"
|
|
class="after-photo-thumb"
|
|
data-img-url="${escapeHtml(url)}">
|
|
</div>
|
|
`;
|
|
|
|
// 클릭 이벤트 연결
|
|
const imgEl = wrap.querySelector('.after-photo-thumb');
|
|
if(imgEl){
|
|
imgEl.addEventListener('click', function(){
|
|
|
|
const modalEl = document.getElementById('customerImageModal');
|
|
const body = document.getElementById('customerImageModalBody');
|
|
|
|
if(!modalEl || !body) return;
|
|
|
|
body.innerHTML = `
|
|
<div class="text-center">
|
|
<img src="${escapeHtml(url)}"
|
|
style="max-width:100%; max-height:75vh; border-radius:10px;">
|
|
</div>
|
|
`;
|
|
|
|
const modal = new bootstrap.Modal(modalEl);
|
|
modal.show();
|
|
});
|
|
}
|
|
}
|
|
|
|
function renderBeforePhoto(photo){
|
|
|
|
const wrap = document.getElementById('wm_before_photo_list');
|
|
if(!wrap) return;
|
|
|
|
if(!photo || !photo.imgPath){
|
|
wrap.innerHTML = `<div class="small text-muted">No before photo.</div>`;
|
|
return;
|
|
}
|
|
|
|
const url = photo.imgPath;
|
|
|
|
wrap.innerHTML = `
|
|
<div style="width:100%;">
|
|
<img src="${escapeHtml(url)}"
|
|
style="width:100%; height:160px; object-fit:cover; border-radius:8px; border:1px solid #ddd; cursor:pointer;">
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function resetPhotoBlocks(){
|
|
|
|
// AFTER
|
|
const afterList = document.getElementById('wm_after_photo_list');
|
|
if(afterList) afterList.innerHTML = '';
|
|
|
|
const afterInput = document.getElementById('wm_photos_after');
|
|
if(afterInput) afterInput.value = '';
|
|
|
|
// BEFORE
|
|
const beforeList = document.getElementById('wm_before_photo_list');
|
|
if(beforeList) beforeList.innerHTML = '';
|
|
|
|
const beforeInput = document.getElementById('wm_photos_before');
|
|
if(beforeInput) beforeInput.value = '';
|
|
|
|
if(selectedRow){
|
|
selectedRow._afterPhoto = null;
|
|
selectedRow._beforePhoto = null;
|
|
}
|
|
}
|
|
|
|
function openCustomerImages(customerUid, iType = 'install_order') {
|
|
|
|
const modalEl = document.getElementById('customerImageModal');
|
|
if(!modalEl){
|
|
console.error('customerImageModal not found');
|
|
return;
|
|
}
|
|
|
|
const modal = new bootstrap.Modal(modalEl);
|
|
const body = document.getElementById('customerImageModalBody');
|
|
|
|
body.innerHTML = '<div class="text-center text-muted py-5">Loading images...</div>';
|
|
modal.show();
|
|
|
|
const params = new URLSearchParams({
|
|
customer_uid: customerUid,
|
|
i_type: iType
|
|
});
|
|
|
|
fetch('/lib/customer_image.php?' + params.toString(), {
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(res => res.text())
|
|
.then(html => {
|
|
body.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
body.innerHTML = '<div class="text-danger text-center py-5">Failed to load images.</div>';
|
|
});
|
|
}
|
|
|
|
function bindPhotoInput(inputId, targetWrapId){
|
|
|
|
const input = document.getElementById(inputId);
|
|
if(!input) return;
|
|
|
|
input.addEventListener('change', function(e){
|
|
|
|
const file = e.target.files[0];
|
|
if(!file) return;
|
|
|
|
if(!file.type.startsWith('image/')){
|
|
alert('Please select an image file.');
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function(ev){
|
|
renderPreviewImage(ev.target.result, targetWrapId);
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
function renderPreviewImage(base64, target){
|
|
|
|
const wrap = document.getElementById(target);
|
|
if(!wrap) return;
|
|
|
|
wrap.innerHTML = `
|
|
<div style="width:100%; position:relative;">
|
|
<img src="${base64}"
|
|
style="width:100%; height:160px; object-fit:cover; border-radius:8px;">
|
|
|
|
<div style="
|
|
position:absolute;
|
|
top:6px;
|
|
left:6px;
|
|
background:#000000aa;
|
|
color:#fff;
|
|
font-size:11px;
|
|
padding:2px 6px;
|
|
border-radius:4px;
|
|
">
|
|
Preview
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function togglePhotoSection(workType){
|
|
|
|
const beforeWrap = document.getElementById('wm_before_wrap');
|
|
const afterWrap = document.getElementById('wm_after_wrap');
|
|
|
|
if(workType === 'CLEAN'){
|
|
// BEFORE 보이기
|
|
beforeWrap.style.display = '';
|
|
beforeWrap.classList.remove('col-12');
|
|
beforeWrap.classList.add('col-6');
|
|
afterWrap.classList.remove('col-12');
|
|
afterWrap.classList.add('col-6');
|
|
|
|
}else{
|
|
// BEFORE 숨김
|
|
beforeWrap.style.display = 'none';
|
|
afterWrap.classList.remove('col-6');
|
|
afterWrap.classList.add('col-12');
|
|
}
|
|
}
|
|
|
|
function uploadPhotoPromise(row, file, type){
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
const ctx = document.getElementById('installOrderContext');
|
|
const memberUid = ctx ? (ctx.dataset.memberUid || '') : '';
|
|
|
|
const fd = new FormData();
|
|
fd.append('di_uid', row.di_uid);
|
|
fd.append('member_uid', memberUid);
|
|
fd.append('note', (document.getElementById('wm_comment').value || '').trim());
|
|
fd.append('i_type', type);
|
|
fd.append('photo', file);
|
|
|
|
fetch('/assets/internal_api.php?func=upload_after_photo', {
|
|
method: 'POST',
|
|
body: fd
|
|
})
|
|
.then(r => r.json())
|
|
.then(json => resolve(json))
|
|
.catch(err => {
|
|
console.error(err);
|
|
resolve({ ok:0 });
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
/* ===============================
|
|
Date
|
|
=================================*/
|
|
document.getElementById('installDate').addEventListener('change', reloadPage);
|
|
|
|
function parseDate(str) {
|
|
const [y, m, d] = (str || '').split('-').map(Number);
|
|
return new Date(y, m - 1, d); // ✅ 로컬 날짜로 생성
|
|
}
|
|
|
|
function formatDate(d) {
|
|
const y = d.getFullYear();
|
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
return `${y}-${m}-${day}`; // ✅ 로컬 날짜로 문자열
|
|
}
|
|
|
|
function moveBusinessDate(date, diffDays) {
|
|
const d = new Date(date);
|
|
let moved = 0;
|
|
|
|
while (moved < Math.abs(diffDays)) {
|
|
d.setDate(d.getDate() + Math.sign(diffDays));
|
|
const day = d.getDay();
|
|
if (day !== 0 && day !== 6) moved++;
|
|
}
|
|
return d;
|
|
}
|
|
|
|
function adjustDate(diff) {
|
|
const input = document.getElementById('installDate');
|
|
let val = input.value;
|
|
|
|
if (!val) return; // 또는 기본값 세팅 원하면 여기서 처리
|
|
|
|
const current = parseDate(val);
|
|
const next = moveBusinessDate(current, diff);
|
|
|
|
input.value = formatDate(next);
|
|
input.dispatchEvent(new Event('change'));
|
|
}
|
|
|
|
document.getElementById('prevDay').onclick = () => adjustDate(-1);
|
|
document.getElementById('nextDay').onclick = () => adjustDate(1);
|
|
|
|
/* ===============================
|
|
List
|
|
=================================*/
|
|
function renderList(){
|
|
|
|
const container = document.getElementById('installList');
|
|
container.innerHTML = '';
|
|
|
|
// ===== 전체 데이터 없음 =====
|
|
if(!installData || installData.length === 0){
|
|
installData = [];
|
|
}
|
|
|
|
// ===== driver 목록 확보 (ALL일 때 빈 driver도 보여주기) =====
|
|
const grouped = {};
|
|
|
|
// 기존 데이터 그룹핑
|
|
installData.forEach(row => {
|
|
const d = row.di_driver_uid;
|
|
if(!grouped[d]) grouped[d] = [];
|
|
grouped[d].push(row);
|
|
});
|
|
|
|
// ALL 모드일 경우 driverInitialMap 기준으로 전체 driver 보장
|
|
const driverSelect = document.getElementById('driverSelect');
|
|
const selectedDriver = driverSelect ? String(driverSelect.value) : '0';
|
|
let driverUids;
|
|
if(selectedDriver !== '0'){
|
|
driverUids = [selectedDriver];
|
|
}else{
|
|
driverUids = Object.keys(driverInitialMap).filter(uid => String(uid) !== '0');
|
|
}
|
|
driverUids.forEach(driverUid => {
|
|
|
|
const driverInitial = driverInitialMap[driverUid] || driverUid;
|
|
const rows = grouped[driverUid] || [];
|
|
|
|
let html = `
|
|
<div class="card mb-4 install-driver-card" data-driver-uid="${driverUid}">
|
|
<div class="fw-bold d-flex justify-content-between align-items-center iw-header" style="padding: 8px 16px;">
|
|
<div>${driverInitial}</div>
|
|
|
|
<div class="d-flex align-items-center gap-2 reorder-controls" data-driver-uid="${driverUid}">
|
|
<button type="button"
|
|
class="btn btn-sm btn-light btn-move-down d-none"
|
|
data-driver-uid="${driverUid}"
|
|
disabled>
|
|
<i class="bi bi-chevron-down"></i>
|
|
</button>
|
|
|
|
<button type="button"
|
|
class="btn btn-sm btn-light btn-move-up d-none"
|
|
data-driver-uid="${driverUid}"
|
|
disabled>
|
|
<i class="bi bi-chevron-up"></i>
|
|
</button>
|
|
|
|
<button type="button"
|
|
class="btn btn-sm btn-outline-light btn-reorder"
|
|
data-driver-uid="${driverUid}">
|
|
<i class="bi bi-arrow-down-up"></i> Reorder
|
|
</button>
|
|
|
|
<button type="button"
|
|
class="btn btn-sm btn-outline-light btn-route"
|
|
data-driver-uid="${driverUid}">
|
|
<i class="bi bi-geo-alt"></i> Route
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<table class="table table-sm table-hover mb-0 install-order-table" style="table-layout: fixed; width:100%;">
|
|
<colgroup>
|
|
<col style="width:2%">
|
|
<col style="width:10%">
|
|
<col style="width:12%">
|
|
<col style="width:26%">
|
|
<col style="width:24%">
|
|
<col style="width:26%">
|
|
</colgroup>
|
|
<tbody id="tbody_${driverUid}">
|
|
`;
|
|
|
|
// ===== 데이터 없을 때 =====
|
|
if(rows.length === 0){
|
|
html += `
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted py-3">
|
|
No orders.
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}else{
|
|
|
|
rows.forEach(row => {
|
|
const req = (row.di_request_note || '').trim();
|
|
const work = (row.di_work_note || '').trim();
|
|
|
|
html += `
|
|
<tr data-uid="${row.di_uid}" class="status-${row.di_status}">
|
|
<td class="fw-bold">${row.di_order_seq}</td>
|
|
<td>${renderWorkCell(row)}</td>
|
|
<td>
|
|
<div>
|
|
<span class="badge bg-light text-dark border">
|
|
${escapeHtml(row.di_accountno || '')}
|
|
</span>
|
|
</div>
|
|
<div class="small fw-bold text-truncate" title="${escapeHtml(row.di_customer_name || '')}" style="max-width:140px;">
|
|
${escapeHtml(row.di_customer_name || '')}
|
|
</div>
|
|
</td>
|
|
<td>${renderContainers(row.containers)}</td>
|
|
<td class="small text-muted">
|
|
<div>
|
|
${escapeHtml(row.di_address)},
|
|
${escapeHtml(row.di_city)}
|
|
</div>
|
|
<div>
|
|
${escapeHtml(row.di_postal)}
|
|
${formatPhone(row.di_phone)}
|
|
</div>
|
|
</td>
|
|
<td class="small text-muted">
|
|
${req ? `
|
|
<div class="small text-muted mb-1">
|
|
<strong>Request:</strong> ${escapeHtml(req)}
|
|
</div>
|
|
` : ''}
|
|
|
|
${work ? `
|
|
<div class="small text-muted">
|
|
<strong>Work:</strong> ${escapeHtml(work)}
|
|
</div>
|
|
` : ''}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += `
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML += html;
|
|
|
|
});
|
|
}
|
|
|
|
function renderWorkCell(row){
|
|
|
|
const status = row.di_status;
|
|
|
|
// 기본 텍스트
|
|
let main = '';
|
|
|
|
if(status === 'A'){
|
|
main = `${renderWorkType(row.di_work_type)}`;
|
|
}else if(status === 'P'){
|
|
main = `<span class="fw-bold small"> Done</span>`;
|
|
}else if(status === 'M'){
|
|
main = `<span class="fw-bold small"> Modified</span>`;
|
|
}else if(status === 'X'){
|
|
main = `<span class="fw-bold small"> Cancelled</span>`;
|
|
}
|
|
|
|
// 보조 아이콘
|
|
const icons = [];
|
|
|
|
const applied = String(row.di_container_applied || '0') === '1';
|
|
if(applied > 0){
|
|
icons.push(`<i class="bi bi-database-check" title="Container applied"></i>`);
|
|
}
|
|
|
|
const note = (row.di_additional_note || '').trim();
|
|
if(note.length > 0){
|
|
icons.push(`<i class="bi bi-journal-text" title="Additional note"></i>`);
|
|
}
|
|
|
|
const oil = parseInt(row.di_oil_pickup || '0', 10) || 0;
|
|
if(oil > 0){
|
|
icons.push(`<i class="bi bi-droplet-fill" title="Oil pickup: ${oil}"></i>`);
|
|
}
|
|
|
|
const hasImage = String(row.has_image || '0') === '1';
|
|
if(hasImage){
|
|
icons.push(`<i class="bi bi-image" title="Has image"></i>`);
|
|
}
|
|
|
|
const iconRow = icons.length
|
|
? `<div class="mt-1 small text-muted">${icons.join('<span class="ms-2"></span>')}</div>`
|
|
: '';
|
|
|
|
// 두 줄 구조
|
|
return `
|
|
<div>
|
|
<div>${main}</div>
|
|
${iconRow}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderWorkType(type){
|
|
let badgeClass = 'bg-secondary';
|
|
switch(type){
|
|
case 'INSTALL':
|
|
badgeClass = 'bg-success text-white';
|
|
break;
|
|
case 'PICKUP':
|
|
badgeClass = 'bg-danger text-white';
|
|
break;
|
|
case 'EXCHANGE':
|
|
badgeClass = 'bg-warning text-dark';
|
|
break;
|
|
case 'RELOCATE':
|
|
badgeClass = 'bg-info text-dark';
|
|
break;
|
|
case 'CLEAN':
|
|
badgeClass = 'bg-primary text-white';
|
|
break;
|
|
}
|
|
return `<span class="badge ${badgeClass}">${type || '-'}</span>`;
|
|
}
|
|
|
|
function renderContainers(containers){
|
|
|
|
if(!containers || containers.length === 0) return '';
|
|
|
|
let installChips = '';
|
|
let pickupChips = '';
|
|
|
|
containers.forEach(c => {
|
|
|
|
let chipClass = 'chip chip-company';
|
|
if(c.dic_owner_type === 'O'){
|
|
chipClass = 'chip chip-restaurant';
|
|
}
|
|
|
|
const icon = getContainerIcon(c.dic_type);
|
|
|
|
const chip = `
|
|
<span class="${chipClass}">
|
|
<i class="bi ${icon}"></i>
|
|
${c.dic_owner_type === 'O' ? '<span class="ms-1 fw-bold">O</span>' : ''}
|
|
${c.dic_is_used === 'Y' ? '<span class="ms-1">Used</span>' : ''}
|
|
<span class="ms-1">${c.dic_type}</span>
|
|
${c.dic_has_lock === 'Y' ? '<i class="bi bi-lock-fill ms-1"></i>' : ''}
|
|
${c.dic_has_wheel === 'Y' ? '<i class="bi bi-gear-fill ms-1"></i>' : ''}
|
|
${c.dic_has_woodframe === 'Y' ? '<i class="bi bi-bounding-box-circles ms-1"></i>' : ''}
|
|
</span>
|
|
`;
|
|
|
|
if(c.dic_action === 'INSTALL'){
|
|
installChips += chip;
|
|
} else {
|
|
pickupChips += chip;
|
|
}
|
|
});
|
|
|
|
let html = `<div class="container-chips inline-chips"">`;
|
|
|
|
if(installChips){
|
|
html += `
|
|
<div class="task-action-title install">
|
|
<i class="bi bi-plus-circle fs-6"></i>
|
|
</div>
|
|
${installChips}
|
|
`;
|
|
}
|
|
|
|
if(pickupChips){
|
|
html += `
|
|
${installChips ? '<span class="mx-1"></span>' : ''}
|
|
<div class="task-action-title pickup">
|
|
<i class="bi bi-dash-circle fs-6"></i>
|
|
</div>
|
|
${pickupChips}
|
|
`;
|
|
}
|
|
|
|
html += `</div>`;
|
|
|
|
return html;
|
|
}
|
|
|
|
function toggleReorderMode(driverUid, btn){
|
|
|
|
const controls = document.querySelector(`.reorder-controls[data-driver-uid="${driverUid}"]`);
|
|
if(!controls) return;
|
|
|
|
const upBtn = controls.querySelector('.btn-move-up');
|
|
const downBtn = controls.querySelector('.btn-move-down');
|
|
|
|
// 현재 상태 확인
|
|
const isActive = !upBtn.classList.contains('d-none');
|
|
|
|
// OFF → ON
|
|
if(!isActive){
|
|
|
|
reorderModeDriverUid = driverUid;
|
|
|
|
upBtn.classList.remove('d-none');
|
|
downBtn.classList.remove('d-none');
|
|
|
|
// 초기에는 비활성
|
|
upBtn.disabled = true;
|
|
downBtn.disabled = true;
|
|
|
|
btn.classList.add('btn-success'); // 활성 표시
|
|
btn.classList.remove('btn-outline-light');
|
|
|
|
//
|
|
btn.classList.remove('btn-outline-secondary');
|
|
btn.classList.add('btn-secondary');
|
|
btn.innerHTML = `<i class="bi bi-check2"></i> Done`;
|
|
//
|
|
|
|
}else{
|
|
// reorder 종료 → 저장
|
|
saveDriverOrder(driverUid);
|
|
|
|
// ON → OFF
|
|
reorderModeDriverUid = null;
|
|
|
|
upBtn.classList.add('d-none');
|
|
downBtn.classList.add('d-none');
|
|
|
|
btn.classList.remove('btn-success');
|
|
btn.classList.add('btn-outline-light');
|
|
|
|
// 선택 초기화 (다음 단계 대비)
|
|
clearReorderSelectedRow();
|
|
}
|
|
}
|
|
|
|
function clearReorderSelectedRow(){
|
|
document.querySelectorAll('.reorder-selected-row').forEach(tr=>{
|
|
tr.classList.remove('reorder-selected-row');
|
|
});
|
|
reorderSelectedRow = null;
|
|
}
|
|
|
|
function renumberActiveRows(driverUid){
|
|
const $tbody = $(`#tbody_${driverUid}`);
|
|
const $rowsA = $tbody.find('tr.status-A');
|
|
|
|
// 완료(P/M/X)가 위에 있다면 그 개수만큼 offset
|
|
const doneCnt = $tbody.find('tr:not(.status-A)').length;
|
|
let seq = doneCnt + 1;
|
|
|
|
$rowsA.each(function(){
|
|
$(this).find('td').first().text(seq);
|
|
seq++;
|
|
});
|
|
}
|
|
|
|
function saveDriverOrder(driverUid){
|
|
|
|
const tbody = document.getElementById(`tbody_${driverUid}`);
|
|
if(!tbody) return;
|
|
|
|
const rows = tbody.querySelectorAll("tr.status-A");
|
|
if(!rows.length){
|
|
//alert("No active rows to reorder.");
|
|
showPopupMessage('No rows to reorder.', 'error', 1500);
|
|
return;
|
|
}
|
|
|
|
const orderList = [];
|
|
rows.forEach((tr, index) => {
|
|
|
|
const diUid = tr.dataset.uid;
|
|
const newSeq = index + 1; // 화면상 순서
|
|
if(!diUid) return;
|
|
|
|
orderList.push({
|
|
di_uid: diUid,
|
|
di_order_seq: newSeq
|
|
});
|
|
});
|
|
|
|
api('daily_install_reorder', {
|
|
driver_uid: driverUid,
|
|
order_list: JSON.stringify(orderList)
|
|
}, (res) => {
|
|
|
|
if(!res || res.ok != 1){
|
|
//alert(res?.msg || 'Reorder save failed');
|
|
showPopupMessage('Save failed', 'error', 1500);
|
|
return;
|
|
}
|
|
showPopupMessage('Successfully Saved', 'success', 800);
|
|
setTimeout(function(){
|
|
location.reload();
|
|
}, 1500);
|
|
});
|
|
|
|
}
|
|
/* ===============================
|
|
SUMMARY
|
|
=================================*/
|
|
function renderSummary(){
|
|
|
|
const install = {};
|
|
const pickup = {};
|
|
|
|
const optionInstall = { lock:0, wheel:0, wood:0 };
|
|
const optionPickup = { lock:0, wheel:0, wood:0 };
|
|
|
|
(installData || []).forEach(row => {
|
|
|
|
(row.containers || []).forEach(c => {
|
|
|
|
// ===== 고유 key 구성 =====
|
|
let key = `${c.dic_type}|${c.dic_is_used}|${c.dic_owner_type}`;
|
|
|
|
if(c.dic_action === 'INSTALL'){
|
|
install[key] = (install[key] || 0) + 1;
|
|
|
|
if(c.dic_has_lock === 'Y') optionInstall.lock++;
|
|
if(c.dic_has_wheel === 'Y') optionInstall.wheel++;
|
|
if(c.dic_has_woodframe === 'Y') optionInstall.wood++;
|
|
|
|
} else {
|
|
|
|
pickup[key] = (pickup[key] || 0) + 1;
|
|
|
|
if(c.dic_has_lock === 'Y') optionPickup.lock++;
|
|
if(c.dic_has_wheel === 'Y') optionPickup.wheel++;
|
|
if(c.dic_has_woodframe === 'Y') optionPickup.wood++;
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
document.getElementById('summaryInstall').innerHTML =
|
|
`
|
|
<div class="container-chips inline-chips mt-1">
|
|
${buildTypeChips(install)}
|
|
</div>
|
|
<div class="container-chips inline-chips mt-1">
|
|
${buildOptionChips(optionInstall)}
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('summaryPickup').innerHTML =
|
|
`
|
|
<div class="container-chips inline-chips mt-1">
|
|
${buildTypeChips(pickup)}
|
|
</div>
|
|
<div class="container-chips inline-chips mt-1">
|
|
${buildOptionChips(optionPickup)}
|
|
</div>
|
|
`;
|
|
|
|
}
|
|
|
|
function getContainerIcon(type){
|
|
if(type === 'D'){
|
|
return 'bi-database-fill';
|
|
}
|
|
|
|
if(type && type.endsWith('B')){
|
|
return 'bi-archive-fill';
|
|
}
|
|
return 'bi-box';
|
|
}
|
|
|
|
function buildTypeChips(typeCounts){
|
|
|
|
let html = '';
|
|
|
|
Object.keys(typeCounts).sort().forEach(key => {
|
|
|
|
const [type, used, owner] = key.split('|');
|
|
|
|
const isUsed = (used === 'Y');
|
|
const isOwner = (owner === 'O');
|
|
|
|
const icon = getContainerIcon(type);
|
|
|
|
let chipClass = 'chip chip-company';
|
|
if(isOwner){
|
|
chipClass = 'chip chip-restaurant';
|
|
}
|
|
|
|
html += `
|
|
<span class="${chipClass} me-2 mb-2" >
|
|
<i class="bi ${icon}"></i>
|
|
${isOwner ? '<span class="ms-1 fw-bold">O</span>' : ''}
|
|
${isUsed ? '<span class="ms-1">Used</span>' : ''}
|
|
<span class="ms-1">${type}</span>
|
|
<strong class="ms-2">x ${typeCounts[key]}</strong>
|
|
</span>
|
|
`;
|
|
});
|
|
|
|
return html || `<span class="text-muted small">-</span>`;
|
|
}
|
|
|
|
function buildOptionChips(optionCounts){
|
|
|
|
let html = '';
|
|
|
|
if(optionCounts.lock > 0){
|
|
html += `
|
|
<span class="chip me-2 mb-2">
|
|
<i class="bi bi-lock-fill"></i>
|
|
<strong class="ms-1">x ${optionCounts.lock}</strong>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
if(optionCounts.wheel > 0){
|
|
html += `
|
|
<span class="chip me-2 mb-2">
|
|
<i class="bi bi-gear-fill"></i>
|
|
<strong class="ms-1">x ${optionCounts.wheel}</strong>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
if(optionCounts.wood > 0){
|
|
html += `
|
|
<span class="chip me-2 mb-2">
|
|
<i class="bi bi-bounding-box-circles"></i>
|
|
<strong class="ms-1">x ${optionCounts.wood}</strong>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
return html; // 옵션 없으면 그냥 빈칸 (줄 자체가 없어짐)
|
|
}
|
|
|
|
function reloadPage(){
|
|
|
|
const date = document.getElementById('installDate').value;
|
|
const driver = document.getElementById('driverSelect').value;
|
|
|
|
const url = new URL(window.location.href);
|
|
|
|
url.searchParams.set('view', 'install_order');
|
|
url.searchParams.set('date', date);
|
|
url.searchParams.set('driver_uid', driver);
|
|
|
|
window.location.href = url.toString();
|
|
}
|
|
|
|
document.getElementById('driverSelect').onchange = reloadPage;
|
|
|
|
/* ===============================
|
|
MAP
|
|
=================================*/
|
|
let installMap;
|
|
let installMarkers = [];
|
|
let installInfo;
|
|
|
|
async function initMap(){
|
|
|
|
const { Map, InfoWindow } = await google.maps.importLibrary("maps");
|
|
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
|
|
|
|
installInfo = new InfoWindow();
|
|
|
|
installMap = new Map(
|
|
document.getElementById("installMap"),
|
|
{
|
|
center: { lat: 43.7, lng: -79.4 },
|
|
zoom: 11,
|
|
mapId: "4504f8b37365c3d0", // waitlist와 동일
|
|
disableDefaultUI: true // Map/Satellite 제거
|
|
}
|
|
);
|
|
|
|
renderMap();
|
|
}
|
|
|
|
const driverPalette = [
|
|
"#4275c1",
|
|
"#91744fff",
|
|
"#198754",
|
|
"#dc3545",
|
|
"#e97517ff",
|
|
"#7443cfff",
|
|
"#20c997",
|
|
"#0dcaf0"
|
|
];
|
|
|
|
function getDriverColor(driverUid) {
|
|
return driverPalette[driverUid % driverPalette.length];
|
|
}
|
|
|
|
function renderMap(){
|
|
|
|
installMarkers.forEach(m => m.map = null);
|
|
installMarkers = [];
|
|
|
|
if(!installData || !installData.length) return;
|
|
|
|
const bounds = new google.maps.LatLngBounds();
|
|
|
|
installData.forEach(row => {
|
|
|
|
if(!row.di_lat || !row.di_lng) return;
|
|
|
|
const pos = {
|
|
lat: parseFloat(row.di_lat),
|
|
lng: parseFloat(row.di_lng)
|
|
};
|
|
|
|
bounds.extend(pos);
|
|
|
|
const baseColor = getDriverColor(row.di_driver_uid);
|
|
const driverColor = row.di_status === 'A' ? baseColor : "#7c7c7cff";
|
|
const marker = new google.maps.marker.AdvancedMarkerElement({
|
|
map: installMap,
|
|
position: pos,
|
|
content: createFlagMarker({
|
|
order: row.di_order_seq,
|
|
jobType: row.di_work_type,
|
|
color: driverColor
|
|
})
|
|
});
|
|
|
|
marker.__di_uid = row.di_uid;
|
|
|
|
marker.addListener('gmp-click', () => {
|
|
openWorkModal(row.di_uid);
|
|
});
|
|
|
|
installMarkers.push(marker);
|
|
});
|
|
|
|
installMap.fitBounds(bounds);
|
|
renderMapLegend(installData);
|
|
}
|
|
|
|
function renderMapLegend(rows){
|
|
const el = document.getElementById('installMapLegend');
|
|
if(!el) return;
|
|
|
|
const seen = new Map();
|
|
|
|
(rows || []).forEach(row => {
|
|
const driverUid = parseInt(row.di_driver_uid, 10);
|
|
if(!driverUid || seen.has(driverUid)) return;
|
|
|
|
seen.set(driverUid, {
|
|
uid: driverUid,
|
|
name: driverInitialMap[driverUid] || `Driver ${driverUid}`,
|
|
color: getDriverColor(driverUid)
|
|
});
|
|
});
|
|
|
|
const items = Array.from(seen.values()).sort((a, b) =>
|
|
a.name.localeCompare(b.name)
|
|
);
|
|
|
|
if(items.length === 0){
|
|
el.innerHTML = `
|
|
<div class="install-map-legend-title">Driver</div>
|
|
<div class="text-muted small">No routes</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = `
|
|
<div class="install-map-legend-title">Driver</div>
|
|
${items.map(item => `
|
|
<div class="install-map-legend-item">
|
|
<span class="install-map-legend-swatch" style="background:${item.color}"></span>
|
|
<span>${escapeHtml(item.name)}</span>
|
|
</div>
|
|
`).join('')}
|
|
`;
|
|
}
|
|
|
|
function createFlagMarker({ order, jobType, color }) {
|
|
|
|
const bg = color;
|
|
const border = color;
|
|
const textColor = "#ffffff";
|
|
const iconClass = getJobBiIcon(jobType);
|
|
|
|
const wrap = document.createElement('div');
|
|
wrap.className = 'flag-marker';
|
|
|
|
wrap.style.background = bg;
|
|
wrap.style.border = `2px solid ${border}`;
|
|
wrap.style.color = textColor;
|
|
wrap.style.borderRadius = "8px";
|
|
wrap.style.padding = "4px 6px";
|
|
wrap.style.fontWeight = "600";
|
|
wrap.style.fontSize = "12px";
|
|
wrap.style.setProperty('--flag-bg', bg);
|
|
|
|
wrap.innerHTML = `
|
|
<div class="flag-body d-flex align-items-center gap-1">
|
|
<span class="flag-order">${order}</span>
|
|
${iconClass ? `<i class="bi ${iconClass}"></i>` : ``}
|
|
</div>
|
|
<div class="flag-tail"></div>
|
|
`;
|
|
|
|
return wrap;
|
|
}
|
|
|
|
function getJobBiIcon(type){
|
|
switch(type){
|
|
case 'INSTALL': return 'bi-plus-lg';
|
|
case 'PICKUP': return 'bi-dash-lg';
|
|
case 'EXCHANGE': return 'bi-arrow-left-right';
|
|
case 'RELOCATE': return 'bi-arrows-move';
|
|
case 'CLEAN': return 'bi-brush';
|
|
default: return '';
|
|
}
|
|
}
|
|
|
|
/* ===============================
|
|
Link
|
|
=================================*/
|
|
function openRoute(driverUid){
|
|
|
|
// 하드코딩
|
|
const OFFICE = "4490 Chesswood Dr, North York, ON M3J 2B9";
|
|
|
|
const rows = installData
|
|
.filter(r => r.di_driver_uid == driverUid)
|
|
.sort((a,b)=>a.di_order_seq - b.di_order_seq);
|
|
|
|
if(rows.length === 0){
|
|
alert('No active orders.');
|
|
return;
|
|
}
|
|
|
|
// 시작/종료는 사무실
|
|
const origin = OFFICE;
|
|
const destination = OFFICE;
|
|
|
|
const waypoints = rows.map(r =>
|
|
`${r.di_address}, ${r.di_city}`
|
|
).join('|');
|
|
|
|
let url = `https://www.google.com/maps/dir/?api=1`;
|
|
url += `&origin=${encodeURIComponent(origin)}`;
|
|
url += `&destination=${encodeURIComponent(destination)}`;
|
|
|
|
if(waypoints){
|
|
url += `&waypoints=${encodeURIComponent(waypoints)}`;
|
|
}
|
|
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
/* ===============================
|
|
UTIL
|
|
=================================*/
|
|
function escapeHtml(str){
|
|
return String(str ?? '')
|
|
.replaceAll('&','&')
|
|
.replaceAll('<','<')
|
|
.replaceAll('>','>')
|
|
.replaceAll('"','"')
|
|
.replaceAll("'","'");
|
|
}
|
|
|
|
function formatPhone(phone){
|
|
if(!phone) return '';
|
|
const digits = phone.replace(/\D/g,'');
|
|
if(digits.length === 10){
|
|
return `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`;
|
|
}
|
|
return phone;
|
|
}
|
|
|
|
function showPopupMessage(message, type = 'success', duration = 1200) {
|
|
|
|
const body = document.querySelector('#myModalPopup .myModalPopup-body');
|
|
if (!body) return;
|
|
|
|
body.innerHTML = message;
|
|
|
|
if (type === 'success') {
|
|
body.style.backgroundColor = '#2A9B56'; // green
|
|
} else {
|
|
body.style.backgroundColor = '#FF8205'; // orange
|
|
}
|
|
|
|
$('#myModalPopup').modal('show');
|
|
|
|
setTimeout(function () {
|
|
$('#myModalPopup').modal('hide');
|
|
}, duration);
|
|
}
|
|
|
|
/* ===============================
|
|
TOGGLE
|
|
=================================*/
|
|
document.querySelectorAll('input[name="view_mode"]').forEach(radio => {
|
|
radio.addEventListener('change', function () {
|
|
document.getElementById('mapSection')
|
|
.classList.toggle('d-none', this.value !== 'map');
|
|
|
|
document.getElementById('listSection')
|
|
.classList.toggle('d-none', this.value !== 'list');
|
|
|
|
if (this.value === 'map') {
|
|
if (!installMap) {
|
|
initMap();
|
|
} else {
|
|
setTimeout(() => google.maps.event.trigger(installMap, 'resize'), 50);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
// map.php에서 쓰는 동일한 로더
|
|
(g=>{
|
|
var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;
|
|
b=b[c]||(b[c]={});
|
|
var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{
|
|
await (a=m.createElement("script"));
|
|
e.set("libraries",[...r]+"");
|
|
for(k in g) e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);
|
|
e.set("callback",c+".maps."+q);
|
|
a.src=`https://maps.${c}apis.com/maps/api/js?`+e;
|
|
d[q]=f;
|
|
a.onerror=()=>h=n(Error(p+" could not load."));
|
|
a.nonce=m.querySelector("script[nonce]")?.nonce||"";
|
|
m.head.append(a);
|
|
}));
|
|
d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))
|
|
})({key:"AIzaSyDg9u03mGrBhyOisp7VGc27CTPI9QXp8sY", v:"weekly"});
|
|
</script>
|