diff --git a/public_html/assets/api/install.php b/public_html/assets/api/install.php index 7a4896d..3da12c0 100644 --- a/public_html/assets/api/install.php +++ b/public_html/assets/api/install.php @@ -1413,7 +1413,10 @@ trait InstallAPI { // 2) 상태 업데이트 $this->updateDailyInstallStatus($di_uid, $newStatus, $install_note, $oil_qty, $sludge); - // 2-1) additional note 저장 + // 2-1) request note 저장 (di_request_note → tbl_note) : additional 보다 먼저 + $this->upsertRequestNote($di); + + // 2-2) additional note 저장 $this->upsertAdditionalNote($di, $additional_note); // 3) 컨테이너 반영 @@ -1484,6 +1487,142 @@ trait InstallAPI { } } + /** + * di_request_note 를 tbl_note 에 upsert + * + * waitlist 에서 넘어온 데이터이고 request 시점에 남긴 노트라 + * di_wait_uid 와 더 강하게 결합. n_dailyuid 를 'w_{di_wait_uid}' 로 저장. + * 예: di_wait_uid = 55 이면 n_dailyuid = 'w_55' + * + * 본문 뒤에 iw_request_at / iw_request_by 를 덧붙인다. + * iw_request_by 가 'Customer' 면 'Customer', 아니면 tbl_driver.dr_initial. + */ + private function upsertRequestNote(array $di): void + { + $connect = $GLOBALS['conn']; + + $di_wait_uid = intval($di['di_wait_uid'] ?? 0); + if ($di_wait_uid <= 0) { + return; // waitlist 에서 온 게 아니면 스킵 + } + + // request 전용 note key (di_wait_uid 기준) + $noteDailyUid = 'w_' . $di_wait_uid; + $n_dailyuid = mysqli_real_escape_string($connect, $noteDailyUid); + + $noteText = trim($di['di_request_note'] ?? ''); + + // 노트 없으면 기존 note 삭제 후 종료 + if (strlen($noteText) <= 1) { + mysqli_query($connect, " + DELETE FROM tbl_note + WHERE n_dailyuid = '{$n_dailyuid}' + "); + return; + } + + // waitlist 에서 request 정보 조회 (request_by → Customer or driver initial) + $qrWait = mysqli_query($connect, " + SELECT + iw.iw_request_at, + CASE + WHEN iw.iw_request_by = 'Customer' THEN 'Customer' + ELSE IFNULL(dr.dr_initial, '') + END AS request_by_name + FROM tbl_install_waitlist iw + LEFT JOIN tbl_driver dr + ON iw.iw_request_by = dr.dr_uid + WHERE iw.iw_uid = '{$di_wait_uid}' + LIMIT 1 + "); + + $requestBy = ''; + $requestAt = ''; + if ($qrWait && ($w = mysqli_fetch_assoc($qrWait))) { + $requestBy = trim($w['request_by_name'] ?? ''); + $requestAt = trim($w['iw_request_at'] ?? ''); + } + + // 본문 뒤에 request 정보 덧붙이기 + $suffixParts = []; + if ($requestBy !== '') $suffixParts[] = $requestBy; + if ($requestAt !== '') $suffixParts[] = date('Y-m-d', strtotime($requestAt)); + + $finalNote = $noteText; + if (count($suffixParts)) { + $finalNote .= " (Install requested by " . implode(' at ', $suffixParts) . ")"; + } + + $customeruid = mysqli_real_escape_string($connect, $di['di_customer_uid'] ?? ''); + $memberuid = mysqli_real_escape_string($connect, $_SESSION['ss_UID'] ?? ''); + $noteEsc = mysqli_real_escape_string($connect, $finalNote); + $now14 = date('YmdHis'); + $level = intval($_SESSION['ss_LEVEL'] ?? 9); + + // 기존 note 존재 여부 확인 + $qr = mysqli_query($connect, " + SELECT n_uid + FROM tbl_note + WHERE n_dailyuid = '{$n_dailyuid}' + LIMIT 1 + "); + + if (!$qr) { + throw new Exception('request note select failed'); + } + + $existing = mysqli_fetch_assoc($qr); + + if ($existing) { + // UPDATE + $ok = mysqli_query($connect, " + UPDATE tbl_note + SET + n_memberuid = '{$memberuid}', + n_customeruid = '{$customeruid}', + n_type = 'D', + n_level = {$level}, + n_view = 1, + n_note = '{$noteEsc}', + n_createddate = '{$now14}' + WHERE n_dailyuid = '{$n_dailyuid}' + LIMIT 1 + "); + + if (!$ok) { + throw new Exception('request note update failed'); + } + + } else { + // INSERT + $ok = mysqli_query($connect, " + INSERT INTO tbl_note ( + n_memberuid, + n_customeruid, + n_dailyuid, + n_type, + n_level, + n_view, + n_note, + n_createddate + ) VALUES ( + '{$memberuid}', + '{$customeruid}', + '{$n_dailyuid}', + 'D', + {$level}, + 1, + '{$noteEsc}', + '{$now14}' + ) + "); + + if (!$ok) { + throw new Exception('request note insert failed'); + } + } + } + /** * install 추가 메모를 tbl_note에 upsert * diff --git a/public_html/doc/customer_detail.php b/public_html/doc/customer_detail.php index f3aaa7b..28f5062 100644 --- a/public_html/doc/customer_detail.php +++ b/public_html/doc/customer_detail.php @@ -1292,6 +1292,11 @@ $(document).ready(function() font-weight: 200; padding:-1em; } +.cm-opt:hover:not(.active):not(:disabled){ + color: #6c757d; + background-color: transparent; + border-color: #6c757d; +} diff --git a/public_html/doc/customer_list.php b/public_html/doc/customer_list.php index dff35cb..bee1d50 100644 --- a/public_html/doc/customer_list.php +++ b/public_html/doc/customer_list.php @@ -164,6 +164,9 @@ foreach ($rows as $list) { $c_statusSTR = $arrStatus[$c_status]; $c_istopSTR = ($c_is_top === 'A') ? 'A Rank' : (($c_is_top === 'G') ? 'Ghost' : ''); + $c_contractdateSTR = substr ($c_contractdate, 0, 4)."-".substr ($c_contractdate, 4, 2)."-".substr ($c_contractdate, 6, 2); + $c_inactivedateSTR = !empty($c_inactivedate) ? substr($c_inactivedate, 0, 4).'-'.substr($c_inactivedate, 4, 2).'-'.substr($c_inactivedate, 6, 2) : ''; + $todayQty = number_format(round(floatval($c_fullquantity - $c_fullquantitydaily))); //$qry_driver = "SELECT m_firstname, m_lastname FROM tbl_member WHERE m_uid = '$c_driveruid' "; //$rt_driver = $jdb->fQuery($qry_driver, "fetch query error"); @@ -183,7 +186,7 @@ foreach ($rows as $list) { $list_number - + ".$todayQty." @@ -191,14 +194,16 @@ foreach ($rows as $list) { $c_accountno $c_addressSTR $c_city - $c_postal + $c_paymenttype $c_rate $containerChipsHtml $m_initial $c_phoneSTR $c_statusSTR - $c_istopSTR + + $c_contractdateSTR + $c_inactivedateSTR "; @@ -430,206 +435,26 @@ $(document).ready(function(){ - + - + - + + + - -
NoOption Liter Restaurant Name Account Address CityPostal Code Payment Rate Container Driver Phone StatusRankContractInactive
diff --git a/public_html/doc/install_wait_list.php b/public_html/doc/install_wait_list.php index 11bdba0..ec3aec0 100644 --- a/public_html/doc/install_wait_list.php +++ b/public_html/doc/install_wait_list.php @@ -1075,7 +1075,12 @@ $categoryLabels = [ - + @@ -1145,6 +1150,68 @@ $categoryLabels = [ ); } + // 작업 상태(날짜/드라이버/뷰) 유지 + const IW_MAP_DATE_KEY = 'iwMapDate'; + const IW_MAP_DRIVER_KEY = 'iwMapDriverUid'; + const IW_MAP_VIEW_KEY = 'iwViewMode'; + + function getSelectedViewMode() { + return document.querySelector('input[name="view_mode"]:checked')?.value || 'list'; + } + + function saveMapControls() { + try { + const date = document.getElementById('iwMapDate')?.value || ''; + if (date) sessionStorage.setItem(IW_MAP_DATE_KEY, date); + else sessionStorage.removeItem(IW_MAP_DATE_KEY); + + const driverUid = getSelectedDriverUid(); + if (driverUid) sessionStorage.setItem(IW_MAP_DRIVER_KEY, String(driverUid)); + else sessionStorage.removeItem(IW_MAP_DRIVER_KEY); + + sessionStorage.setItem(IW_MAP_VIEW_KEY, getSelectedViewMode()); + } catch (e) { /* sessionStorage 사용 불가 시 무시 */ } + } + + // change 이벤트를 발생시키지 않고 값만 복원한다(중복 로드 방지). + function restoreMapControls() { + try { + const savedDate = sessionStorage.getItem(IW_MAP_DATE_KEY); + if (savedDate) { + const input = document.getElementById('iwMapDate'); + if (input) input.value = savedDate; + } + + const savedDriver = sessionStorage.getItem(IW_MAP_DRIVER_KEY); + if (savedDriver) { + // 저장된 드라이버가 현재 목록에 남아 있을 때만 선택을 복원한다. + const radio = document.querySelector( + 'input[name="driver"][value="' + savedDriver + '"]' + ); + if (radio) radio.checked = true; + } + + // 뷰(map/list) 복원: change 이벤트 없이 직접 토글한다. + // initInstallMap() 안에서 iwMap 생성 직후 호출되므로 이 시점에 iwMap 존재. + const savedView = sessionStorage.getItem(IW_MAP_VIEW_KEY); + if (savedView) { + const viewRadio = document.querySelector( + 'input[name="view_mode"][value="' + savedView + '"]' + ); + if (viewRadio) viewRadio.checked = true; + + const isMap = savedView === 'map'; + document.getElementById('view-map')?.classList.toggle('d-none', !isMap); + document.getElementById('view-list')?.classList.toggle('d-none', isMap); + + // 숨겨진 div에서 init된 구글맵의 사이즈 어긋남 방지 + if (isMap && typeof iwMap !== 'undefined' && iwMap) { + setTimeout(() => google.maps.event.trigger(iwMap, 'resize'), 50); + } + } + } catch (e) { /* sessionStorage 사용 불가 시 무시 */ } + } + function clearMarkers(){ if (iwInfo) iwInfo.close(); iwMarkers.forEach(m => { if ('map' in m) m.map = null; }); @@ -1169,6 +1236,8 @@ $categoryLabels = [ disableDefaultUI: true }); + // 새로고침 전에 작업하던 날짜/드라이버 복원 (loadDailyInstallPoints 호출 전) + restoreMapControls(); loadDailyInstallPoints(); } @@ -1198,11 +1267,13 @@ $categoryLabels = [ } $('#iwMapDate').on('change', function(){ + saveMapControls(); resetTrialSelectionUI() loadDailyInstallPoints(); }); $(document).on('change', 'input[name="driver"]', function () { + saveMapControls(); resetTrialSelectionUI() loadDailyInstallPoints(); }); @@ -1780,6 +1851,35 @@ $categoryLabels = [ updateModifyButtonState(); }); }); + + // 헤더 일괄 체크박스 (onchange 라 재바인딩 시 중복 안 됨) + const checkAll = document.getElementById('iwCheckAll'); + if (checkAll) { + checkAll.onchange = function () { + const on = this.checked; + document.querySelectorAll('.iw-checkbox').forEach(cb => { + cb.checked = on; + }); + updateModifyButtonState(); + }; + } + } + + function syncCheckAllState() { + const checkAll = document.getElementById('iwCheckAll'); + if (!checkAll) return; + + const boxes = document.querySelectorAll('.iw-checkbox'); + const checked = document.querySelectorAll('.iw-checkbox:checked'); + + if (!boxes.length) { + checkAll.checked = false; + checkAll.indeterminate = false; + return; + } + + checkAll.checked = checked.length === boxes.length; + checkAll.indeterminate = checked.length > 0 && checked.length < boxes.length; } function updateModifyButtonState() { @@ -1836,6 +1936,7 @@ $categoryLabels = [ if (this.value === 'map' && iwMap) { setTimeout(() => google.maps.event.trigger(iwMap, 'resize'), 50); } + saveMapControls(); }); }); diff --git a/public_html/doc/map.php b/public_html/doc/map.php index e13adfe..5c3a8c9 100644 --- a/public_html/doc/map.php +++ b/public_html/doc/map.php @@ -2672,8 +2672,9 @@ function popup(){ fd.append('i_createdby', ); fd.append('i_note', $installNote); fd.append('i_sourceuid', iw_uid); - - [...files].forEach(file => { + // 파일명 순서대로 + [...files].sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })) + .forEach(file => { fd.append('upload_file[]', file); }); diff --git a/public_html/js/install_wait_modal.js b/public_html/js/install_wait_modal.js index 3cb76f5..426de84 100644 --- a/public_html/js/install_wait_modal.js +++ b/public_html/js/install_wait_modal.js @@ -344,7 +344,9 @@ function initInstallWaitStep3() { fd.append('i_createdby', memberUid); fd.append('i_note', 'Before Install'); fd.append('i_sourceuid', InstallWait.iw_uid); - [...files].forEach(file => { + // 파일명 순서대로 + [...files].sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })) + .forEach(file => { fd.append('upload_file[]', file); }); diff --git a/public_html/lib/add_install_wait_modal.php b/public_html/lib/add_install_wait_modal.php index f82ddb7..33c64c0 100644 --- a/public_html/lib/add_install_wait_modal.php +++ b/public_html/lib/add_install_wait_modal.php @@ -164,7 +164,7 @@
- +
diff --git a/public_html/lib/customer_image_upload_popup.php b/public_html/lib/customer_image_upload_popup.php index ffdab48..454fc53 100644 --- a/public_html/lib/customer_image_upload_popup.php +++ b/public_html/lib/customer_image_upload_popup.php @@ -119,9 +119,23 @@ document.addEventListener("DOMContentLoaded", function () { if (input) { input.addEventListener("change", function () { + // 파일명순 보장: 선택된 파일을 파일명 기준 자연정렬(numeric)하고 + // input.files 자체를 재구성한다. native form submit 은 이 input.files 를 + // 그대로 전송하므로, 서버에도 이 순서대로 올라간다. 미리보기도 같은 순서. + const sorted = Array.from(this.files) + .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })); + + try { + const dt = new DataTransfer(); + sorted.forEach(f => dt.items.add(f)); + this.files = dt.files; // 재할당해도 change 가 다시 발생하지는 않음 + } catch (e) { + // 구형 브라우저에서 DataTransfer 미지원 시: 미리보기만 정렬됨 + } + previewList.innerHTML = ""; - Array.from(this.files).forEach(file => { + sorted.forEach(file => { // ← this.files 대신 sorted 로 미리보기 if (!file.type.startsWith("image/")) return; const reader = new FileReader();
+ + # Customer UCO