2201 lines
72 KiB
PHP
2201 lines
72 KiB
PHP
<?php
|
||
$func->checkLevelModal(5);
|
||
// ============================
|
||
// Search filters (LEFT ONLY)
|
||
// ============================
|
||
$where = [];
|
||
if (!empty($kw_customer)) $where[] = "(iw_customer_name LIKE '%{$kw_customer}%' OR iw_address LIKE '%{$kw_customer}%' OR iw_accountno LIKE '%{$kw_customer}%' OR iw_city LIKE '%{$kw_customer}%')";
|
||
if (!empty($kw_work)) $where[] = "(iw_work_type = '{$kw_work}')";
|
||
if (!empty($_GET['kw_two_person'])) $where[] = "(iw_required_person_cnt IS NOT NULL AND iw_required_person_cnt > 0)";
|
||
if (!empty($_GET['kw_draft'])) $where[] = "(iw_status = 'DRAFT')";
|
||
if (!empty($where)) {
|
||
$add_where = ' AND ' . implode(' AND ', $where);
|
||
}
|
||
function db_escape($jdb, $str) {
|
||
return mysqli_real_escape_string($jdb->DBConn, $str);
|
||
}
|
||
|
||
// ============================
|
||
// Total count
|
||
// ============================
|
||
$sql_cnt = "
|
||
SELECT COUNT(*)
|
||
FROM tbl_install_waitlist
|
||
WHERE iw_status in ('WAITING','DRAFT')
|
||
$add_where
|
||
";
|
||
$total_count = $jdb->rQuery($sql_cnt, "Install wait list count error");
|
||
|
||
// ============================
|
||
// 1) Load waitlist
|
||
// ============================
|
||
$sql = "
|
||
SELECT iw.*, CONCAT(m.m_firstname, ' ', m.m_lastname) as iw_created_member
|
||
FROM tbl_install_waitlist iw
|
||
LEFT OUTER JOIN tbl_member m
|
||
ON iw.iw_created_by = m.m_uid
|
||
WHERE iw_status in ('WAITING','DRAFT')
|
||
$add_where
|
||
ORDER BY iw_requested_due_date ASC, iw_request_at ASC
|
||
";
|
||
$rs = $jdb->nQuery($sql, "Load waitlist error");
|
||
|
||
$rows = [];
|
||
$iw_uids = [];
|
||
$customer_uids = [];
|
||
|
||
while ($r = mysqli_fetch_assoc($rs)) {
|
||
$rows[] = $r;
|
||
$iw_uids[] = (int)$r['iw_uid'];
|
||
if (!empty($r['iw_customer_uid'])) {
|
||
$customer_uids[] = (int)$r['iw_customer_uid'];
|
||
}
|
||
}
|
||
$iw_uids = array_values(array_unique($iw_uids));
|
||
$customer_uids = array_values(array_unique($customer_uids));
|
||
|
||
/* ============================
|
||
* 2) Customer containers (cc_status='A')
|
||
* ============================ */
|
||
$customerContainerMap = []; // [customer_uid => [rows...]]
|
||
if ($customer_uids) {
|
||
$in = implode(',', $customer_uids);
|
||
$sql_cc = "
|
||
SELECT cc_customer_uid, cc_type, cc_owner_type, cc_is_used, cc_has_lock, cc_has_wheel, cc_has_woodframe
|
||
FROM tbl_customer_container
|
||
WHERE cc_status = 'A'
|
||
AND cc_customer_uid IN ($in)
|
||
ORDER BY cc_customer_uid, cc_type, cc_uid
|
||
";
|
||
|
||
$rc = $jdb->nQuery($sql_cc, "Load customer containers error");
|
||
|
||
while ($c = mysqli_fetch_assoc($rc)) {
|
||
$cu = (int)$c['cc_customer_uid'];
|
||
if (!isset($customerContainerMap[$cu])) $customerContainerMap[$cu] = [];
|
||
$customerContainerMap[$cu][] = $c;
|
||
}
|
||
}
|
||
|
||
/* ============================
|
||
* 3) Task containers (waitlist)
|
||
* ============================ */
|
||
$taskMap = []; // [iw_uid => rows]
|
||
if ($iw_uids) {
|
||
$in = implode(',', $iw_uids);
|
||
$sql_task = "
|
||
SELECT *
|
||
FROM tbl_install_waitlist_container
|
||
WHERE iwc_wait_uid IN ($in)
|
||
ORDER BY iwc_wait_uid, iwc_uid
|
||
";
|
||
$rt = $jdb->nQuery($sql_task, "Load task containers error");
|
||
|
||
while ($t = mysqli_fetch_assoc($rt)) {
|
||
$taskMap[$t['iwc_wait_uid']][] = $t;
|
||
}
|
||
}
|
||
|
||
/* ============================
|
||
* 4) Customer images (install_order)
|
||
* ============================ */
|
||
$imageMap = []; // [customer_uid => true]
|
||
if ($customer_uids) {
|
||
$in = implode(',', $customer_uids);
|
||
|
||
$sql_img = "
|
||
SELECT DISTINCT i_customeruid
|
||
FROM tbl_customer_image
|
||
WHERE i_status = 'A'
|
||
AND i_type = 'install_order'
|
||
AND i_customeruid IN ($in)
|
||
";
|
||
|
||
$ri = $jdb->nQuery($sql_img, "Load customer images error");
|
||
|
||
while ($img = mysqli_fetch_assoc($ri)) {
|
||
$imageMap[(int)$img['i_customeruid']] = true;
|
||
}
|
||
}
|
||
|
||
// ============================
|
||
// 5) customer driver
|
||
// ============================
|
||
$customerDriverMap = []; // [customer_uid => driver_uid]
|
||
if ($customer_uids) {
|
||
$in = implode(',', $customer_uids);
|
||
|
||
$sql_cd = "
|
||
SELECT
|
||
c.c_uid,
|
||
d.dr_initial
|
||
FROM tbl_customer c
|
||
LEFT JOIN tbl_member m
|
||
ON c.c_regionuid = m.m_regionuid
|
||
LEFT JOIN tbl_driver d
|
||
ON m.m_defaultdriver = d.dr_uid
|
||
WHERE c.c_uid IN ($in)
|
||
AND d.dr_type = 'UCO'
|
||
";
|
||
|
||
$rc = $jdb->nQuery($sql_cd, "Load customer driver error");
|
||
|
||
while ($c = mysqli_fetch_assoc($rc)) {
|
||
$cu = (int)$c['c_uid'];
|
||
$customerDriverMap[$cu] = $c['dr_initial'] ?? '';
|
||
}
|
||
}
|
||
|
||
// ============================
|
||
// 6) install drivers
|
||
// ============================
|
||
$drivers = [];
|
||
|
||
$sql_install = "
|
||
SELECT dr_uid, dr_initial
|
||
FROM tbl_driver
|
||
WHERE dr_status = 'A'
|
||
AND dr_type = 'INSTALL'
|
||
ORDER BY dr_initial
|
||
";
|
||
$ris = $jdb->nQuery($sql_install, "Load install drivers error");
|
||
|
||
while ($r = mysqli_fetch_assoc($ris)) {
|
||
$drivers[] = $r;
|
||
}
|
||
|
||
// ============================
|
||
// 7) driver initials (modal)
|
||
// ============================
|
||
$driverInitialMap = []; // [dr_uid => dr_initial]
|
||
$sql_driver = "
|
||
SELECT dr_uid, dr_initial
|
||
FROM tbl_driver
|
||
WHERE dr_status = 'A'
|
||
";
|
||
$res = $jdb->nQuery($sql_driver, "Load driver map error");
|
||
|
||
while ($r = mysqli_fetch_assoc($res)) {
|
||
$driverInitialMap[(int)$r['dr_uid']] = $r['dr_initial'];
|
||
}
|
||
|
||
// ============================
|
||
// 전화번호
|
||
// ============================
|
||
function formatPhone($phone) {
|
||
$digits = preg_replace('/\D+/', '', (string)$phone);
|
||
|
||
// 10 digits => (xxx) xxx-xxxx
|
||
if (strlen($digits) === 10) {
|
||
return sprintf(
|
||
'(%s) %s - %s',
|
||
substr($digits, 0, 3),
|
||
substr($digits, 3, 3),
|
||
substr($digits, 6, 4)
|
||
);
|
||
}
|
||
|
||
return $phone;
|
||
}
|
||
?>
|
||
|
||
<!-- drag and drop -->
|
||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
||
<script>(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>
|
||
<script>
|
||
document.querySelectorAll('input[name="view_mode"]').forEach(radio => {
|
||
radio.addEventListener('change', function () {
|
||
document.getElementById('view-map').classList.toggle(
|
||
'd-none', this.value !== 'map'
|
||
);
|
||
document.getElementById('view-list').classList.toggle(
|
||
'd-none', this.value !== 'list'
|
||
);
|
||
});
|
||
});
|
||
|
||
function searchWaitlist() {
|
||
const form = document.querySelector('.search-bar');
|
||
const params = new URLSearchParams(new FormData(form));
|
||
params.append('ajax', 'waitlist');
|
||
|
||
fetch('index_intranet.php?view=install_wait_list&' + params.toString())
|
||
.then(res => res.text())
|
||
.then(html => {
|
||
// 1) HTML 파싱
|
||
const parser = new DOMParser();
|
||
const doc = parser.parseFromString(html, 'text/html');
|
||
|
||
// 2) 새 문서에서 waitlistArea만 추출
|
||
const newWaitlist = doc.getElementById('waitlistArea');
|
||
|
||
if (!newWaitlist) {
|
||
throw new Error('waitlistArea not found in response');
|
||
}
|
||
|
||
// 3) 기존 waitlistArea 교체
|
||
document.getElementById('waitlistArea').innerHTML =newWaitlist.innerHTML;
|
||
|
||
// 4) total count 교체
|
||
const newTotal = doc.getElementById('waitlistTotalCount');
|
||
const oldTotal = document.getElementById('waitlistTotalCount');
|
||
|
||
if (newTotal && oldTotal) {
|
||
oldTotal.innerHTML = newTotal.innerHTML;
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error(err);
|
||
document.getElementById('waitlistArea').innerHTML = '<div class="text-danger text-center py-5">Failed to load.</div>';
|
||
});
|
||
}
|
||
|
||
function openCustomerImages(customerUid) {
|
||
|
||
const modalEl = document.getElementById('customerImageModal');
|
||
if (!modalEl) {
|
||
console.error('customerImageModal not found');
|
||
return;
|
||
}
|
||
|
||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||
|
||
const body = document.getElementById('customerImageModalBody');
|
||
if (body) {
|
||
body.innerHTML = '<div class="text-center text-muted py-5">Loading images...</div>';
|
||
}
|
||
|
||
modal.show();
|
||
|
||
const params = new URLSearchParams({
|
||
customer_uid: customerUid,
|
||
i_type: 'install_order'
|
||
});
|
||
|
||
fetch('/lib/customer_image.php?' + params.toString())
|
||
.then(res => res.text())
|
||
.then(html => {
|
||
if (body) body.innerHTML = html;
|
||
})
|
||
.catch(err => {
|
||
console.error(err);
|
||
if (body) {
|
||
body.innerHTML = '<div class="text-danger text-center py-5">Failed to load images.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
function openAddInstallWaitModal(options = {}) {
|
||
const modalEl = document.getElementById('addInstallWaitModal');
|
||
const modal = new bootstrap.Modal(modalEl);
|
||
|
||
const body = modalEl.querySelector('.modal-body');
|
||
body.innerHTML = `<div class="text-center text-muted py-5">Loading...</div>`;
|
||
|
||
fetch('/lib/add_install_wait_modal.php')
|
||
.then(res => {
|
||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||
return res.text();
|
||
})
|
||
.then(html => {
|
||
body.innerHTML = html;
|
||
|
||
// Step1 기본 초기화 (기존 add 모드 로직)
|
||
initAddInstallWaitModal();
|
||
|
||
// ===== Edit Mode =====
|
||
if (options.mode === 'edit' && options.iw_uid) {
|
||
InstallWait.mode = 'edit';
|
||
InstallWait.iw_uid = options.iw_uid;
|
||
|
||
// Step3로 이동은 DOM 로드 이후에
|
||
goInstallWaitStep(3);
|
||
|
||
// Step3 이벤트 바인딩/초기화
|
||
initInstallWaitStep3();
|
||
|
||
// 편집 데이터 로드
|
||
loadInstallWaitForEdit(options.iw_uid);
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error('Failed to load modal body:', err);
|
||
body.innerHTML = `
|
||
<div class="text-danger text-center py-5">
|
||
Failed to load. (${err})
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
modal.show();
|
||
}
|
||
|
||
function openRoute() {
|
||
|
||
const OFFICE = "4490 Chesswood Dr, North York, ON M3J 2B9";
|
||
|
||
const rows = [];
|
||
|
||
document.querySelectorAll('#iwListBody tr').forEach(tr => {
|
||
|
||
const address = tr.dataset.address || '';
|
||
const city = tr.dataset.city || '';
|
||
const postal = tr.dataset.postal || '';
|
||
|
||
if (!address) return;
|
||
|
||
rows.push({
|
||
address: [address, city, postal].filter(Boolean).join(', '),
|
||
order: parseInt(tr.dataset.orderSeq || 0)
|
||
});
|
||
});
|
||
|
||
if (!rows.length) {
|
||
showPopupMessage('No route data.', 'error', 1500);
|
||
return;
|
||
}
|
||
|
||
// 순서 정렬 (중요)
|
||
rows.sort((a, b) => a.order - b.order);
|
||
|
||
const waypoints = rows.map(r => r.address).join('|');
|
||
|
||
let url = `https://www.google.com/maps/dir/?api=1`;
|
||
url += `&origin=${encodeURIComponent(OFFICE)}`;
|
||
url += `&destination=${encodeURIComponent(OFFICE)}`;
|
||
url += `&waypoints=${encodeURIComponent(waypoints)}`;
|
||
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
function loadInstallWaitForEdit(iw_uid) {
|
||
// 1) install_wait 메타 로드
|
||
api('get_install_wait_detail', { iw_uid }, data => {
|
||
if (!data) return;
|
||
// customer / job 세팅
|
||
InstallWait.customer = {
|
||
uid: data.iw_customer_uid,
|
||
account: data.iw_accountno,
|
||
name: data.iw_customer_name,
|
||
address: data.iw_address,
|
||
city: data.iw_city,
|
||
postal: data.iw_postal,
|
||
phone: data.iw_phone
|
||
};
|
||
|
||
InstallWait.job = {
|
||
type: data.iw_work_type
|
||
};
|
||
|
||
// Step3 의 기본 필드 채우기
|
||
iwRequestDate.value = data.iw_request_at?.substring(0,10) || '';
|
||
iwScheduleDate.value = data.iw_requested_due_date || '';
|
||
//iwRequestBy.value = data.iw_request_by || '';
|
||
iwRequestNote.value = data.iw_request_note || '';
|
||
iwCsNote.value = data.iw_cs_note || '';
|
||
iwWorkNote.value = data.iw_work_note || '';
|
||
fillRequestBySelect(data.iw_request_by || 'Customer');
|
||
|
||
// Two-person
|
||
if (data.iw_required_person_cnt == 2) {
|
||
btnTwoPerson.classList.add('iw-toggle-on');
|
||
iwTwoPersonReason.disabled = false;
|
||
iwTwoPersonReason.value = data.iw_multi_person_reason || '';
|
||
}
|
||
|
||
// Recurring
|
||
if (data.iw_scheduled == 1) {
|
||
btnRecurring.classList.add('iw-toggle-on');
|
||
iwRecurringDays.disabled = false;
|
||
iwRecurringDays.value = data.iw_scheduled_cycle || '';
|
||
}
|
||
|
||
// job label 갱신
|
||
document.getElementById('iwStep3Title').innerText =
|
||
`Step 3 of 3 - ${data.iw_work_type} Detail`;
|
||
|
||
// action box 다시 그림
|
||
setStep3Title();
|
||
renderStep3ActionBoxes();
|
||
loadCurrentContainersIfNeeded(() => {
|
||
currentReady = true;
|
||
});
|
||
checkCustomerHasImages(InstallWait.customer?.uid);
|
||
});
|
||
|
||
// 2) 기존 PICKUP / INSTALL 로드
|
||
api('get_install_wait_containers', { iw_uid }, rows => {
|
||
const pickupWrap = document.querySelector('.container-chips[data-action="PICKUP"]');
|
||
const installWrap = document.querySelector('.container-chips[data-action="INSTALL"]');
|
||
|
||
// edit 모드로 열릴 때 잔상 제거
|
||
if (pickupWrap) pickupWrap.innerHTML = '';
|
||
if (installWrap) installWrap.innerHTML = '';
|
||
|
||
const pickupRows = [];
|
||
rows.forEach(r => {
|
||
if (r.iwc_action === 'PICKUP') {
|
||
pickupRows.push(r); // 이동 판단용
|
||
}
|
||
if (r.iwc_action === 'INSTALL' && installWrap) {
|
||
installWrap.insertAdjacentHTML('beforeend', renderInstallChipFromDB(r));
|
||
}
|
||
});
|
||
|
||
// CURRENT 로딩이 끝난 뒤에 이동 로직 실행
|
||
loadCurrentContainersIfNeeded(() => {
|
||
applyPickupFromDBToCurrent(pickupRows);
|
||
});
|
||
|
||
// 이벤트 바인딩
|
||
bindPickupContainerChipEvents();
|
||
bindInstallChipEvents();
|
||
});
|
||
|
||
}
|
||
|
||
function openInstallWaitEditModal(iw_uid) {
|
||
openAddInstallWaitModal({
|
||
mode: 'edit',
|
||
iw_uid: iw_uid
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<main id="main" class="main" style="padding-bottom:25px;">
|
||
<div class="breadcrumbs">
|
||
<div class="container">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<h2>INSTALL WAIT LIST</h2>
|
||
<ol>
|
||
<li><a href="index_intranet.php">HOME</a></li>
|
||
<li>PLANNING</li>
|
||
<li>INSTALL WAIT LIST</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="installWaitContext"
|
||
data-member-uid="<?=$_SESSION['ss_UID']?>"
|
||
data-member-id="<?=$_SESSION['ss_ID']?>">
|
||
</div>
|
||
|
||
<section class="page">
|
||
<div class="container-fluid">
|
||
|
||
<div class="row">
|
||
<!-- ============================ -->
|
||
<!-- LEFT : WAITING LIST -->
|
||
<!-- ============================ -->
|
||
<div class="col-lg-6">
|
||
|
||
<div class="card planning-card h-100">
|
||
|
||
<!-- Search -->
|
||
<div class="card-body border-bottom pb-0">
|
||
<form method="get" class="row g-2 align-items-stretch search-bar position-relative pb-4" id="waitlistSearchForm">
|
||
<!-- Work -->
|
||
<div class="col-md-2">
|
||
<select name="kw_work" class="form-select h-100" onchange="searchWaitlist()">
|
||
<option value="">All Work</option>
|
||
<?php foreach (['INSTALL','PICKUP','EXCHANGE','RELOCATE','CLEAN'] as $w): ?>
|
||
<option value="<?=$w?>" <?=($kw_work==$w?'selected':'')?>><?=$w?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 2+ -->
|
||
<div class="col-md-1 d-flex">
|
||
<button type="button" id="filterTwoPerson" class="btn btn-outline-dark w-100 h-100" data-active="0" title="multi-person required">
|
||
<i class="bi bi-people-fill"></i> 2+
|
||
</button>
|
||
<input type="hidden" name="kw_two_person" id="kw_two_person">
|
||
</div>
|
||
|
||
<div class="col-md-1 d-flex">
|
||
<button type="button" id="filterDraft" class="btn btn-outline-dark w-100 h-100" data-active="0" title="Draft Filter">
|
||
<i class="bi bi-clipboard-check"></i>
|
||
</button>
|
||
<input type="hidden" name="kw_draft" id="kw_draft">
|
||
</div>
|
||
|
||
<!-- Search -->
|
||
<div class="col-md-5">
|
||
<div class="input-group h-100 search-container">
|
||
<input type="text" name="kw_customer" id="kwCustomer" class="form-control" placeholder="City / Customer / Account" value="<?=htmlspecialchars($kw_customer)?>">
|
||
<button type="submit" class="btn btn-outline-secondary">
|
||
<i class="bi bi-search"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add -->
|
||
<div class="col-md-3 d-flex justify-content-end">
|
||
<button type="button" class="btn-add" onclick="openAddInstallWaitModal()">
|
||
Add Wait List
|
||
</button>
|
||
</div>
|
||
|
||
<div id="waitlistTotalCount"
|
||
class="position-absolute bottom-0 end-0 text-muted small pe-2 pb-1">
|
||
Total: <?=$total_count?>
|
||
</div>
|
||
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Scroll Area -->
|
||
<div class="card-body p-0 waitlist-scroll" id="waitlistArea">
|
||
<?php if (!$rows): ?>
|
||
<div class="text-center text-muted py-5">No waiting items.</div>
|
||
<?php endif; ?>
|
||
|
||
<?php foreach ($rows as $row): ?>
|
||
|
||
<?php
|
||
/* Work badge */
|
||
$work = $row['iw_work_type'];
|
||
$workClass = "bg-secondary text-white";
|
||
if ($work=="INSTALL") $workClass="bg-success text-white";
|
||
if ($work=="PICKUP") $workClass="bg-danger text-white";
|
||
if ($work=="EXCHANGE") $workClass="bg-warning text-dark";
|
||
if ($work=="RELOCATE") $workClass="bg-info text-dark";
|
||
if ($work=="CLEAN") $workClass="bg-primary text-white";
|
||
|
||
/* Due type */
|
||
$dueTypeClass = "";
|
||
$dueTypeLabel = "";
|
||
if ($row['iw_request_due_type']=="EXACT") {
|
||
$dueTypeClass="bg-danger text-white"; $dueTypeLabel="Exact";
|
||
} elseif ($row['iw_request_due_type']=="RANGE") {
|
||
$dueTypeClass="bg-warning text-dark"; $dueTypeLabel="Range";
|
||
} elseif ($row['iw_request_due_type']=="FLEXIBLE") {
|
||
$dueTypeClass="bg-secondary text-white"; $dueTypeLabel="Flexible";
|
||
}
|
||
|
||
$cu = (int)$row['iw_customer_uid'];
|
||
$customerContainers = $customerContainerMap[$cu] ?? [];
|
||
$tasks = $taskMap[$row['iw_uid']] ?? [];
|
||
$pickup_d = $pickup_b = $install_d = $install_b = 0;
|
||
$driverInitial = $customerDriverMap[$cu] ?? '';
|
||
|
||
foreach ($tasks as $t) {
|
||
$action = strtoupper(trim($t['iwc_action'] ?? ''));
|
||
$type = strtoupper(trim($t['iwc_type'] ?? ''));
|
||
|
||
if ($action === 'PICKUP') {
|
||
if ($type === 'D') $pickup_d++;
|
||
else $pickup_b++;
|
||
}
|
||
elseif ($action === 'INSTALL') {
|
||
if ($type === 'D') $install_d++;
|
||
else $install_b++;
|
||
}
|
||
}
|
||
?>
|
||
|
||
<div class="wait-card"
|
||
data-iw="<?= (int)$row['iw_uid'] ?>"
|
||
data-status="<?= htmlspecialchars($row['iw_status']) ?>"
|
||
data-accountno="<?= htmlspecialchars($row['iw_accountno']) ?>"
|
||
data-name="<?= htmlspecialchars($row['iw_customer_name']) ?>"
|
||
data-job-type="<?= htmlspecialchars($row['iw_work_type']) ?>"
|
||
data-driver="<?= htmlspecialchars($driverInitial) ?>"
|
||
data-address="<?= htmlspecialchars($row['iw_address']) ?>"
|
||
data-city="<?= htmlspecialchars($row['iw_city']) ?>"
|
||
data-lat="<?= htmlspecialchars($row['iw_lat']) ?>"
|
||
data-lng="<?= htmlspecialchars($row['iw_lng']) ?>"
|
||
data-pickup-d="<?= $pickup_d ?>"
|
||
data-pickup-b="<?= $pickup_b ?>"
|
||
data-install-d="<?= $install_d ?>"
|
||
data-install-b="<?= $install_b ?>">
|
||
|
||
<!-- Top -->
|
||
<div class="d-flex align-items-center mb-1">
|
||
|
||
<!-- LEFT -->
|
||
<div class="d-flex gap-2 flex-wrap">
|
||
<span class="badge <?=$workClass?>"><?=$work?></span>
|
||
<small class="text-muted">created by <?=$row['iw_created_member']?></small>
|
||
|
||
<?php if ($row['iw_status'] === 'DRAFT'): ?>
|
||
<span class="badge bg-dark text-white">
|
||
<i class="bi bi-clipboard-check"></i> Draft
|
||
</span>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!empty($row['iw_required_person_cnt']) && $row['iw_required_person_cnt'] > 1): ?>
|
||
<span class="badge bg-dark text-white">
|
||
<i class="bi bi-people-fill"></i>
|
||
x<?=$row['iw_required_person_cnt']?>
|
||
</span>
|
||
<?php if (!empty($row['iw_multi_person_reason'])): ?>
|
||
<small class="text-muted">
|
||
<?=$row['iw_multi_person_reason']?>
|
||
</small>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($row['iw_scheduled']): ?>
|
||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||
<span class="badge bg-dark text-white">
|
||
<i class="bi bi-arrow-repeat"></i>
|
||
Every <?=$row['iw_scheduled_cycle']?> days
|
||
</span>
|
||
|
||
<?php if ($row['iw_last_exchange_date']): ?>
|
||
<small class="text-muted">
|
||
· Last <?= $row['iw_last_exchange_date'] ?>
|
||
</small>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- RIGHT -->
|
||
<div class="ms-auto d-flex align-items-center gap-2">
|
||
<small class="text-muted me-2">
|
||
Due: <?=$row['iw_requested_due_date'] ?: '-';?>
|
||
</small>
|
||
|
||
<a href="#" class="btn btn-sm btn-outline-secondary btn-edit-install-wait" data-iw-uid="<?=$row['iw_uid']?>" title="Edit">
|
||
<i class="bi bi-pencil"></i>
|
||
</a>
|
||
|
||
<a href="#" class="btn btn-sm btn-outline-secondary btn-delete-install-wait" data-iw-uid="<?=$row['iw_uid']?>" title="Delete">
|
||
<i class="bi bi-trash"></i>
|
||
</a>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Customer + Containers + Image button -->
|
||
<div class="d-flex justify-content-between align-items-start gap-2">
|
||
|
||
<!-- LEFT : Customer + Containers -->
|
||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||
|
||
<!-- Account No -->
|
||
<span class="badge bg-light text-dark border">
|
||
<?=$row['iw_accountno']?>
|
||
</span>
|
||
|
||
<!-- Customer Name -->
|
||
<?php
|
||
$cu = (int)$row['iw_customer_uid'];
|
||
$driverInitial = $customerDriverMap[$cu] ?? '';
|
||
?>
|
||
|
||
<span class="fw-bold">
|
||
<?=$row['iw_customer_name']?>
|
||
|
||
<?php if ($driverInitial): ?>
|
||
<span class="badge bg-light text-dark border ms-1" title="Assigned driver">
|
||
<?=$driverInitial?>
|
||
</span>
|
||
<?php endif; ?>
|
||
</span>
|
||
|
||
<!-- Containers -->
|
||
<?php
|
||
include $_SERVER['DOCUMENT_ROOT'].'/lib/customer_container_chips.php';
|
||
?>
|
||
|
||
</div>
|
||
|
||
<!-- RIGHT : Image button -->
|
||
<?php if (!empty($imageMap[$row['iw_customer_uid']])): ?>
|
||
<div class="ms-2">
|
||
<button type="button" class="btn btn-sm btn-outline-primary btn-image" title="View photos" onclick="openCustomerImages(<?=$row['iw_customer_uid']?>)">
|
||
<i class="bi bi-image"></i>
|
||
</button>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Address / Phone -->
|
||
<div class="text-muted small">
|
||
<?=$row['iw_address']?>, <?=$row['iw_city']?>, <?=$row['iw_postal']?> ·
|
||
<?=formatPhone($row['iw_phone'])?>
|
||
</div>
|
||
|
||
<!-- Due type -->
|
||
<?php if ($dueTypeLabel): ?>
|
||
<div class="mt-1">
|
||
<span class="badge <?=$dueTypeClass?>"><?=$dueTypeLabel?></span>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Notes -->
|
||
<?php
|
||
$reqByRaw = $row['iw_request_by'];
|
||
$reqByDisplay = '';
|
||
|
||
// Customer면 그대로
|
||
if ($reqByRaw === 'Customer') {
|
||
$reqByDisplay = 'Customer';
|
||
}
|
||
// 숫자(uid)면 m_initial로 변환
|
||
elseif (is_numeric($reqByRaw)) {
|
||
$uid = (int)$reqByRaw;
|
||
if (isset($driverInitialMap[$uid])) {
|
||
$reqByDisplay = $driverInitialMap[$uid];
|
||
} else {
|
||
$reqByDisplay = 'Customer'; // fallback (선택)
|
||
}
|
||
}
|
||
// 그 외 문자열이면 그대로 표시 (예외 대비)
|
||
else {
|
||
$reqByDisplay = htmlspecialchars($reqByRaw);
|
||
}
|
||
?>
|
||
<div class="note-line mt-2">
|
||
<span class="note-label">Request</span>
|
||
<span class="note-text">
|
||
(by <?=$reqByDisplay?> <?=substr($row['iw_request_at'],0,10)?>)
|
||
<?=htmlspecialchars($row['iw_request_note'])?>
|
||
</span>
|
||
</div>
|
||
|
||
<?php if ($row['iw_cs_note']): ?>
|
||
<div class="note-line">
|
||
<span class="note-label">CS</span>
|
||
<span class="note-text"><?=htmlspecialchars($row['iw_cs_note'])?></span>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($row['iw_work_note']): ?>
|
||
<div class="note-line">
|
||
<span class="note-label">Work</span>
|
||
<span class="note-text"><?=htmlspecialchars($row['iw_work_note'])?></span>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Task containers -->
|
||
<?php if ($tasks): ?>
|
||
<?php
|
||
$taskByAction = [];
|
||
foreach ($tasks as $t) {
|
||
$taskByAction[$t['iwc_action']][] = $t;
|
||
}
|
||
$actionOrder = ['PICKUP', 'INSTALL'];
|
||
?>
|
||
|
||
<!-- TASK ACTION WRAPPER -->
|
||
<div class="task-actions mt-2">
|
||
|
||
<?php foreach ($actionOrder as $action): ?>
|
||
<?php if (empty($taskByAction[$action])) continue; ?>
|
||
|
||
<?php
|
||
$actionClass = 'bg-secondary text-white';
|
||
if ($action === 'PICKUP') $actionClass = 'bg-danger text-white';
|
||
if ($action === 'INSTALL') $actionClass = 'bg-success text-white';
|
||
?>
|
||
|
||
<!-- ACTION BOX -->
|
||
<div class="task-action-box">
|
||
|
||
<div class="task-action-title <?=$action === 'PICKUP' ? 'pickup' : 'install'?>">
|
||
<?php if ($action === 'PICKUP'): ?>
|
||
<i class="bi bi-dash-circle"></i> Pickup
|
||
<?php elseif ($action === 'INSTALL'): ?>
|
||
<i class="bi bi-plus-circle"></i> Install
|
||
<?php else: ?>
|
||
<?=$action?>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="container-chips inline-chips mt-1">
|
||
<?php foreach ($taskByAction[$action] as $t): ?>
|
||
<?php
|
||
$usedLabel = ($t['iwc_is_used'] === 'Y') ? 'Used ' : '';
|
||
|
||
$type = strtoupper($t['iwc_type']);
|
||
if (strpos($type,'D') !== false) {
|
||
$icon = 'bi-database-fill';
|
||
} elseif (strpos($type,'B') !== false && strpos($type,'BUCKET') === false) {
|
||
$icon = 'bi-archive-fill';
|
||
} else {
|
||
$icon = 'bi-box2';
|
||
}
|
||
|
||
$optIcons = '';
|
||
if ($t['iwc_has_lock'] === 'Y') $optIcons .= ' <i class="bi bi-lock-fill"></i>';
|
||
if ($t['iwc_has_wheel'] === 'Y') $optIcons .= ' <i class="bi bi-gear-fill"></i>';
|
||
if ($t['iwc_has_woodframe'] === 'Y') $optIcons .= ' <i class="bi bi-bounding-box-circles"></i>';
|
||
?>
|
||
|
||
<span class="chip chip-task">
|
||
<i class="bi <?=$icon?>"></i>
|
||
<?=$usedLabel?><?=$t['iwc_type']?>
|
||
<?=$optIcons?>
|
||
</span>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============================ -->
|
||
<!-- RIGHT : ASSIGN / PLANNING -->
|
||
<!-- ============================ -->
|
||
<div class="col-lg-6">
|
||
|
||
<div class="card planning-card border-secondary h-100">
|
||
<!-- HEADER : Date + Driver -->
|
||
<div class="card-header bg-light">
|
||
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<!-- Date Navigator -->
|
||
<div class="d-flex align-items-center gap-2">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary btn-date-prev">
|
||
<i class="bi bi-chevron-left"></i>
|
||
</button>
|
||
|
||
<strong>
|
||
<input type="text" id="iwMapDate" class="form-control form-control-sm date-picker-form" style="max-height:20px;" autocomplete="off" placeholder="<?=date('Y-m-d')?>" readonly>
|
||
</strong>
|
||
|
||
<button type="button" class="btn btn-sm btn-outline-secondary btn-date-next">
|
||
<i class="bi bi-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Center : Toggle -->
|
||
<div class="d-flex align-items-center ms-auto gap-2">
|
||
<div class="view-toggle iw-ui-control">
|
||
<label>
|
||
<input type="radio" name="view_mode" value="map" checked>
|
||
<span><i class="bi bi-map"></i></span>
|
||
</label>
|
||
<label>
|
||
<input type="radio" name="view_mode" value="list">
|
||
<span><i class="bi bi-list"></i></span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Driver Selector -->
|
||
<div class="radio-segment iw-ui-control" style="width:120px;">
|
||
<?php foreach ($drivers as $i => $d): ?>
|
||
<label>
|
||
<input type="radio" name="driver"
|
||
value="<?=$d['dr_uid']?>"
|
||
<?= $i === 0 ? 'checked' : '' ?>>
|
||
<span><?=htmlspecialchars($d['dr_initial'])?></span>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Button -->
|
||
<div class="d-flex iw-ui-control align-items-center ms-auto gap-2">
|
||
<button id="btnRouteSchedule" class="btn btn-outline-secondary">
|
||
<i class="bi bi-geo-alt"></i> Route
|
||
</button>
|
||
|
||
<button id="btnModifySchedule" class="btn btn-outline-primary" disabled>
|
||
<i class="bi bi-sliders"></i> Modify
|
||
</button>
|
||
|
||
<button id="btnConfirmSchedule" class="btn btn-success" disabled>
|
||
<i class="bi bi-check"></i> Assign
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ============================ -->
|
||
<!-- BODY -->
|
||
<!-- ============================ -->
|
||
<div class="card-body d-flex flex-column gap-3">
|
||
|
||
<!-- Summary + Toggle -->
|
||
<div class="summary-bar d-flex align-items-center justify-content-between">
|
||
|
||
<!-- Left : Summary text -->
|
||
<div id="iwSummary" class="alert alert-secondary text-start mb-0 flex-grow-1 gap-3">
|
||
<span id="sumStops">Stops: 0</span>
|
||
<span id="sumPickup"></span>
|
||
<span id="sumInstall"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Map View (DEFAULT) -->
|
||
<div id="view-map" class="border rounded flex-fill position-relative">
|
||
<div id="iwMapCanvas" style="width:100%; height: calc(100%); min-height:520px;"></div>
|
||
</div>
|
||
<!-- List View (HIDDEN) -->
|
||
<div id="view-list" class="border rounded p-0 flex-fill d-none">
|
||
<table class="table table-sm table-hover align-middle mb-0 small">
|
||
<thead class="iw-header">
|
||
<tr>
|
||
<th></th>
|
||
<th style="width:20px;">#</th>
|
||
<th>Customer</th>
|
||
<th>UCO</th>
|
||
<th>Address</th>
|
||
<th>Type</th>
|
||
<th>Pickup</th>
|
||
<th>Install</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="iwListBody">
|
||
<!-- JS render -->
|
||
</tbody>
|
||
</table>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<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);
|
||
}));
|
||
// 한번만ㄴ 로드
|
||
if (!d[l]) {
|
||
d[l] = (f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n));
|
||
}
|
||
|
||
})({key:"AIzaSyDg9u03mGrBhyOisp7VGc27CTPI9QXp8sY", v:"weekly"});
|
||
</script>
|
||
|
||
<script>
|
||
let iwMap = null;
|
||
let iwMarkers = [];
|
||
let iwInfo = null;
|
||
let iwDailyRows = [];
|
||
let iwTrialRows = {}; // 가상 추가 리스트
|
||
let iwMapInitialized = false; // 최초 맵
|
||
// 상태 변화
|
||
let baseState = null;
|
||
let currentState = null;
|
||
let isChanged = false;
|
||
|
||
function getSelectedDriverUid() {
|
||
return parseInt(
|
||
document.querySelector('input[name="driver"]:checked')?.value || 0,
|
||
10
|
||
);
|
||
}
|
||
|
||
function clearMarkers(){
|
||
if (iwInfo) iwInfo.close();
|
||
iwMarkers.forEach(m => { if ('map' in m) m.map = null; });
|
||
iwMarkers = [];
|
||
}
|
||
|
||
async function initInstallMap(){
|
||
const { Map, InfoWindow } = await google.maps.importLibrary("maps");
|
||
const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary("marker");
|
||
$('.date-picker-form').datepicker({
|
||
dateFormat: 'yy-mm-dd',
|
||
changeMonth: true,
|
||
changeYear: true
|
||
});
|
||
|
||
iwInfo = new InfoWindow();
|
||
|
||
iwMap = new Map(document.getElementById('iwMapCanvas'), {
|
||
center: { lat:43.65, lng:-79.38 },
|
||
zoom: 10,
|
||
mapId: "4504f8b37365c3d0",
|
||
disableDefaultUI: true
|
||
});
|
||
|
||
loadDailyInstallPoints();
|
||
}
|
||
|
||
function buildInfoHtml(r){
|
||
return `
|
||
<div style="min-width:260px;">
|
||
<div style="font-weight:700; font-size:14px;">
|
||
${r.di_customer_name || ''}
|
||
<span style="color:#666;">(#${r.di_order_seq} ${r.di_accountno || ''})</span>
|
||
</div>
|
||
|
||
<div class="small text-muted" style="margin-top:2px;">
|
||
${r.di_city || ''}
|
||
</div>
|
||
|
||
<div style="margin-top:6px;">
|
||
${r.di_address || ''} ${r.di_postal || ''}
|
||
</div>
|
||
|
||
${r.di_request_note ? `
|
||
<div style="margin-top:8px; padding-top:6px; border-top:1px solid #eee;">
|
||
${r.di_request_note}
|
||
</div>
|
||
` : ``}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
$('#iwMapDate').on('change', function(){
|
||
resetTrialSelectionUI()
|
||
loadDailyInstallPoints();
|
||
});
|
||
|
||
$(document).on('change', 'input[name="driver"]', function () {
|
||
resetTrialSelectionUI()
|
||
loadDailyInstallPoints();
|
||
});
|
||
|
||
function resetTrialSelectionUI() {
|
||
// trial 데이터 초기화
|
||
iwTrialRows = {};
|
||
isChanged = false;
|
||
updateConfirmButton();
|
||
|
||
// 왼쪽 wait-card에서 trial-selected 제거
|
||
document.querySelectorAll('.wait-card.trial-selected').forEach(el => {
|
||
el.classList.remove('trial-selected');
|
||
});
|
||
}
|
||
|
||
$('#filterTwoPerson').on('click', function () {
|
||
const active = $(this).attr('data-active') === '1';
|
||
|
||
if (active) {
|
||
$(this)
|
||
.removeClass('btn-dark text-white')
|
||
.addClass('btn-outline-dark')
|
||
.attr('data-active', '0');
|
||
$('#kw_two_person').val('');
|
||
} else {
|
||
$(this)
|
||
.removeClass('btn-outline-dark')
|
||
.addClass('btn-dark text-white')
|
||
.attr('data-active', '1');
|
||
$('#kw_two_person').val('1');
|
||
}
|
||
|
||
searchWaitlist();
|
||
});
|
||
|
||
$('#filterDraft').on('click', function () {
|
||
const active = $(this).attr('data-active') === '1';
|
||
|
||
if (active) {
|
||
$(this)
|
||
.removeClass('btn-dark text-white')
|
||
.addClass('btn-outline-dark')
|
||
.attr('data-active', '0');
|
||
|
||
$('#kw_draft').val('');
|
||
} else {
|
||
$(this)
|
||
.removeClass('btn-outline-dark')
|
||
.addClass('btn-dark text-white')
|
||
.attr('data-active', '1');
|
||
|
||
$('#kw_draft').val('1');
|
||
}
|
||
|
||
searchWaitlist();
|
||
});
|
||
|
||
function loadDailyInstallPoints(){
|
||
// 날짜
|
||
const input = document.getElementById('iwMapDate');
|
||
let di_install_date = input.value;
|
||
|
||
// 비어 있으면 기본값 세팅
|
||
if (!di_install_date && !iwMapInitialized) {
|
||
const nextBiz = getNextBusinessDate();
|
||
di_install_date = formatDate(nextBiz);
|
||
input.value = di_install_date;
|
||
}
|
||
iwMapInitialized = true;
|
||
|
||
// 드라이버
|
||
const di_driver_uid = getSelectedDriverUid();
|
||
//clearMarkers();
|
||
api('inq_daily_install_points', { di_install_date, di_driver_uid }, res => {
|
||
iwDailyRows = res || [];
|
||
redrawRightPanel();
|
||
|
||
// Assign 버튼
|
||
baseState = getCurrentState();
|
||
recalcChanged(); // isChanged = false, Assign 비활성
|
||
});
|
||
}
|
||
|
||
function drawInstallMap(rows){
|
||
// 항상 먼저 기존 마커 정리
|
||
clearMarkers();
|
||
if (!rows || !rows.length) {
|
||
return;
|
||
}
|
||
|
||
const bounds = new google.maps.LatLngBounds();
|
||
|
||
rows.forEach(r => {
|
||
if (!r.di_lat || !r.di_lng) return;
|
||
|
||
const pos = { lat:+r.di_lat, lng:+r.di_lng };
|
||
bounds.extend(pos);
|
||
|
||
const pin = new google.maps.marker.PinElement({
|
||
glyphText: String(r.di_order_seq || ''),
|
||
background: r.is_trial ? '#fff3cd' : '#0d6efd',
|
||
borderColor: r.is_trial ? '#ffc107' : '#0d6efd',
|
||
glyphColor: r.is_trial ? '#856404' : '#ffffff'
|
||
});
|
||
|
||
const marker = new google.maps.marker.AdvancedMarkerElement({
|
||
map: iwMap,
|
||
position: pos,
|
||
content: createFlagMarker({
|
||
order: r.di_order_seq,
|
||
jobType: r.di_work_type,
|
||
isTrial: r.is_trial
|
||
})
|
||
});
|
||
marker.__di_uid = r.di_uid;
|
||
iwMarkers.push(marker);
|
||
|
||
marker.addListener('gmp-click', () => {
|
||
iwInfo.setContent(buildInfoHtml(r));
|
||
iwInfo.open({ map: iwMap, anchor: marker });
|
||
});
|
||
|
||
});
|
||
|
||
safeFitBounds(bounds);
|
||
}
|
||
|
||
function drawInstallList(rows){
|
||
const tbody = document.getElementById('iwListBody');
|
||
tbody.innerHTML = '';
|
||
|
||
if (!rows.length) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="8" class="text-center text-muted py-4">
|
||
No assigned installs
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
rows.forEach(r => {
|
||
const tr = document.createElement('tr');
|
||
tr.dataset.address = r.di_address || '';
|
||
tr.dataset.city = r.di_city || '';
|
||
tr.dataset.postal = r.di_postal || '';
|
||
if (r.is_trial) {
|
||
tr.dataset.isTrial = '1';
|
||
tr.dataset.iwUid = r.iw_uid; // waitlist uid
|
||
tr.dataset.orderSeq = r.di_order_seq;
|
||
tr.classList.add('table-warning');
|
||
} else {
|
||
tr.dataset.isTrial = '0';
|
||
tr.dataset.iwUid = ''; // 기존 daily_install row
|
||
tr.dataset.diUid = r.di_uid;
|
||
tr.dataset.orderSeq = r.di_order_seq;
|
||
}
|
||
tr.innerHTML = `
|
||
<td>${r.is_trial ? '': `<label class="iw-checkbox-wrap"><input type="checkbox" class="iw-checkbox" value="${r.di_uid}"><span class="iw-checkbox-box"></span></label>`}</td>
|
||
<td>${r.di_order_seq}</td>
|
||
<td class="iw-drag-handle" style="cursor:grab; max-width: 220px;">
|
||
<span class="text-muted me-2" title="Drag to reorder">
|
||
${r.di_accountno || ''}</span>
|
||
<span class="iw-name" title="${r.di_customer_name || ''}">
|
||
<b>${r.di_customer_name || ''}</b>
|
||
</span>
|
||
</td>
|
||
<td>${r.di_customer_driver_initial}</td>
|
||
<td>${r.di_address || ''}<br><span class="text-muted small">${r.di_city || ''}</span></td>
|
||
<td><span class="badge ${getWorkTypeBadge(r.di_work_type)}" title="${r.di_work_type || ''}"> ${(r.di_work_type || '').substring(0, 2)}</span></td>
|
||
<td>${renderContainerSummary(r.pickup_d_cnt, r.pickup_b_cnt)}</td>
|
||
<td>${renderContainerSummary(r.install_d_cnt, r.install_b_cnt)}</td>
|
||
<td class="text-center">${r.is_trial ? '' : `<button class="btn btn-sm btn-outline-danger btn-unassign" title="Remove from schedule"> <i class="bi bi-dash-circle"></i></button>`}</td>
|
||
`;
|
||
|
||
tr.style.cursor = 'pointer';
|
||
//tr.onclick = () => focusMapMarker(r.di_uid);
|
||
tbody.appendChild(tr);
|
||
bindCheckboxEvents();
|
||
});
|
||
bindCheckboxEvents();
|
||
updateModifyButtonState();
|
||
initInstallListSortable();
|
||
}
|
||
|
||
function safeFitBounds(bounds, tryCount = 0) {
|
||
const el = document.getElementById('iwMapCanvas');
|
||
const w = el?.offsetWidth || 0;
|
||
const h = el?.offsetHeight || 0;
|
||
|
||
//
|
||
if (w > 0 && h > 0) {
|
||
iwMap.fitBounds(bounds);
|
||
return;
|
||
}
|
||
|
||
// 너무 오래 끌면 UX 안 좋으니 제한
|
||
if (tryCount >= 10) {
|
||
return;
|
||
}
|
||
|
||
// 다음 프레임에 재시도
|
||
requestAnimationFrame(() => safeFitBounds(bounds, tryCount + 1));
|
||
}
|
||
|
||
function isMapVisible() {
|
||
const viewMap = document.getElementById('view-map');
|
||
const canvas = document.getElementById('iwMapCanvas');
|
||
if (!viewMap || !canvas) return false;
|
||
|
||
// d-none이면 숨김
|
||
if (viewMap.classList.contains('d-none')) return false;
|
||
|
||
// 실제 사이즈 체크
|
||
return canvas.offsetWidth > 0 && canvas.offsetHeight > 0;
|
||
}
|
||
|
||
function getWorkTypeBadge(work) {
|
||
switch ((work || '').toUpperCase()) {
|
||
case 'INSTALL':
|
||
return 'bg-success text-white';
|
||
case 'PICKUP':
|
||
return 'bg-danger text-white';
|
||
case 'EXCHANGE':
|
||
return 'bg-warning text-dark';
|
||
case 'RELOCATE':
|
||
return 'bg-info text-dark';
|
||
case 'CLEAN':
|
||
return 'bg-primary text-white';
|
||
default:
|
||
return 'bg-secondary text-white';
|
||
}
|
||
}
|
||
|
||
function getNextBusinessDate(startDate = new Date()) {
|
||
const d = new Date(startDate);
|
||
d.setDate(d.getDate() + 1);
|
||
|
||
// 토(6), 일(0) 제외
|
||
while (d.getDay() === 0 || d.getDay() === 6) {
|
||
d.setDate(d.getDate() + 1);
|
||
}
|
||
|
||
return d;
|
||
}
|
||
|
||
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 changeMapDate(diff) {
|
||
const input = document.getElementById('iwMapDate');
|
||
let val = input.value;
|
||
|
||
// 값이 비어 있으면 기본값(다음 영업일)부터
|
||
if (!val) {
|
||
const base = getNextBusinessDate();
|
||
val = formatDate(base);
|
||
}
|
||
|
||
const current = parseDate(val);
|
||
const next = moveBusinessDate(current, diff);
|
||
|
||
input.value = formatDate(next);
|
||
|
||
// datepicker + 기존 change 로직 트리거
|
||
$(input).trigger('change');
|
||
}
|
||
|
||
function getCurrentState() {
|
||
const date = document.getElementById('iwMapDate').value;
|
||
|
||
// trial 선택(존재) 상태: iw_uid 목록
|
||
const trials = Object.values(iwTrialRows[date] || [])
|
||
.map(w => String(w.iw_uid))
|
||
.sort();
|
||
|
||
// 현재 화면 순서/seq 상태: trial + existing 둘 다 수집
|
||
const order = [];
|
||
document.querySelectorAll('#iwListBody tr').forEach(tr => {
|
||
const isTrial = tr.dataset.isTrial === '1';
|
||
const seq = parseInt(tr.dataset.orderSeq, 10);
|
||
|
||
if (!seq) return;
|
||
|
||
if (isTrial) {
|
||
const iw_uid = tr.dataset.iwUid;
|
||
if (!iw_uid) return;
|
||
order.push({ t: 'T', id: String(iw_uid), seq });
|
||
} else {
|
||
const di_uid = tr.dataset.diUid;
|
||
if (!di_uid) return;
|
||
order.push({ t: 'D', id: String(di_uid), seq });
|
||
}
|
||
});
|
||
|
||
// 비교가 쉽게 정렬/직렬화 (순서 변경 감지하려면 seq 기준 정렬하면 안됨)
|
||
// DOM 순서 그대로 유지
|
||
return { trials, order };
|
||
}
|
||
|
||
function recalcChanged() {
|
||
currentState = getCurrentState();
|
||
isChanged = !isSameState(baseState, currentState);
|
||
updateConfirmButton();
|
||
}
|
||
|
||
function updateConfirmButton() {
|
||
const btn = document.getElementById('btnConfirmSchedule');
|
||
btn.disabled = !isChanged;
|
||
}
|
||
|
||
function isSameState(a, b) {
|
||
if (!a || !b) return false;
|
||
if (a.trials.length !== b.trials.length) return false;
|
||
for (let i = 0; i < a.trials.length; i++) {
|
||
if (a.trials[i] !== b.trials[i]) return false;
|
||
}
|
||
if (a.order.length !== b.order.length) return false;
|
||
for (let i = 0; i < a.order.length; i++) {
|
||
const x = a.order[i], y = b.order[i];
|
||
if (x.t !== y.t || x.id !== y.id || x.seq !== y.seq) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
|
||
// ◀ 하루 전
|
||
document.querySelector('.btn-date-prev')?.addEventListener('click', () => {
|
||
changeMapDate(-1);
|
||
});
|
||
|
||
// ▶ 하루 후
|
||
document.querySelector('.btn-date-next')?.addEventListener('click', () => {
|
||
changeMapDate(1);
|
||
});
|
||
|
||
function renderSummary(rows){
|
||
let stops = rows.length;
|
||
let pickupD = 0, pickupB = 0;
|
||
let installD = 0, installB = 0;
|
||
|
||
rows.forEach(r => {
|
||
pickupD += Number(r.pickup_d_cnt || 0);
|
||
pickupB += Number(r.pickup_b_cnt || 0);
|
||
installD += Number(r.install_d_cnt || 0);
|
||
installB += Number(r.install_b_cnt || 0);
|
||
});
|
||
|
||
document.getElementById('sumStops').innerText = `Stops: ${stops}`;
|
||
document.getElementById('sumPickup').innerHTML = `Pickup: ${renderContainerSummary(pickupD, pickupB)}`;
|
||
document.getElementById('sumInstall').innerHTML =`Install: ${renderContainerSummary(installD, installB)}`;
|
||
}
|
||
|
||
function renderContainerSummary(dCnt, bCnt){
|
||
let html = '';
|
||
|
||
if (dCnt > 0) {
|
||
html += `<span class="me-2"><i class="bi bi-database-fill"></i> × ${dCnt}</span>`;
|
||
}
|
||
if (bCnt > 0) {
|
||
html += `<span class="me-2"><i class="bi bi-archive-fill"></i> × ${bCnt}</span>`;
|
||
}
|
||
|
||
return html || '<span class="text-muted">–</span>';
|
||
}
|
||
|
||
function toggleTrialInstall(waitRow) {
|
||
const date = document.getElementById('iwMapDate').value;
|
||
|
||
if (!iwTrialRows[date]) {
|
||
iwTrialRows[date] = [];
|
||
}
|
||
|
||
const idx = iwTrialRows[date].findIndex(w => w.iw_uid == waitRow.iw_uid);
|
||
|
||
if (idx > -1) {
|
||
iwTrialRows[date].splice(idx, 1);
|
||
} else {
|
||
iwTrialRows[date].push(waitRow); // 순서 보존
|
||
}
|
||
|
||
redrawRightPanel();
|
||
recalcChanged();
|
||
}
|
||
|
||
let pointGeocoder = null;
|
||
function getPointGeocoder() {
|
||
// google maps가 아직 준비 안 됐으면 null
|
||
if (!window.google || !google.maps || !google.maps.Geocoder) return null;
|
||
|
||
// 이미 만들었으면 재사용
|
||
if (pointGeocoder) return pointGeocoder;
|
||
|
||
// 이제 생성
|
||
pointGeocoder = new google.maps.Geocoder();
|
||
return pointGeocoder;
|
||
}
|
||
|
||
function redrawRightPanel() {
|
||
const date = document.getElementById('iwMapDate').value;
|
||
const realRows = iwDailyRows.slice(); // 실제 daily_install
|
||
const trialRows = iwTrialRows[date] || [];
|
||
const geocoder = getPointGeocoder();
|
||
const trialConverted = trialRows.map((w, idx) => {
|
||
|
||
// 기본 row (lat/lng 없어도 일단 생성)
|
||
const row = {
|
||
iw_uid: w.iw_uid,
|
||
di_uid: `trial_${w.iw_uid}`, // 임시 uid
|
||
di_order_seq: realRows.length + idx + 1,
|
||
di_accountno: w.iw_accountno,
|
||
di_customer_name: w.iw_customer_name,
|
||
di_address: w.iw_address,
|
||
di_city: w.iw_city,
|
||
di_lat: w.iw_lat,
|
||
di_lng: w.iw_lng,
|
||
di_work_type: w.iw_work_type,
|
||
di_customer_driver_initial: w.iw_driver_initial || '',
|
||
is_trial: true,
|
||
|
||
pickup_d_cnt: w.pickup_d_cnt || 0,
|
||
pickup_b_cnt: w.pickup_b_cnt || 0,
|
||
install_d_cnt: w.install_d_cnt || 0,
|
||
install_b_cnt: w.install_b_cnt || 0
|
||
};
|
||
|
||
// lat/lng 없으면 여기서 geocode
|
||
if (!row.di_lat || !row.di_lng) {
|
||
const fullAddress = [w.iw_address, w.iw_city].filter(Boolean).join(', ');
|
||
|
||
if (fullAddress) {
|
||
geocoder.geocode({ address: fullAddress }, (results, status) => {
|
||
if (status === 'OK' && results[0]) {
|
||
|
||
const loc = results[0].geometry.location;
|
||
const lat = loc.lat();
|
||
const lng = loc.lng();
|
||
|
||
// 메모리 갱신 (다시 redraw될 때 사용)
|
||
w.iw_lat = lat;
|
||
w.iw_lng = lng;
|
||
|
||
// 서버 저장
|
||
api('update_waitlist_latlng', {
|
||
iw_uid: w.iw_uid,
|
||
lat: lat,
|
||
lng: lng
|
||
});
|
||
|
||
// 지도 마커 즉시 반영 (있으면 이동)
|
||
updateTrialMarkerPosition?.(w.iw_uid, lat, lng);
|
||
|
||
} else {
|
||
console.warn(
|
||
'[Trial Geocode Failed]',
|
||
w.iw_uid,
|
||
fullAddress,
|
||
status
|
||
);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
return row;
|
||
});
|
||
|
||
const merged = realRows.concat(trialConverted);
|
||
|
||
drawInstallMap(merged);
|
||
drawInstallList(merged);
|
||
renderSummary(merged);
|
||
}
|
||
|
||
function updateTrialMarkerPosition(iw_uid, lat, lng) {
|
||
const di_uid = `trial_${iw_uid}`;
|
||
const marker = iwMarkers.find(m => m.__di_uid === di_uid);
|
||
|
||
if (marker) {
|
||
marker.position = { lat: +lat, lng: +lng }; // AdvancedMarkerElement는 position 재할당 가능
|
||
}
|
||
}
|
||
|
||
function isTrialSelected(iw_uid){
|
||
const date = document.getElementById('iwMapDate').value;
|
||
const list = iwTrialRows[date] || [];
|
||
|
||
return list.some(w => String(w.iw_uid) === String(iw_uid));
|
||
}
|
||
|
||
function updateWaitCardState(iw_uid){
|
||
const card = document.querySelector(`.wait-card[data-iw="${iw_uid}"]`);
|
||
if (!card) return;
|
||
|
||
card.classList.toggle('trial-selected', isTrialSelected(iw_uid));
|
||
}
|
||
// sorting
|
||
let iwSortable = null;
|
||
|
||
function initInstallListSortable() {
|
||
const tbody = document.getElementById('iwListBody');
|
||
if (!tbody) return;
|
||
|
||
if (iwSortable) {
|
||
iwSortable.destroy();
|
||
iwSortable = null;
|
||
}
|
||
|
||
iwSortable = new Sortable(tbody, {
|
||
animation: 150,
|
||
handle: '.iw-drag-handle',
|
||
draggable: 'tr',
|
||
ghostClass: 'table-active',
|
||
onEnd: function () {
|
||
applyOrderSeqFromDom();
|
||
}
|
||
});
|
||
}
|
||
|
||
function bindCheckboxEvents() {
|
||
document.querySelectorAll('.iw-checkbox').forEach(cb => {
|
||
cb.addEventListener('change', function () {
|
||
updateModifyButtonState();
|
||
});
|
||
});
|
||
}
|
||
|
||
function updateModifyButtonState() {
|
||
const checked = document.querySelectorAll('.iw-checkbox:checked');
|
||
const btn = document.getElementById('btnModifySchedule');
|
||
|
||
if (!btn) return;
|
||
|
||
btn.disabled = checked.length === 0;
|
||
}
|
||
|
||
function applyOrderSeqFromDom() {
|
||
const trs = document.querySelectorAll('#iwListBody tr');
|
||
|
||
trs.forEach((tr, idx) => {
|
||
const newSeq = idx + 1;
|
||
tr.dataset.orderSeq = String(newSeq);
|
||
|
||
const tds = tr.querySelectorAll('td');
|
||
const tdSeq = tds[1]; // 0: checkbox, 1: sequence
|
||
if (tdSeq) tdSeq.textContent = newSeq;
|
||
});
|
||
|
||
recalcChanged();
|
||
}
|
||
|
||
function getCurrentOrderState() {
|
||
const arr = [];
|
||
document.querySelectorAll('#iwListBody tr').forEach(tr => {
|
||
const seq = parseInt(tr.dataset.orderSeq || '0', 10);
|
||
|
||
if (tr.dataset.isTrial === '1') {
|
||
arr.push(`T:${tr.dataset.iwUid}:${seq}`);
|
||
} else {
|
||
arr.push(`D:${tr.dataset.diUid}:${seq}`);
|
||
}
|
||
});
|
||
return arr.join('|');
|
||
}
|
||
|
||
// view_mode 토글에서 map으로 전환될 때 “흰 화면/깨짐” 방지
|
||
// (구글맵은 숨겨진 div에서 init되면 사이즈 계산이 어긋날 때가 있음)
|
||
document.querySelectorAll('input[name="view_mode"]').forEach(radio => {
|
||
radio.addEventListener('change', function () {
|
||
document.getElementById('view-map').classList.toggle('d-none', this.value !== 'map');
|
||
document.getElementById('view-list').classList.toggle('d-none', this.value !== 'list');
|
||
if (this.value === 'map' && iwMap) {
|
||
setTimeout(() => google.maps.event.trigger(iwMap, 'resize'), 50);
|
||
}
|
||
});
|
||
});
|
||
|
||
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 '';
|
||
}
|
||
}
|
||
|
||
function createFlagMarker({ order, jobType, isTrial }) {
|
||
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'flag-marker';
|
||
|
||
wrap.style.background = isTrial ? '#7c7c7cff' : '#b30bb3ff';
|
||
wrap.style.color = isTrial ? '#ffffff' : '#ffffff';
|
||
wrap.style.setProperty('--flag-bg', isTrial ? '#7c7c7cff' : '#b30bb3ff');
|
||
|
||
const iconClass = getJobBiIcon(jobType);
|
||
|
||
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;
|
||
}
|
||
|
||
// 좌표 없을 때
|
||
let iwGeocoder = null;
|
||
function getIWGeocoder() {
|
||
if (!iwGeocoder) {
|
||
iwGeocoder = new google.maps.Geocoder();
|
||
}
|
||
return iwGeocoder;
|
||
}
|
||
|
||
// 스케줄에서 삭제
|
||
document.getElementById('iwListBody').addEventListener('click', function (e) {
|
||
const btn = e.target.closest('.btn-unassign');
|
||
if (!btn) return;
|
||
|
||
const tr = btn.closest('tr');
|
||
const di_uid = [tr.dataset.diUid];
|
||
|
||
if (!confirm('Remove this install from today’s schedule?')) {
|
||
return;
|
||
}
|
||
|
||
unassignDailyInstall(di_uid);
|
||
});
|
||
|
||
function unassignDailyInstall(di_uids) {
|
||
|
||
if (!di_uids.length) return;
|
||
|
||
api(
|
||
'unassign_daily_install',
|
||
{ di_uids },
|
||
function (res) {
|
||
if (!res || res.ok !== 1) {
|
||
showPopupMessage(res?.msg || 'Failed to remove install', 'error', 1500);
|
||
return;
|
||
}
|
||
|
||
showPopupMessage(res?.msg || 'Removed Successfully', 'success', 1500);
|
||
|
||
const date = document.getElementById('iwMapDate').value;
|
||
|
||
if (iwTrialRows[date]) {
|
||
iwTrialRows[date] = [];
|
||
}
|
||
|
||
redrawRightPanel();
|
||
recalcChanged();
|
||
loadDailyInstallPoints();
|
||
searchWaitlist();
|
||
},
|
||
function (xhr) {
|
||
console.error(xhr);
|
||
showPopupMessage('Server error while removing install', 'error', 1500);
|
||
}
|
||
);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
// Submit 막기
|
||
document.getElementById('waitlistSearchForm')?.addEventListener('submit', function(e){
|
||
e.preventDefault(); // 페이지 이동 막기
|
||
searchWaitlist(); // ajax 검색
|
||
});
|
||
|
||
// Assign 버튼
|
||
document.getElementById('btnConfirmSchedule').onclick = () => {
|
||
if (!isChanged) return;
|
||
|
||
const btn = document.getElementById('btnConfirmSchedule');
|
||
btn.disabled = true;
|
||
|
||
const di_install_date = document.getElementById('iwMapDate').value;
|
||
const di_driver_uid = getSelectedDriverUid();
|
||
|
||
const trial_rows = [];
|
||
const existing_rows = [];
|
||
|
||
document.querySelectorAll('#iwListBody tr').forEach(tr => {
|
||
const di_order_seq = parseInt(tr.dataset.orderSeq, 10);
|
||
if (!di_order_seq) return;
|
||
|
||
if (tr.dataset.isTrial === '1') {
|
||
const iw_uid = tr.dataset.iwUid;
|
||
if (!iw_uid) return;
|
||
trial_rows.push({ iw_uid, di_order_seq });
|
||
} else {
|
||
const di_uid = tr.dataset.diUid; // 기존 row는 이게 있어야 함
|
||
if (!di_uid) return;
|
||
existing_rows.push({ di_uid, di_order_seq });
|
||
}
|
||
});
|
||
|
||
// trial이 없어도 순서 변경만 저장하려면 confirm 가능해야 하니까
|
||
if (!trial_rows.length && !existing_rows.length) {
|
||
// alert('Nothing to confirm.');
|
||
showPopupMessage('Nothing to confirm.', 'error', 1500);
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
|
||
const payload = { di_install_date, di_driver_uid, trial_rows, existing_rows };
|
||
|
||
api('confirm_daily_install', payload, function (res) {
|
||
if (!res || res.ok !== 1) {
|
||
//alert(res?.msg || 'Assign failed');
|
||
showPopupMessage('Assign failed', 'error', 1500);
|
||
btn.disabled = false;
|
||
return;
|
||
} else {
|
||
showPopupMessage('Successfully Saved', 'success', 1000);
|
||
}
|
||
|
||
const date = document.getElementById('iwMapDate').value;
|
||
iwTrialRows[date] = [];
|
||
|
||
baseState = getCurrentState();
|
||
recalcChanged();
|
||
loadDailyInstallPoints();
|
||
searchWaitlist();
|
||
|
||
btn.disabled = false;
|
||
},
|
||
function (xhr) {
|
||
console.error('Assign AJAX error:', xhr);
|
||
//alert('Failed to confirm schedule.');
|
||
showPopupMessage('Failed to confirm schedule.', 'error', 1500);
|
||
btn.disabled = false;
|
||
}
|
||
);
|
||
};
|
||
// 시작
|
||
initInstallMap();
|
||
</script>
|
||
|
||
|
||
<?php
|
||
// request by select box
|
||
$iwRequestByOptions = "<option value=\"Customer\">Customer</option>";
|
||
|
||
$sql_driver = "
|
||
SELECT dr_uid, dr_initial
|
||
FROM tbl_driver
|
||
WHERE dr_status = 'A'
|
||
ORDER BY dr_initial
|
||
";
|
||
|
||
$rmem = $jdb->nQuery($sql_driver, "Load driver list error");
|
||
|
||
while ($row = mysqli_fetch_assoc($rmem)) {
|
||
$driveruid = $row['dr_uid'];
|
||
$ini = htmlspecialchars(trim($row['dr_initial']));
|
||
if ($ini === '') continue;
|
||
|
||
$iwRequestByOptions .= "<option value=\"{$driveruid}\">{$ini}</option>";
|
||
}
|
||
?>
|
||
|
||
<script>
|
||
window.IW_BINS = <?= json_encode($arrBin, JSON_UNESCAPED_UNICODE) ?>;
|
||
window.IW_REQUEST_BY_OPTIONS = <?= json_encode($iwRequestByOptions) ?>;
|
||
|
||
document.addEventListener('click', function (e) {
|
||
// 1) DELETE
|
||
const delBtn = e.target.closest('.btn-delete-install-wait');
|
||
if (delBtn) {
|
||
e.preventDefault();
|
||
|
||
const iw_uid = delBtn.dataset.iwUid;
|
||
if (!iw_uid) return showPopupMessage('Invalid install request', 'error', 1500); //alert('Invalid install request');
|
||
|
||
if (!confirm('Delete this install request?')) return;
|
||
|
||
delBtn.disabled = true;
|
||
api('install_wait_delete', { iw_uid }, (res) => {
|
||
delBtn.disabled = false;
|
||
if (!res || !res.ok) return showPopupMessage(res?.msg || 'Delete failed', 'error', 1500); //alert(res?.msg || 'Delete failed');
|
||
else showPopupMessage('Deleted successfully.', 'success', 1500);
|
||
delBtn.closest('.wait-card')?.remove();
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 2) EDIT
|
||
const editBtn = e.target.closest('.btn-edit-install-wait');
|
||
if (editBtn) {
|
||
e.preventDefault();
|
||
|
||
const iw_uid = editBtn.dataset.iwUid;
|
||
if (!iw_uid) return showPopupMessage('Invalid install request', 'error', 1500); //alert('Invalid install request');
|
||
|
||
openAddInstallWaitModal({ mode: 'edit', iw_uid });
|
||
return;
|
||
}
|
||
|
||
const routeBtn = e.target.closest('#btnRouteSchedule');
|
||
if (routeBtn) {
|
||
e.preventDefault();
|
||
|
||
openRoute();
|
||
return;
|
||
}
|
||
|
||
// 3) WAIT CARD CLICK (trial toggle)
|
||
const card = e.target.closest('.wait-card');
|
||
if (!card) return;
|
||
|
||
// 버튼/링크 클릭이면 카드 토글 로직 자체를 타지 않게 먼저 컷
|
||
if (e.target.closest('a, button')) return;
|
||
|
||
const status = (card.dataset.status || '').toUpperCase();
|
||
if (status === 'DRAFT') {
|
||
// showPopupMessage('Draft - Complete it first.', 'error', 1500);
|
||
// 완료하세요 안내보다 완료하라고 창 띄우는게 나을 듯
|
||
openAddInstallWaitModal({ mode: 'edit', iw_uid: card.dataset.iw });
|
||
return;
|
||
}
|
||
|
||
const iw_uid = card.dataset.iw;
|
||
if (!iw_uid) return;
|
||
|
||
const waitRow = {
|
||
iw_uid: iw_uid,
|
||
iw_accountno: card.dataset.accountno || '',
|
||
iw_customer_name: card.dataset.name || '',
|
||
iw_work_type: card.dataset.jobType || '',
|
||
iw_driver_initial: card.dataset.driver || '',
|
||
iw_address: card.dataset.address || '',
|
||
iw_city: card.dataset.city || '',
|
||
iw_lat: card.dataset.lat ? Number(card.dataset.lat) : null,
|
||
iw_lng: card.dataset.lng ? Number(card.dataset.lng) : null,
|
||
pickup_d_cnt: parseInt(card.dataset.pickupD || 0),
|
||
pickup_b_cnt: parseInt(card.dataset.pickupB || 0),
|
||
install_d_cnt: parseInt(card.dataset.installD || 0),
|
||
install_b_cnt: parseInt(card.dataset.installB || 0)
|
||
};
|
||
|
||
toggleTrialInstall(waitRow);
|
||
updateWaitCardState(iw_uid);
|
||
|
||
});
|
||
|
||
// 일괄 수정
|
||
function getSelectedDailyInstallIds() {
|
||
return Array.from(document.querySelectorAll('.iw-checkbox:checked'))
|
||
.map(cb => cb.value)
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function openUpdateInstallWaitModal() {
|
||
const checkedIds = getSelectedDailyInstallIds();
|
||
|
||
if (!checkedIds.length) {
|
||
showPopupMessage('Select at least one schedule.', 'error', 1500);
|
||
return;
|
||
}
|
||
|
||
const modalEl = document.getElementById('updateInstallWaitModal');
|
||
|
||
// 선택 개수 / ids
|
||
document.getElementById('updateInstallSelectedCount').textContent = `${checkedIds.length} selected`;
|
||
document.getElementById('updateInstallSelectedIds').value = checkedIds.join(',');
|
||
|
||
// 날짜: 현재 오른쪽 상단 날짜를 그대로 넣기
|
||
const currentMapDate = document.getElementById('iwMapDate')?.value || '';
|
||
const dateInput = document.getElementById('bulkUpdateInstallDate');
|
||
if (dateInput) {
|
||
dateInput.disabled = false;
|
||
dateInput.value = currentMapDate;
|
||
|
||
// datepicker 중복 방지 후 다시 붙이기
|
||
if ($(dateInput).hasClass('hasDatepicker')) {
|
||
$(dateInput).datepicker('destroy');
|
||
}
|
||
|
||
$(dateInput).datepicker({
|
||
dateFormat: 'yy-mm-dd',
|
||
changeMonth: true,
|
||
changeYear: true
|
||
});
|
||
}
|
||
|
||
// 드라이버
|
||
const currentDriverRadio = document.querySelector('input[name="driver"]:checked');
|
||
const currentDriverUid = currentDriverRadio?.value || '';
|
||
const driverSelect = document.getElementById('bulkUpdateDriverUid');
|
||
if (driverSelect) {
|
||
driverSelect.value = String(currentDriverUid);
|
||
}
|
||
|
||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||
modal.show();
|
||
}
|
||
|
||
document.getElementById('btnModifySchedule')?.addEventListener('click', function () {
|
||
openUpdateInstallWaitModal();
|
||
});
|
||
|
||
function submitBulkUpdateInstall() {
|
||
const ids = (document.getElementById('updateInstallSelectedIds')?.value || '')
|
||
.split(',')
|
||
.map(v => v.trim())
|
||
.filter(Boolean);
|
||
|
||
if (!ids.length) {
|
||
showPopupMessage('No selected schedule.', 'error', 1500);
|
||
return;
|
||
}
|
||
|
||
const di_install_date = document.getElementById('bulkUpdateInstallDate')?.value || '';
|
||
const di_driver_uid = document.getElementById('bulkUpdateDriverUid')?.value || '';
|
||
|
||
if (!di_install_date || !di_driver_uid) {
|
||
showPopupMessage('Choose date and driver.', 'error', 1500);
|
||
return;
|
||
}
|
||
|
||
api('bulk_update_daily_install', {
|
||
di_uids: ids,
|
||
di_install_date,
|
||
di_driver_uid
|
||
}, function (res) {
|
||
if (!res || res.ok !== 1) {
|
||
showPopupMessage(res?.msg || 'Bulk update failed', 'error', 1500);
|
||
return;
|
||
}
|
||
|
||
bootstrap.Modal.getInstance(document.getElementById('updateInstallWaitModal'))?.hide();
|
||
|
||
showPopupMessage(res?.msg || 'Updated successfully', 'success', 1200);
|
||
|
||
const date = document.getElementById('iwMapDate').value;
|
||
if (iwTrialRows[date]) {
|
||
iwTrialRows[date] = [];
|
||
}
|
||
|
||
redrawRightPanel();
|
||
recalcChanged();
|
||
loadDailyInstallPoints();
|
||
searchWaitlist();
|
||
}, function (xhr) {
|
||
console.error(xhr);
|
||
showPopupMessage('Server error while updating schedules', 'error', 1500);
|
||
});
|
||
}
|
||
|
||
document.addEventListener('click', function (e) {
|
||
const btn = e.target.closest('#btnBulkUpdate');
|
||
if (!btn) return;
|
||
|
||
submitBulkUpdateInstall();
|
||
});
|
||
|
||
document.addEventListener('click', function (e) {
|
||
const btn = e.target.closest('#btnBulkDelete');
|
||
if (!btn) return;
|
||
|
||
const ids = getSelectedDailyInstallIds();
|
||
|
||
if (!confirm('Remove selected installs from today’s schedule?')) {
|
||
return;
|
||
}
|
||
|
||
bootstrap.Modal.getInstance(document.getElementById('updateInstallWaitModal'))?.hide();
|
||
|
||
unassignDailyInstall(ids);
|
||
});
|
||
</script>
|
||
<script src="/js/install_wait_modal.js?v=20260204"></script>
|
||
<script src="/js/customer_image.js?v=20260204"></script>
|
||
|
||
<!-- add modal -->
|
||
<div class="modal fade" id="addInstallWaitModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||
<div class="modal-content">
|
||
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
Add Install Wait List
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
|
||
<div class="modal-body p-0">
|
||
<!-- ajax loaded -->
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- update modal -->
|
||
<div class="modal fade" id="updateInstallWaitModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-md modal-dialog-centered">
|
||
<div class="modal-content">
|
||
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
Update Schedule
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
|
||
<div class="modal-body" style="padding: 1.25rem;">
|
||
<div class="text-muted small" id="updateInstallSelectedCount">0 selected</div>
|
||
<input type="hidden" id="updateInstallSelectedIds" value="">
|
||
|
||
<div>
|
||
<table class="tb-info-box">
|
||
<tr>
|
||
<td class="td-title-info">Install Date</td>
|
||
<td class="td-text-info">
|
||
<input type="text" id="bulkUpdateInstallDate" class="form-control form-control-sm date-picker-form" style="max-height:20px; margin:0px;" autocomplete="off" placeholder="<?=date('Y-m-d')?>" readonly>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="td-title-info">Driver</td>
|
||
<td class="td-text-info">
|
||
<select id="bulkUpdateDriverUid" class="form-select">
|
||
<?php foreach ($drivers as $d): ?>
|
||
<option value="<?=$d['dr_uid']?>"><?=htmlspecialchars($d['dr_initial'])?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="d-flex modal-footer gap-2">
|
||
<button id="btnBulkDelete" class="btn btn-outline-danger">
|
||
Delete
|
||
</button>
|
||
|
||
<button id="btnBulkUpdate" class="btn btn-success">
|
||
Update
|
||
</button>
|
||
|
||
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||
Cancel
|
||
</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>
|