goiintra/public_html/doc/install_order.php

2227 lines
60 KiB
PHP

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