goiintra/public_html/doc/install_wait_list.php

2361 lines
78 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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)";
$kw_categories = $_GET['kw_category'] ?? [];
if (!is_array($kw_categories)) $kw_categories = [$kw_categories];
$kw_categories = array_filter(array_map('intval', $kw_categories), function($v){
return $v >= 0 && $v <= 5;
});
if ($kw_categories) {
$hasZero = in_array(0, $kw_categories);
$cats = array_filter($kw_categories, fn($v) => $v > 0);
$cond = [];
if ($cats) $cond[] = "iw_category IN (" . implode(',', $cats) . ")";
if ($hasZero) $cond[] = "(iw_category = 0 OR iw_category IS NULL)";
$where[] = '(' . implode(' OR ', $cond) . ')';
}
$statusList = [];
if (!empty($_GET['kw_waiting'])) $statusList[] = "'WAITING'";
if (!empty($_GET['kw_draft'])) $statusList[] = "'DRAFT'";
if (!empty($_GET['kw_assigned'])) $statusList[] = "'ASSIGNED'";
// 기본
if (!$statusList) {
$statusList = ["'WAITING'", "'DRAFT'"];
}
$statusWhere = "iw_status IN (" . implode(',', $statusList) . ")";
if (!empty($where)) {
$add_where = ' AND ' . implode(' AND ', $where);
}
function db_escape($jdb, $str) {
return mysqli_real_escape_string($jdb->DBConn, $str);
}
$view_mode = ($_GET['view_mode'] ?? 'list') === 'map' ? 'map' : 'list';
// ============================
// Total count
// ============================
$sql_cnt = "
SELECT COUNT(*)
FROM tbl_install_waitlist
WHERE {$statusWhere}
$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, di.di_install_date
FROM tbl_install_waitlist iw
LEFT OUTER JOIN tbl_member m
ON iw.iw_created_by = m.m_uid
LEFT JOIN (
SELECT
di_wait_uid,
MAX(di_install_date) as di_install_date
FROM tbl_daily_install
WHERE di_status != 'X'
GROUP BY di_wait_uid
) di ON di.di_wait_uid = iw.iw_uid
WHERE {$statusWhere}
$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 ($iw_uids) {
$in = implode(',', $iw_uids);
$sql_img = "
SELECT DISTINCT i_customeruid
FROM tbl_customer_image
WHERE i_status = 'A'
AND i_type = 'install_order'
AND i_sourceuid 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;
}
function getWaitCategoryColor($cat) {
switch ((int)$cat) {
case 1: return '#ff4d4f'; // red
case 2: return '#1677ff'; // blue
case 3: return '#52c41a'; // green
case 4: return '#fa9614'; // orange
case 5: return '#9254de'; // purple
default: return 'transparent';
}
}
// 하드코딩
$categoryLabels = [
0 => 'No Category',
1 => 'To do list',
2 => 'Power Wash',
3 => 'Category 3',
4 => 'Category 4',
5 => 'Category 5'
];
?>
<!-- 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 || '';
document.getElementById('iwLockDate').checked = (data.iw_lock_date == 1);
//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 || '';
}
// Oil-Pickup
if (data.iw_need_oil_pickup == 1) {
btnNeedOilPickup.classList.add('iw-toggle-on');
}
InstallWait.category = parseInt(data.iw_category || 0, 10);
// job label 갱신
document.getElementById('iwStep3Title').innerText = `Step 3 of 3 - ${data.iw_work_type} Detail`;
// action box 다시 그림
setStep3Title();
renderStep3ActionBoxes();
renderCategorySelector();
bindCategoryEvents();
loadCurrentContainersIfNeeded(() => {
currentReady = true;
});
checkCustomerHasImages(InstallWait.customer?.uid, InstallWait.iw_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-2 d-flex align-items-stretch">
<div class="radio-segment w-100">
<label>
<input type="checkbox" name="kw_draft" id="kw_draft" checked>
<span title="Draft"><i class="bi bi-clipboard-check"></i> </span>
</label>
<label>
<input type="checkbox" name="kw_waiting" id="kw_waiting" checked>
<span title="Waiting"><i class="bi bi-hourglass-split"></i> </span>
</label>
<label>
<input type="checkbox" name="kw_assigned" id="kw_assigned">
<span title="Assigned"><i class="bi bi-link-45deg"></i> </span>
</label>
</div>
</div>
<!-- Category -->
<div class="col-md-3">
<div class="cat-filter-box w-100 h-100">
<div class="category-filter">
<?php
$allCats = [0,1,2,3,4,5];
foreach ($allCats as $i):
$checked = in_array($i, $kw_categories ?? []) ? 'checked' : '';
?>
<label class="cat-filter-dot-wrap" title="<?=htmlspecialchars($categoryLabels[$i] ?? '')?>">
<input type="checkbox" name="kw_category[]" value="<?=$i?>" <?=$checked?>>
<span class="cat-filter-dot" style="--cat-color:<?=getWaitCategoryColor($i)?>"></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Search -->
<div class="col-md-3">
<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-1 d-flex justify-content-end">
<button type="button" class="btn-add" onclick="openAddInstallWaitModal()">
Add
</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++;
}
}
?>
<?php
$isAssigned = ($row['iw_status'] === 'ASSIGNED');
?>
<div class="wait-card <?= $isAssigned ? 'assigned' : '' ?>"
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 ?>"
data-category="<?= (int)($row['iw_category'] ?? 0) ?>"
>
<!-- 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_need_oil_pickup'] > 0): ?>
<span class="badge bg-dark text-white">
<i class="bi bi-droplet"></i>
Pickup
</span>
<?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">
<?php if (!empty($row['iw_category'])): ?>
<span style="
width:16px;
height:16px;
border-radius:50%;
display:inline-block;
background:<?=getWaitCategoryColor($row['iw_category'])?>;
" title="<?=htmlspecialchars($categoryLabels[$row['iw_category']] ?? '')?>"></span>
<?php endif; ?>
<small class="text-muted me-2">
<?php if (!empty($row['di_install_date'])): ?>
<i class="bi bi-link-45deg text-secondary" title="Assigned"></i>
Assigned: <?=$row['di_install_date']?>
<?php else: ?>
<?=$row['iw_lock_date'] == 1 ? '<i class="bi bi-lock text-secondary" title="Preferred Date"></i>' : '';?>
Due: <?=$row['iw_requested_due_date'] ?: '-'?>
<?php endif; ?>
</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; max-width:120px;" 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" <?= $view_mode === 'map' ? 'checked' : '' ?>>
<span><i class="bi bi-map"></i></span>
</label>
<label>
<input type="radio" name="view_mode" value="list" <?= $view_mode === 'list' ? 'checked' : '' ?>>
<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="btnPreview" class="btn btn-outline-secondary" onclick="previewPopup();">
<i class="bi bi-eye"></i> Preview
</button>
<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>&nbsp;&nbsp;
<span id="sumPickup"></span>
<span id="sumInstall"></span>
</div>
</div>
<!-- Map View (DEFAULT) -->
<div id="view-map" class="border rounded flex-fill position-relative d-none">
<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">
<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();
});
$(document).on('change', 'input[name="kw_category[]"]', function () {
searchWaitlist();
});
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();
});
$('#kw_draft, #kw_waiting, #kw_assigned').on('change', function () {
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 class="iw-drag-handle" style="cursor:grab;">
${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>
${r.di_lock_date == 1 ? '<i class="bi bi-lock text-secondary" title="Preferred Date"></i>' : ''}
`}
</td>
<td class="iw-drag-handle" style="cursor:grab;">${r.di_order_seq}</td>
<td style="max-width: 220px;">
<span class="text-muted me-2" title="Drag to reorder">
${r.di_accountno || ''}</span>
<span style="cursor:grab;" class="iw-drag-handle iw-name" title="${r.di_customer_name || ''}">
<b>${r.di_customer_name || ''}</b>
</span>
</td>
<td>${r.di_customer_driver_initial}</td>
<td class="iw-drag-handle" style="cursor:grab;">${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>
`;
const tooltipHtml = `
Req note: ${r.di_request_note || '-'}<br>
CS note: ${r.di_cs_note || '-'}<br>
Work note: ${r.di_work_note || '-'}
`;
tr.setAttribute('data-bs-toggle', 'tooltip');
tr.setAttribute('data-bs-html', 'true');
tr.setAttribute('title', tooltipHtml);
tr.style.cursor = 'pointer';
//tr.onclick = () => focusMapMarker(r.di_uid);
tbody.appendChild(tr);
bindCheckboxEvents();
});
bindCheckboxEvents();
updateModifyButtonState();
initInstallListSortable();
initTooltips();
}
function initTooltips() {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
}
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_postal: w.iw_postal,
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, w.iw_postal].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 = Array.from(document.querySelectorAll('#iwListBody tr'));
if (!trs.length) return;
const seqValues = trs
.map(tr => parseInt(tr.dataset.orderSeq || '0', 10))
.filter(v => v > 0);
const startSeq = seqValues.length ? Math.min(...seqValues) : 1;
trs.forEach((tr, idx) => {
const newSeq = startSeq + idx;
tr.dataset.orderSeq = String(newSeq);
const tds = tr.querySelectorAll('td');
const tdSeq = tds[1];
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 todays 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 todays schedule?')) {
return;
}
bootstrap.Modal.getInstance(document.getElementById('updateInstallWaitModal'))?.hide();
unassignDailyInstall(ids);
});
function previewPopup() {
const startDate = '<?= date('Y-m-d') ?>';
const endDate = '<?= date('Y-m-d', strtotime('+6 days')) ?>';
const url =
`/lib/install_preview_popup.php` +
`?startDate=${startDate}` +
`&endDate=${endDate}`;
window.open(
url,
"installPreviewPopup",
"width=1400,height=900,resizable=yes,scrollbars=yes"
);
}
</script>
<script src="/js/install_wait_modal.js?v=<?=time()?>"></script>
<script src="/js/customer_image.js?v=<?=time()?>"></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>