diff --git a/public_html/assets/api/install.php b/public_html/assets/api/install.php index a1f7697..9fa3861 100644 --- a/public_html/assets/api/install.php +++ b/public_html/assets/api/install.php @@ -2,6 +2,11 @@ trait InstallAPI { + protected $dailyService; + public function __construct() { + $this->dailyService = new DailyService(); + } + public function search_customer() { $keyword = trim($_POST['keyword'] ?? ''); if ($keyword === '') { @@ -29,7 +34,7 @@ trait InstallAPI { SELECT 1 FROM tbl_install_waitlist iw WHERE iw.iw_customer_uid = c.c_uid - AND iw.iw_status <> 'DELETED' + AND iw.iw_status not in ('DELETED', 'COMPLETED') ) ORDER BY c.c_name LIMIT 20 @@ -47,6 +52,7 @@ trait InstallAPI { $c_uid = $_POST['c_uid'] ?? 0; $job_type = $_POST['job_type'] ?? ''; $driver_uid = $_POST['driver_uid'] ?? ''; + $member_uid = $_POST['member_uid'] ?? ''; $request_note = $_POST['request_note'] ?? ''; if (!$c_uid || !$job_type) { @@ -90,7 +96,7 @@ trait InstallAPI { '{$driver_uid}' AS iw_request_by, '".addslashes($request_note)."' AS iw_request_note, NOW() AS iw_request_at, - '{$driver_uid}' AS iw_created_by, + '{$member_uid}' AS iw_created_by, NOW() AS iw_created_at FROM tbl_customer c WHERE c.c_uid = '{$c_uid}' @@ -216,11 +222,11 @@ trait InstallAPI { FROM tbl_install_waitlist iw LEFT JOIN tbl_daily_install di - ON di.di_wait_uid = iw.iw_uid + ON di.di_wait_uid = iw.iw_uid LEFT JOIN tbl_member m - ON iw.iw_request_by = m.m_uid + ON iw.iw_created_by = m.m_uid WHERE iw.iw_customer_uid = '{$c_uid}' - AND iw.iw_status IN ('DRAFT','WAITING','ASSIGNED') + AND iw.iw_status IN ('DRAFT','WAITING','ASSIGNED') ORDER BY iw.iw_uid DESC, di.di_uid DESC LIMIT 1 "; @@ -270,49 +276,74 @@ trait InstallAPI { $iw_uid = intval($_POST['iw_uid'] ?? 0); $items = $_POST['items'] ?? []; - $action = $_POST['action']; + $requestAction = $_POST['action'] ?? null; if (!$iw_uid) { echo $this->json(['ok' => 0, 'msg' => 'no iw_uid']); return; } - // 기존 컨테이너 전부 삭제 - qry(" - DELETE FROM tbl_install_waitlist_container - WHERE iwc_wait_uid = '{$iw_uid}' AND iwc_action = '{$action}' - "); + qry("START TRANSACTION"); - // 재 insert - if (is_array($items)) { - foreach ($items as $it) { + try { - $action = addslashes($it['action'] ?? ''); - $type = addslashes($it['type'] ?? ''); - $capacity = isset($it['capacity']) && $it['capacity'] !== '' - ? intval($it['capacity']) - : 'NULL'; - $is_used = addslashes($it['is_used'] ?? 'N'); - $owner_type = addslashes($it['owner_type'] ?? 'C'); - $has_lock = addslashes($it['has_lock'] ?? 'N'); - $has_wheel = addslashes($it['has_wheel'] ?? 'N'); - $has_frame = addslashes($it['has_frame'] ?? 'N'); + // 기존 컨테이너 전부 삭제 + qry(" + DELETE FROM tbl_install_waitlist_container + WHERE iwc_wait_uid = '{$iw_uid}' AND iwc_action = '{$requestAction}' + "); - $sql = " - INSERT INTO tbl_install_waitlist_container - SET - iwc_wait_uid = '{$iw_uid}', - iwc_action = '{$action}', - iwc_type = '{$type}', - iwc_capacity = {$capacity}, - iwc_is_used = '{$is_used}', - iwc_owner_type = '{$owner_type}', - iwc_has_lock = '{$has_lock}', - iwc_has_wheel = '{$has_wheel}', - iwc_has_woodframe = '{$has_frame}' - "; - qry($sql); + // 재 insert + if (is_array($items)) { + foreach ($items as $it) { + + // item action 따로 사용 + $itemAction = addslashes($it['action'] ?? ''); + if (!$itemAction) { + throw new Exception('missing item action'); + } + + $type = addslashes($it['type'] ?? ''); + $capacity = isset($it['capacity']) && $it['capacity'] !== '' + ? intval($it['capacity']) + : 'NULL'; + $is_used = addslashes($it['is_used'] ?? 'N'); + $owner_type = addslashes($it['owner_type'] ?? 'C'); + $has_lock = addslashes($it['has_lock'] ?? 'N'); + $has_wheel = addslashes($it['has_wheel'] ?? 'N'); + $has_frame = addslashes($it['has_frame'] ?? 'N'); + + $sql = " + INSERT INTO tbl_install_waitlist_container + SET + iwc_wait_uid = '{$iw_uid}', + iwc_action = '{$itemAction}', + iwc_type = '{$type}', + iwc_capacity = {$capacity}, + iwc_is_used = '{$is_used}', + iwc_owner_type = '{$owner_type}', + iwc_has_lock = '{$has_lock}', + iwc_has_wheel = '{$has_wheel}', + iwc_has_woodframe = '{$has_frame}' + "; + + if (!qry($sql)) { + throw new Exception('insert failed'); + } + } } + + qry("COMMIT"); + + } catch (Exception $e) { + + qry("ROLLBACK"); + + echo $this->json([ + 'ok' => 0, + 'msg' => $e->getMessage() + ]); + return; } echo $this->json(['ok' => 1]); @@ -512,18 +543,18 @@ trait InstallAPI { c.c_name AS di_customer_name, SUM( CASE - WHEN dic.dic_action = 'REMOVE' + WHEN dic.dic_action = 'PICKUP' AND dic.dic_type = 'D' THEN 1 ELSE 0 END - ) AS remove_d_cnt, + ) AS pickup_d_cnt, SUM( CASE - WHEN dic.dic_action = 'REMOVE' + WHEN dic.dic_action = 'PICKUP' AND dic.dic_type <> 'D' THEN 1 ELSE 0 END - ) AS remove_b_cnt, + ) AS pickup_b_cnt, SUM( CASE WHEN dic.dic_action = 'INSTALL' @@ -688,78 +719,124 @@ trait InstallAPI { $fail('validate', 'invalid params'); } - // 둘 다 비면 아무것도 할게 없음 if ((!is_array($trial_rows) || !count($trial_rows)) && (!is_array($existing_rows) || !count($existing_rows))) { $fail('validate', 'nothing to confirm'); } + /* ======================================================= + 0. 기존 DB max seq 가져오기 + ======================================================= */ + $qr_max = qry(" + SELECT MAX(di_order_seq) AS max_seq + FROM tbl_daily_install + WHERE di_driver_uid = '{$di_driver_uid}' + AND di_install_date = '{$di_install_date}' + "); + + $row_max = fetch_array($qr_max); + $base_seq = intval($row_max['max_seq'] ?? 0); + + /* ======================================================= + 1. 전체 rows 합치고 seq 재정렬 + ======================================================= */ + $all = array_merge( + is_array($existing_rows) ? $existing_rows : [], + is_array($trial_rows) ? $trial_rows : [] + ); + + usort($all, function($a, $b){ + return intval($a['di_order_seq'] ?? 0) <=> intval($b['di_order_seq'] ?? 0); + }); + + $seq = 1; + foreach($all as &$r){ + // base_seq 더해줌 + $r['di_order_seq'] = $base_seq + $seq++; + } + unset($r); + + /* ======================================================= + 2. 다시 existing / trial 분리 + ======================================================= */ + $existing_map = []; + foreach($existing_rows as $row){ + $existing_map[intval($row['di_uid'])] = true; + } + + $new_existing_rows = []; + $new_trial_rows = []; + + foreach($all as $r){ + if(isset($existing_map[intval($r['di_uid'] ?? 0)])){ + $new_existing_rows[] = $r; + }else{ + $new_trial_rows[] = $r; + } + } + qry("START TRANSACTION"); /* ======================================================= A) 기존 daily_install 순서 업데이트 ======================================================= */ - if (is_array($existing_rows) && count($existing_rows)) { - foreach ($existing_rows as $idx => $row) { - $di_uid = intval($row['di_uid'] ?? 0); - $di_order_seq = intval($row['di_order_seq'] ?? 0); + foreach ($new_existing_rows as $row) { - if (!$di_uid || !$di_order_seq) { - qry("ROLLBACK"); - $fail('validate_existing_row', 'invalid existing row', ['row'=>$row]); - } + $di_uid = intval($row['di_uid'] ?? 0); + $di_order_seq = intval($row['di_order_seq'] ?? 0); - // driver/date까지 같이 조건으로 걸면 더 안전함 (다른 날짜/driver 수정 방지) - $sql = " - UPDATE tbl_daily_install - SET di_order_seq = '{$di_order_seq}' - WHERE di_uid = '{$di_uid}' + if (!$di_uid || !$di_order_seq) { + qry("ROLLBACK"); + $fail('validate_existing_row', 'invalid existing row', ['row'=>$row]); + } + + $sql = " + UPDATE tbl_daily_install + SET di_order_seq = '{$di_order_seq}' + WHERE di_uid = '{$di_uid}' AND di_driver_uid = '{$di_driver_uid}' AND di_install_date = '{$di_install_date}' - LIMIT 1 - "; + LIMIT 1 + "; - if (!qry($sql)) { - qry("ROLLBACK"); - $fail('update_existing_order', 'failed', ['di_uid'=>$di_uid]); - } + if (!qry($sql)) { + qry("ROLLBACK"); + $fail('update_existing_order', 'failed', ['di_uid'=>$di_uid]); } } /* ======================================================= - B) trial(waitlist) -> daily_install INSERT + status update + B) trial(waitlist) → INSERT ======================================================= */ - if (is_array($trial_rows) && count($trial_rows)) { - foreach ($trial_rows as $idx => $row) { + foreach ($new_trial_rows as $row) { - $iw_uid = intval($row['iw_uid'] ?? 0); - $di_order_seq = intval($row['di_order_seq'] ?? 0); + $iw_uid = intval($row['iw_uid'] ?? 0); + $di_order_seq = intval($row['di_order_seq'] ?? 0); - if (!$iw_uid || !$di_order_seq) { - qry("ROLLBACK"); - $fail('validate_trial_row', 'invalid trial row', ['row'=>$row]); - } + if (!$iw_uid || !$di_order_seq) { + qry("ROLLBACK"); + $fail('validate_trial_row', 'invalid trial row', ['row'=>$row]); + } - $di_uid = $this->insert_daily_install_from_waitlist( - $iw_uid, - $di_driver_uid, - $di_install_date, - $di_order_seq - ); + $di_uid = $this->insert_daily_install_from_waitlist( + $iw_uid, + $di_driver_uid, + $di_install_date, + $di_order_seq + ); - if (!$di_uid) { - qry("ROLLBACK"); - $fail('insert_daily_install', 'failed', ['iw_uid'=>$iw_uid]); - } + if (!$di_uid) { + qry("ROLLBACK"); + $fail('insert_daily_install', 'failed', ['iw_uid'=>$iw_uid]); + } - if (!$this->insert_daily_install_container_from_waitlist($di_uid, $iw_uid)) { - qry("ROLLBACK"); - $fail('insert_container', 'failed', ['iw_uid'=>$iw_uid]); - } + if (!$this->insert_daily_install_container_from_waitlist($di_uid, $iw_uid)) { + qry("ROLLBACK"); + $fail('insert_container', 'failed', ['iw_uid'=>$iw_uid]); + } - if (!$this->update_waitlist_status($iw_uid, 'ASSIGNED')) { - qry("ROLLBACK"); - $fail('update_waitlist', 'failed', ['iw_uid'=>$iw_uid]); - } + if (!$this->update_waitlist_status($iw_uid, 'ASSIGNED')) { + qry("ROLLBACK"); + $fail('update_waitlist', 'failed', ['iw_uid'=>$iw_uid]); } } @@ -771,12 +848,23 @@ trait InstallAPI { public function unassign_daily_install() { - $di_uid = intval($_POST['di_uid'] ?? 0); - if (!$di_uid) { - echo $this->json(['ok'=>0,'msg'=>'invalid di_uid']); + $di_uids = $_POST['di_uids'] ?? []; + if (!is_array($di_uids) || !count($di_uids)) { + echo $this->json(['ok'=>0,'msg'=>'invalid di_uids']); return; } + // 숫자만 필터 + $di_uids = array_map('intval', $di_uids); + $di_uids = array_filter($di_uids); + + if (!count($di_uids)) { + echo $this->json(['ok'=>0,'msg'=>'empty di_uids']); + return; + } + + $in = implode(',', $di_uids); + $fail = function($stage, $msg, $extra = []) { echo $this->json(array_merge([ 'ok' => 0, @@ -787,45 +875,70 @@ trait InstallAPI { exit; }; - $qr = qry("SELECT di_wait_uid, di_driver_uid, di_install_date FROM tbl_daily_install WHERE di_uid = '{$di_uid}'"); - $row = fetch_array($qr); + // 1. 대상 조회 + $qr = qry(" + SELECT di_uid, di_wait_uid, di_driver_uid, di_install_date + FROM tbl_daily_install + WHERE di_uid IN ($in) + "); - if (empty($row['di_wait_uid'])) { - $fail('select', 'daily_install not found', ['di_uid'=>$di_uid]); + $rows = []; + while ($r = fetch_array($qr)) { + $rows[] = $r; } - $iw_uid = intval($row['di_wait_uid']); - $di_driver_uid = intval($row['di_driver_uid']); - $di_install_date = $row['di_install_date']; // ✅ 문자열 유지 + if (!count($rows)) { + $fail('select', 'no rows found'); + } qry("START TRANSACTION"); - // container 삭제는 하지말자. - // if (qry("DELETE FROM tbl_daily_install_container WHERE dic_di_uid = '{$di_uid}'") === false) { - // qry("ROLLBACK"); - // $fail('delete_container', 'failed', ['di_uid'=>$di_uid]); - // } - - // daily_install 삭제 - if (!qry("DELETE FROM tbl_daily_install WHERE di_uid = '{$di_uid}' LIMIT 1")) { + // 2. 삭제 + if (!qry("DELETE FROM tbl_daily_install WHERE di_uid IN ($in)")) { qry("ROLLBACK"); - $fail('delete_daily_install', 'failed', ['di_uid'=>$di_uid]); + $fail('delete_daily_install', 'failed'); } - // waitlist WAITING 복귀 - if (!$this->update_waitlist_status($iw_uid, 'WAITING')) { - qry("ROLLBACK"); - $fail('update_waitlist', 'failed', ['iw_uid'=>$iw_uid]); + // 3. waitlist 복귀 (중복 제거) + $iw_uids = []; + foreach ($rows as $r) { + if (!empty($r['di_wait_uid'])) { + $iw_uids[] = intval($r['di_wait_uid']); + } + } + $iw_uids = array_unique($iw_uids); + + foreach ($iw_uids as $iw_uid) { + if (!$this->update_waitlist_status($iw_uid, 'WAITING')) { + qry("ROLLBACK"); + $fail('update_waitlist', 'failed', ['iw_uid'=>$iw_uid]); + } } - // seq 재정렬 (같은 driver/date) - if (!$this->resequence_daily_install($di_driver_uid, $di_install_date)) { - qry("ROLLBACK"); - $fail('resequence', 'failed', ['di_driver_uid'=>$di_driver_uid,'di_install_date'=>$di_install_date]); + // 4. resequence (driver + date별 그룹) + $group = []; + + foreach ($rows as $r) { + $key = $r['di_driver_uid'].'|'.$r['di_install_date']; + $group[$key] = [ + 'driver_uid' => $r['di_driver_uid'], + 'date' => $r['di_install_date'] + ]; + } + + foreach ($group as $g) { + if (!$this->resequence_daily_install($g['driver_uid'], $g['date'])) { + qry("ROLLBACK"); + $fail('resequence', 'failed', $g); + } } qry("COMMIT"); - echo $this->json(['ok'=>1]); + + echo $this->json([ + 'ok' => 1, + 'cnt' => count($rows) + ]); } public function resequence_daily_install($di_driver_uid, $di_install_date) { @@ -916,28 +1029,28 @@ trait InstallAPI { $c_uid = intval($_POST['c_uid'] ?? 0); $type = addslashes($_POST['type'] ?? ''); - $capacity = $_POST['capacity'] !== '' ? intval($_POST['capacity']) : "NULL"; + if( $mode != 'PICKUP' ) $capacity = $_POST['capacity'] !== '' ? intval($_POST['capacity']) : "NULL"; $is_used = $_POST['is_used'] ?? 'N'; $has_lock = $_POST['has_lock'] ?? 'N'; $has_wheel = $_POST['has_wheel'] ?? 'N'; $has_frame = $_POST['has_frame'] ?? 'N'; $owner_type = $_POST['owner_type'] ?? 'C'; $install_date = $_POST['install_date'] ?? null; - $remove_date = $_POST['remove_date'] ?? null; + $pickup_date = $_POST['pickup_date'] ?? null; $install_note = addslashes($_POST['install_note'] ?? ''); - $remove_note = addslashes($_POST['remove_note'] ?? ''); + $pickup_note = addslashes($_POST['pickup_note'] ?? ''); if($mode==='ADD'){ $sql = " INSERT INTO tbl_customer_container (cc_customer_uid,cc_type,cc_capacity,cc_is_used, cc_has_lock,cc_has_wheel,cc_has_woodframe,cc_owner_type, - cc_status,cc_install_at,cc_install_note, + cc_status, cc_install_date, cc_install_at,cc_install_note, cc_createddate,cc_updateddate) VALUES( '{$c_uid}','{$type}',{$capacity}, '{$is_used}','{$has_lock}','{$has_wheel}','{$has_frame}','{$owner_type}', - 'A','{$install_date}','{$install_note}', + 'A','{$install_date}','{$install_date}','{$install_note}', DATE_FORMAT(NOW(),'%Y%m%d%H%i%s'), DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') ) @@ -958,22 +1071,24 @@ trait InstallAPI { $status = $row['cc_status']; // 날짜 처리 (빈 값이면 NULL) $install_at_sql = $install_date ? "'{$install_date} 00:00:00'" : "NULL"; - $remove_at_sql = $remove_date ? "'{$remove_date} 00:00:00'" : "NULL"; + $pickup_at_sql = $pickup_date ? "'{$pickup_date} 00:00:00'" : "NULL"; // ===== ACTIVE 수정 ===== if ($status === 'A') { $sql = " - UPDATE tbl_customer_container SET - cc_capacity={$capacity}, - cc_is_used='{$is_used}', - cc_has_lock='{$has_lock}', - cc_has_wheel='{$has_wheel}', - cc_has_woodframe='{$has_frame}', - cc_owner_type='{$owner_type}', - cc_install_at={$install_at_sql}, - cc_install_note='{$install_note}', - cc_modified_at=NOW(), - cc_updateddate=DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') + UPDATE tbl_customer_container + SET + cc_capacity={$capacity}, + cc_is_used='{$is_used}', + cc_has_lock='{$has_lock}', + cc_has_wheel='{$has_wheel}', + cc_has_woodframe='{$has_frame}', + cc_owner_type='{$owner_type}', + cc_install_date={$install_date}, + cc_install_at={$install_at_sql}, + cc_install_note='{$install_note}', + cc_modified_at=NOW(), + cc_updateddate=DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') WHERE cc_uid='{$cc_uid}' "; qry($sql); @@ -981,25 +1096,27 @@ trait InstallAPI { // ===== INACTIVE 수정 ===== else { $sql = " - UPDATE tbl_customer_container SET - cc_remove_at={$remove_at_sql}, - cc_remove_note='{$remove_note}', - cc_modified_at=NOW(), - cc_updateddate=DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') + UPDATE tbl_customer_container + SET + cc_pickup_at={$pickup_at_sql}, + cc_pickup_note='{$pickup_note}', + cc_modified_at=NOW(), + cc_updateddate=DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') WHERE cc_uid='{$cc_uid}' "; qry($sql); } } - if($mode==='REMOVE'){ + if($mode==='PICKUP'){ $sql=" - UPDATE tbl_customer_container SET - cc_status='I', - cc_remove_date='{$remove_date}', - cc_remove_note='{$remove_note}', - cc_remove_at=NOW(), - cc_updateddate=DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') + UPDATE tbl_customer_container + SET + cc_status='I', + cc_pickup_date='{$pickup_date}', + cc_pickup_note='{$pickup_note}', + cc_pickup_at=NOW(), + cc_updateddate=DATE_FORMAT(NOW(),'%Y%m%d%H%i%s') WHERE cc_uid='{$cc_uid}' "; qry($sql); @@ -1025,19 +1142,19 @@ trait InstallAPI { } } - if ($remove_date) { + if ($pickup_date) { - $remove_yyyymmdd = str_replace('-', '', $remove_date); + $pickup_yyyymmdd = str_replace('-', '', $pickup_date); $r = qry("SELECT c_removaldate FROM tbl_customer WHERE c_uid='{$c_uid}'"); $row = mysqli_fetch_assoc($r); $current = $row['c_removaldate']; - if (!$current || $current === 'N/A' || $remove_yyyymmdd > $current) { + if (!$current || $current === 'N/A' || $pickup_yyyymmdd > $current) { qry(" UPDATE tbl_customer - SET c_removaldate='{$remove_yyyymmdd}' + SET c_removaldate='{$pickup_yyyymmdd}' WHERE c_uid='{$c_uid}' "); } @@ -1050,11 +1167,13 @@ trait InstallAPI { // DAILY INSTALL public function daily_install_update_status() { - $di_uid = intval($_POST['di_uid'] ?? 0); - $member_uid = intval($_POST['member_uid'] ?? 0); - $newStatus = strtoupper(trim($_POST['status'] ?? '')); - $note = trim($_POST['install_note'] ?? ''); - $oil_qty = intval($_POST['oil_qty'] ?? 0); + $di_uid = intval($_POST['di_uid'] ?? 0); + $member_uid = intval($_POST['member_uid'] ?? 0); + $newStatus = strtoupper(trim($_POST['status'] ?? '')); + $install_note = trim($_POST['install_note'] ?? ''); + $additional_note = trim($_POST['additional_note'] ?? ''); + $oil_qty = intval($_POST['oil_qty'] ?? 0); + $sludge = intval($_POST['sludge'] ?? 0); $allowed = ['P','M','X']; @@ -1089,18 +1208,24 @@ trait InstallAPI { try { // 2) 상태 업데이트 - $this->updateDailyInstallStatus($di_uid, $newStatus, $note, $oil_qty); + $this->updateDailyInstallStatus($di_uid, $newStatus, $install_note, $oil_qty, $sludge); + + // 2-1) additional note 저장 + $this->upsertAdditionalNote($di, $additional_note); // 3) 컨테이너 반영 if ($di['di_status'] === 'A' && $newStatus === 'P') { - $this->apply_container_changes_for_di($di_uid, intval($di['di_customer_uid'])); + $this->apply_container_changes_for_di($di_uid, $install_note, intval($di['di_customer_uid'])); + + // 3-1) 컨테이너 반영 후 customer 의 main 볼륨이랑 main 컨테이너 변경 + $this->updateCustomerMainContainer(intval($di['di_customer_uid'])); } // 4) oil pickup → daily - $this->saveDailyFromInstallIfNeeded($di_uid, $oil_qty); + $this->saveDailyFromInstallIfNeeded($di_uid, $oil_qty, $sludge); // 5) waitlist 처리 - $this->handleWaitlistAfterInstall($di, $newStatus, $member_uid, $note); + $this->handleWaitlistAfterInstall($di, $newStatus, $member_uid, $install_note); // 6) 순서 재정렬 $this->reorder_daily_install_sequence( @@ -1121,15 +1246,17 @@ trait InstallAPI { private function updateDailyInstallStatus( int $di_uid, string $newStatus, - string $note, - int $oil_qty + string $install_note, + int $oil_qty, + int $sludge ) { $conn = $GLOBALS['conn']; $newStatus = mysqli_real_escape_string($conn, $newStatus); - $noteEsc = mysqli_real_escape_string($conn, $note); + $noteEsc = mysqli_real_escape_string($conn, $install_note); $oil_qty = intval($oil_qty); + $sludge = intval($sludge); $sql = " UPDATE tbl_daily_install @@ -1137,6 +1264,7 @@ trait InstallAPI { di_status = '{$newStatus}', di_install_note = '{$noteEsc}', di_oil_pickup = '{$oil_qty}', + di_sludge = '{$sludge}', di_install_time = IF('{$newStatus}' <> 'A', CURTIME(), di_install_time) WHERE di_uid = '{$di_uid}' LIMIT 1 @@ -1147,18 +1275,166 @@ trait InstallAPI { } } - private function saveDailyFromInstallIfNeeded(int $di_uid, int $oil_qty): void + /** + * install 추가 메모를 tbl_note에 upsert + * + * install 하면서 컨테이너 위치관련 note를 n_dailyuid 를 'i_{di_uid}' 형태로 저장 + * 예: di_uid = 123 이면 n_dailyuid = 'i_123' + */ + private function upsertAdditionalNote(array $di, string $additional_note): void + { + $connect = $GLOBALS['conn']; + // 디버그 + // file_put_contents(__DIR__.'/debug.log', "NOTE: ".$additional_note."\n", FILE_APPEND); + $noteText = trim($additional_note); + if (strlen($noteText) <= 1) { + return; + } + + $di_uid = intval($di['di_uid'] ?? 0); + if ($di_uid <= 0) { + return; + } + + // install 전용 note key + $noteDailyUid = 'i_' . $di_uid; + + $n_dailyuid = mysqli_real_escape_string($connect, $noteDailyUid); + $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, $noteText); + $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('install 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('install 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('install note insert failed'); + } + } + } + + private function saveDailyFromInstallIfNeeded(int $di_uid, int $oil_qty, int $sludge): void { - // 0이면 daily에 반영할 필요 없음 if ($oil_qty <= 0) return; - // payload 만들기 - $payload = $this->buildDailyPayloadFromInstall($di_uid, $oil_qty); + $qr = qry("SELECT * FROM tbl_daily_install WHERE di_uid='{$di_uid}' LIMIT 1"); + $di = mysqli_fetch_assoc($qr); + if (!$di) throw new Exception('install not found'); - // ************************************************* payload 에 아래 포함시켜라. - // $payload['d_jobtype'] = $payload['d_jobtype'] ?? 'INSTALL'; + $visitdate = date('Ymd', strtotime($di['di_install_date'])); + $now14 = date('YmdHis'); - $this->saveDaily($payload, ['in_tx' => 1]); + // customer account + $qrCus = qry(" + SELECT + c_accountno, + c_regionuid, + c_region + FROM tbl_customer + WHERE c_uid='{$di['di_customer_uid']}' + LIMIT 1 + "); + $customer = mysqli_fetch_assoc($qrCus); + + if (!$customer) { + throw new Exception('customer not found'); + } + + // ========================= + // payload (saveInput 스타일) + // ========================= + $payload = [ + 'd_customeruid' => $di['di_customer_uid'], + 'd_accountno' => $customer['c_accountno'], + 'd_orderdate' => $visitdate, + 'd_driveruid' => $_SESSION['ss_UID'] ?? 0, // member uid + 'd_druid' => 11, // driver uid 고정 hard coding + 'd_drinitial' => 'I.N', // driver initial 고정 hard coding + 'd_regionuid' => $customer['c_regionuid'] ?? 0, + 'd_region' => $customer['c_region'] ?? '', + 'd_visit' => 'Y', + 'd_visitdate' => $visitdate . '000000', + 'd_status' => 'F', + 'd_jobtype' => 'INSTALL', + 'd_quantity' => $oil_qty, + 'd_sludge' => $sludge, + 'd_inputdate' => $now14, + 'd_is_transfer' => 'N', + 'd_level' => $_SESSION['ss_LEVEL'] ?? '', + 'd_createruid' => $_SESSION['ss_UID'] ?? '', + 'd_createddate' => $now14, + ]; + + // ========================= + // 기존 daily 찾기 (중복 방지) + // ========================= + $opt = [ + 'allow_update' => true, + 'old_key' => [ + 'd_orderdate' => $visitdate, + 'd_accountno' => $customer['c_accountno'], + 'd_jobtype' => 'INSTALL' + ] + ]; + + $this->dailyService->saveDaily($payload, $opt); } private function handleWaitlistAfterInstall(array $di, string $newStatus, int $member_uid, string $note) @@ -1257,6 +1533,8 @@ trait InstallAPI { iw_address, iw_city, iw_postal, + iw_lat, + iw_lng, iw_requested_due_date, iw_work_type, iw_status, @@ -1265,8 +1543,12 @@ trait InstallAPI { iw_scheduled, iw_scheduled_cycle, iw_request_by, + iw_request_at, iw_request_note, iw_cs_note, + iw_work_note, + iw_required_person_cnt, + iw_multi_person_reason, iw_request_source ) VALUES ( '{$wait['iw_customer_uid']}', @@ -1276,6 +1558,8 @@ trait InstallAPI { '".mysqli_real_escape_string($GLOBALS['conn'], $wait['iw_address'])."', '".mysqli_real_escape_string($GLOBALS['conn'], $wait['iw_city'])."', '".mysqli_real_escape_string($GLOBALS['conn'], $wait['iw_postal'])."', + '{$wait['iw_lat']}', + '{$wait['iw_lng']}', '{$nextDate}', '".mysqli_real_escape_string($GLOBALS['conn'], $wait['iw_work_type'])."', 'WAITING', @@ -1284,8 +1568,12 @@ trait InstallAPI { 1, '{$cycleDays}', '{$wait['iw_request_by']}', + '{$installDate}', '".mysqli_real_escape_string($GLOBALS['conn'], (string)$wait['iw_request_note'])."', '".mysqli_real_escape_string($GLOBALS['conn'], (string)$wait['iw_cs_note'])."', + '".mysqli_real_escape_string($GLOBALS['conn'], (string)$wait['iw_work_note'])."', + '".mysqli_real_escape_string($GLOBALS['conn'], (string)$wait['iw_required_person_cnt'])."', + '".mysqli_real_escape_string($GLOBALS['conn'], (string)$wait['iw_multi_person_reason'])."', 'AUTO' ) "; @@ -1339,63 +1627,12 @@ trait InstallAPI { } } - private function buildDailyPayloadFromInstall(int $di_uid, int $oil_qty): array - { - $qr = qry("SELECT * FROM tbl_daily_install WHERE di_uid='{$di_uid}' LIMIT 1"); - $di = mysqli_fetch_assoc($qr); - if (!$di) throw new Exception('install not found'); - - $orderdate = date('Ymd', strtotime($di['di_install_date'])); - $now14 = date('YmdHis'); - - $qrCus = qry("SELECT * FROM tbl_customer WHERE c_uid='{$di['di_customer_uid']}' LIMIT 1"); - $customer = mysqli_fetch_assoc($qrCus); - if (!$customer) throw new Exception('customer not found'); - - $days = 0; - if (!empty($customer['c_lastpickupdate'])) { - $last = strtotime(substr($customer['c_lastpickupdate'],0,8)); - $today = strtotime($orderdate); - $days = max(0, floor(($today-$last)/86400)); - } - - $estquantity = max( - 0, - ($customer['c_fullquantity'] - $customer['c_fullquantitydaily']) - + ($days * $customer['c_fullquantitydaily']) - ); - - return [ - 'd_orderdate' => $orderdate, - 'd_accountno' => $di['di_accountno'], - 'd_customeruid' => $di['di_customer_uid'], - // 'd_driveruid' => $di['di_driver_uid'], - 'd_driveruid' => 43, // 현재 E.X 로 고정, hard coding - 'd_name' => $di['di_customer_name'], - 'd_paymenttype' => $customer['c_paymenttype'], - 'd_cycle' => $customer['c_paymentcycle'], - 'd_rate' => $customer['c_rate'], - 'd_status' => 'F', - 'd_visit' => 'Y', - 'd_visitdate' => $now14, - 'd_quantity' => $oil_qty, - 'd_estquantity' => $estquantity, - 'd_inputdate' => $now14, - 'd_createddate' => $now14, - 'd_createruid' => $_SESSION['ss_UID'] ?? '' - ]; - } - - - - - - public function upload_after_photo() { $di_uid = intval($_POST['di_uid'] ?? 0); $member_uid = intval($_POST['member_uid'] ?? 0); $note = trim($_POST['note'] ?? ''); + $i_type = $_POST['i_type'] ?? 'install'; $fail = function($stage, $msg, $extra = []) { echo $this->json(array_merge([ @@ -1423,17 +1660,17 @@ trait InstallAPI { $fail('select_di', 'daily_install not found'); } - if(!isset($_FILES['after_photo']) || $_FILES['after_photo']['error'] !== 0){ + if(!isset($_FILES['photo']) || $_FILES['photo']['error'] !== 0){ $fail('file', 'no file'); } + $file = $_FILES['photo']; $customer_uid = intval($di['di_customer_uid']); - $file = $_FILES['after_photo']; $ok = $this->save_customer_image_single( $customer_uid, $member_uid, - 'install', + $i_type, $di_uid, // i_sourceuid = di_uid $note, $file @@ -1466,14 +1703,13 @@ trait InstallAPI { return false; } - // 작업 단위 1장 유지: (customeruid + type + sourceuid) + // 작업 단위 1장 유지: (customeruid + type, sourceuid 는 제외) $qry_update = " UPDATE tbl_customer_image SET i_status = 'I' WHERE i_customeruid = '{$i_customeruid}' - AND i_type = '{$i_type}' - AND i_sourceuid = '{$i_sourceuid}' - AND i_status = 'A' + AND i_type = '{$i_type}' + AND i_status = 'A' "; if(qry($qry_update) === false) return false; @@ -1483,10 +1719,10 @@ trait InstallAPI { mkdir($upload_folder, 0777, true); } - $newFileName = "IMG_".$i_customeruid."_".time().rand(1000,9999)."_0.".$ext; + $newFileName = "IMG_".$i_customeruid."_".time().rand(1000,9999)."_0.jpg"; $savePath = $upload_folder . "/" . $newFileName; - if(!move_uploaded_file($file['tmp_name'], $savePath)){ + if (!$this->compressAndResizeImage($file['tmp_name'], $savePath, $ext, 1200, 75)) { return false; } @@ -1521,7 +1757,7 @@ trait InstallAPI { return qry($sqlIns) !== false; } - private function apply_container_changes_for_di($di_uid, $customer_uid){ + private function apply_container_changes_for_di($di_uid, $note, $customer_uid){ $containers = []; $rc = qry("SELECT * FROM tbl_daily_install_container WHERE dic_di_uid = '{$di_uid}'"); @@ -1550,29 +1786,23 @@ trait InstallAPI { $has_wheel = mysqli_real_escape_string($GLOBALS['conn'], $c['dic_has_wheel'] ?? 'N'); $has_wood = mysqli_real_escape_string($GLOBALS['conn'], $c['dic_has_woodframe'] ?? 'N'); - // REMOVE - if($action === 'REMOVE'){ - - $removeNote = mysqli_real_escape_string( - $GLOBALS['conn'], - "Removed via DI#{$di_uid}" - ); - + // PICKUP + if($action === 'PICKUP'){ qry(" UPDATE tbl_customer_container SET cc_status='I', - cc_remove_note='{$removeNote}', - cc_remove_at=NOW(), - cc_remove_date=NOW(), + cc_pickup_note='{$note}', + cc_pickup_at=NOW(), + cc_pickup_date=NOW(), cc_modified_at=NOW() WHERE cc_uid = ( SELECT cc_uid FROM ( SELECT cc_uid FROM tbl_customer_container WHERE cc_customer_uid='{$customer_uid}' - AND cc_status='A' - AND cc_type='{$type}' - AND (cc_capacity {$capacityCond}) + AND cc_status='A' + AND cc_type='{$type}' + AND (cc_capacity {$capacityCond}) ORDER BY IFNULL(cc_install_at,'1970-01-01'), cc_uid LIMIT 1 ) x @@ -1594,24 +1824,230 @@ trait InstallAPI { cc_status, cc_install_note, cc_install_at, cc_install_date, - cc_createddate, cc_updateddate, - cc_modified_at + cc_createddate, cc_updateddate ) VALUES ( '{$customer_uid}', '{$type}', {$capacityVal}, '{$owner_type}', '{$is_used}', '{$has_lock}', '{$has_wheel}', '{$has_wood}', 'A', - 'Installed via DI#{$di_uid}', + '{$note}', NOW(), NOW(), - '{$nowYmdHis}', '{$nowYmdHis}', - NOW() + NOW(), NOW() ) "); } } } + private function updateCustomerMainContainer($customer_uid) + { + $customer_uid = intval($customer_uid); + + // ========================================= + // 0) 수동 관리 + // ========================================= + $qr = qry(" + SELECT + c_manualvolume, + c_fullcycleflag, + c_fullcycleforced, + c_mainvolume + FROM tbl_customer + WHERE c_uid = '{$customer_uid}' + LIMIT 1 + "); + $cus = mysqli_fetch_assoc($qr); + + if (!$cus) { + throw new Exception('customer not found'); + } + + if ($cus['c_manualvolume'] === 'Y') { + // 아무것도 하지 않음 + return; + } + // 기존 mainvolume 저장 + $oldVolume = intval($cus['c_mainvolume'] ?? 0); + + // ========================================= + // 1) active container 조회 + // ========================================= + $rc = qry(" + SELECT + cc_type, + cc_capacity, + cc_owner_type + FROM tbl_customer_container + WHERE cc_customer_uid = '{$customer_uid}' + AND cc_status = 'A' + "); + + $containers = []; + while ($row = mysqli_fetch_assoc($rc)) { + $containers[] = $row; + } + + // ========================================= + // 2) 컨테이너 없으면 초기화 + // ========================================= + if (!count($containers)) { + + qry(" + UPDATE tbl_customer + SET + c_mainvolume = 0, + c_maincontainer = '' + WHERE c_uid = '{$customer_uid}' + LIMIT 1 + "); + + return; + } + + // ========================================= + // 3) 타입 분류 (D / B) + // ========================================= + $d_list = []; // D 계열 + $b_list = []; // B 계열 + + foreach ($containers as $c) { + + $type = strtoupper(trim($c['cc_type'] ?? '')); + $capacity = intval($c['cc_capacity'] ?? 0); // null / '' → 0 처리 + + // B 타입 판별 400B, 600B, CB 등 + if (preg_match('/^\d*B$/', $type) || $type === 'CB') { + + $b_list[] = [ + 'type' => $type, + 'capacity' => $capacity + ]; + } + + // D 타입 판별 + else if ($type === 'D') { + + $d_list[] = [ + 'type' => $type, + 'capacity' => 200 // D는 고정 200 + ]; + } + + // BUCKET / TANK 등은 무시 + } + + // ========================================= + // 4) B가 하나라도 있으면 → B 기준 처리 + // ========================================= + if (count($b_list) > 0) { + + $maxCapacity = 0; + $maxType = ''; + + foreach ($b_list as $b) { + + if ($b['capacity'] > $maxCapacity) { + $maxCapacity = $b['capacity']; + $maxType = $b['type']; + } + } + + // capacity가 전부 0일 수도 있음 (예외 처리) + if ($maxCapacity <= 0) { + $maxType = $b_list[0]['type']; // fallback + } + + // forced cycle + $newVolume = $maxCapacity; + $this->adjustFullCycle($customer_uid, $cus, $oldVolume, $newVolume); + + // main volume + qry(" + UPDATE tbl_customer + SET + c_mainvolume = '{$maxCapacity}', + c_maincontainer = '{$maxType}' + WHERE c_uid = '{$customer_uid}' + LIMIT 1 + "); + + return; + } + + // ========================================= + // 5) B 없고 D만 있는 경우 + // ========================================= + if (count($d_list) > 0) { + + $count = count($d_list); + + if ($count === 1) { + $volume = 200; + } else { + $volume = $count * 200; + } + + // forced cycle + $newVolume = $volume; + $this->adjustFullCycle($customer_uid, $cus, $oldVolume, $newVolume); + + // main volume + qry(" + UPDATE tbl_customer + SET + c_mainvolume = '{$volume}', + c_maincontainer = 'D' + WHERE c_uid = '{$customer_uid}' + LIMIT 1 + "); + + return; + } + + // ========================================= + // 6) fallback (이상 케이스) + // ========================================= + qry(" + UPDATE tbl_customer + SET + c_mainvolume = 0, + c_maincontainer = '' + WHERE c_uid = '{$customer_uid}' + LIMIT 1 + "); + } + + private function adjustFullCycle($customer_uid, $cus, $oldVolume, $newVolume) + { + // 조건 체크 + if ($cus['c_manualvolume'] !== 'N') return; + if (intval($cus['c_fullcycleflag']) !== 1) return; + + $forced = floatval($cus['c_fullcycleforced'] ?? 0); + + // 기존 값이 없으면 의미 없음 + if ($forced <= 0) return; + + // oldVolume 0이면 비율 계산 불가 → skip + if ($oldVolume <= 0) return; + + // newVolume 0이면 forced도 0으로 + if ($newVolume <= 0) { + $newForced = 0; + } else { + $ratio = $newVolume / $oldVolume; + $newForced = $forced * $ratio; + } + + qry(" + UPDATE tbl_customer + SET c_fullcycleforced = '{$newForced}' + WHERE c_uid = '{$customer_uid}' + LIMIT 1 + "); + } + private function reorder_daily_install_sequence($driver_uid, $install_date) { $driver_uid = intval($driver_uid); @@ -1738,4 +2174,186 @@ trait InstallAPI { echo $this->json(['ok'=>1]); exit; } + + public function bulk_update_daily_install() { + + $di_uids = $_POST['di_uids'] ?? []; + $new_driver_uid = intval($_POST['di_driver_uid'] ?? 0); + $new_install_date = trim($_POST['di_install_date'] ?? ''); + + if (!is_array($di_uids) || !count($di_uids)) { + echo $this->json(['ok'=>0, 'msg'=>'invalid di_uids']); + return; + } + + $di_uids = array_map('intval', $di_uids); + $di_uids = array_filter($di_uids); + + if (!count($di_uids)) { + echo $this->json(['ok'=>0, 'msg'=>'empty di_uids']); + return; + } + + if (!$new_driver_uid || !$new_install_date) { + echo $this->json(['ok'=>0, 'msg'=>'invalid target params']); + return; + } + + $in = implode(',', $di_uids); + $conn = $GLOBALS['conn']; + + $fail = function($stage, $msg, $extra = []) use ($conn) { + echo $this->json(array_merge([ + 'ok' => 0, + 'stage' => $stage, + 'msg' => $msg, + 'db' => mysqli_error($conn) + ], $extra)); + exit; + }; + + // 1) 대상 조회 + $qr = qry(" + SELECT di_uid, di_driver_uid, di_install_date + FROM tbl_daily_install + WHERE di_uid IN ($in) + "); + + $rows = []; + while ($r = fetch_array($qr)) { + $rows[] = $r; + } + + if (!count($rows)) { + $fail('select', 'no rows found'); + } + + // 2) 기존 그룹 수집 + $oldGroups = []; + foreach ($rows as $r) { + $key = intval($r['di_driver_uid']) . '|' . $r['di_install_date']; + $oldGroups[$key] = [ + 'driver_uid' => intval($r['di_driver_uid']), + 'date' => $r['di_install_date'] + ]; + } + + qry("START TRANSACTION"); + + // 3) 대상 업데이트 + $new_driver_uid_esc = intval($new_driver_uid); + $new_install_date_esc = mysqli_real_escape_string($conn, $new_install_date); + + $sql = " + UPDATE tbl_daily_install + SET + di_driver_uid = '{$new_driver_uid_esc}', + di_install_date = '{$new_install_date_esc}' + WHERE di_uid IN ($in) + "; + + if (!qry($sql)) { + qry("ROLLBACK"); + $fail('update', 'bulk update failed'); + } + + // 4) 새 그룹 resequence + if (!$this->resequence_daily_install($new_driver_uid_esc, $new_install_date_esc)) { + qry("ROLLBACK"); + $fail('resequence_new', 'failed', [ + 'di_driver_uid' => $new_driver_uid_esc, + 'di_install_date' => $new_install_date_esc + ]); + } + + // 5) 이전 그룹 resequence + foreach ($oldGroups as $g) { + $sameGroup = + intval($g['driver_uid']) === $new_driver_uid_esc && + $g['date'] === $new_install_date_esc; + + if ($sameGroup) continue; + + if (!$this->resequence_daily_install($g['driver_uid'], $g['date'])) { + qry("ROLLBACK"); + $fail('resequence_old', 'failed', $g); + } + } + + qry("COMMIT"); + + echo $this->json([ + 'ok' => 1, + 'cnt' => count($rows) + ]); + } + + function compressAndResizeImage($srcPath, $destPath, $ext, $maxWidth = 1200, $quality = 75) { + + list($width, $height, $imageType) = getimagesize($srcPath); + + if (!$width || !$height) return false; + + switch ($imageType) { + case IMAGETYPE_JPEG: + $srcImg = imagecreatefromjpeg($srcPath); + + if (function_exists('exif_read_data')) { + $exif = @exif_read_data($srcPath); + + if (!empty($exif['Orientation'])) { + switch ($exif['Orientation']) { + case 3: + $srcImg = imagerotate($srcImg, 180, 0); + break; + case 6: + $srcImg = imagerotate($srcImg, -90, 0); + break; + case 8: + $srcImg = imagerotate($srcImg, 90, 0); + break; + } + } + + $width = imagesx($srcImg); + $height = imagesy($srcImg); + } + + break; + case IMAGETYPE_PNG: + $srcImg = imagecreatefrompng($srcPath); + break; + case IMAGETYPE_WEBP: + $srcImg = imagecreatefromwebp($srcPath); + break; + default: + return false; + } + + if (!$srcImg) return false; + + if ($width > $maxWidth) { + $newWidth = $maxWidth; + $newHeight = intval(($height / $width) * $newWidth); + } else { + $newWidth = $width; + $newHeight = $height; + } + + $dstImg = imagecreatetruecolor($newWidth, $newHeight); + + imagecopyresampled( + $dstImg, $srcImg, + 0, 0, 0, 0, + $newWidth, $newHeight, + $width, $height + ); + + $result = imagejpeg($dstImg, $destPath, $quality); + + imagedestroy($srcImg); + imagedestroy($dstImg); + + return $result; + } } diff --git a/public_html/assets/css/main.css b/public_html/assets/css/main.css index 704f774..bdbbfda 100644 --- a/public_html/assets/css/main.css +++ b/public_html/assets/css/main.css @@ -1815,6 +1815,491 @@ table.table-search-report .tb-list th { z-index: 999; } +/* Install wait list */ +.waitlist-scroll { + height: calc(100vh - 260px); /* header + search 고려 */ + overflow-y: auto; +} +.wait-card { + border-bottom: 1px solid #eee; + padding: 12px 16px; + cursor: pointer; +} +.wait-card:hover { + background: #f8f9fa; +} +.wait-card.trial-selected { + background: #fff8e1; /* 연한 노랑 */ + border-color: #ffc107; + box-shadow: 0 0 0 2px rgba(255,193,7,.25); +} +.container-chips { + cursor: pointer; + display: flex; + gap: 6px; + flex-wrap: wrap; +} +.container-chips .chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 12px; + line-height: 1.2; + padding: 3px 8px; + border-radius: 6px; + white-space: nowrap; + background: #e9ecef; + color: #333; +} +.container-chips .chip-company { + background: #e9ecef; + color: #333; +} +.container-chips .chip-restaurant { + background: #fff3cd; /* warning subtle */ + color: #664d03; + border: 1px solid #ffecb5; +} +.container-chips .chip-flag { + margin-left: 4px; + padding: 0 6px; + font-size: 11px; + font-weight: 700; + border-radius: 999px; + background: #ffe69c; + color: #664d03; +} +.container-chips .chip-task { + background: #dee2e6; + color: #212529; +} +.container-chips .chip i { + font-size: 13px; + line-height: 1; +} +.container-chips .chip-count { + margin-left: 4px; + font-size: 11px; + font-weight: 600; + color: #495057; +} +.note-line { + display: flex; + gap: 6px; + align-items: flex-start; +} +.note-label { + font-size: 12px; + font-weight: 600; + color: #666; + min-width: 58px; +} +.note-text { + font-size: 12px; + line-height: 1.3; +} +/*modal*/ +.modal-body { + overflow: hidden; + padding: 0; +} +.install-wait-viewport { + overflow: hidden; + width: 100%; +} +.install-wait-steps-wrapper { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 100%; + transition: transform 0.35s ease; +} +.install-wait-step { + padding: 1.25rem; + box-sizing: border-box; + display: flex; + flex-direction: column; +} +.install-wait-step > .step-content { + flex: 1; +} +.customer-row.selected { + background-color: #e7f1ff; + border-left: 4px solid #0d6efd; + padding-left: 0.75rem; +} +#btnStep1Next, +#btnStep2Next { + min-width: 48px; + transition: all 0.15s ease; +} +.iw-inline-ctrl { + height: 34px; +} +button.iw-inline-ctrl { + padding: 0 12px; + line-height: 1.2; +} +input.iw-inline-ctrl, +.iw-inline-ctrl input { + padding-top: 4px; + padding-bottom: 4px; + line-height: 1.2; +} +.input-group.iw-inline-ctrl { + display: flex; + align-items: stretch; +} +.input-group.iw-inline-ctrl .form-control { + height: 100%; +} +.iw-toggle-on { + background-color: #e7f1ff; + border-color: #0d6efd; + color: #0d6efd; +} +.iw-toggle-on:hover { + background-color: #dbe9ff; +} +.active-step { + background-color: #e7f1ff; + border-color: #0d6efd; + color: #0d6efd; +} +.active-step:hover { + background-color: #dbe9ff; + transform: translateX(2px); +} +.iw-job-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); /* 항상 한 줄 */ + gap: 0.5rem; +} +.iw-job-card { + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + padding: 0.6rem 0.5rem; + font-size: 0.9rem; + background: #fff; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.15s ease; +} +.iw-job-card i { + font-size: 1rem; +} +.iw-job-card:hover { + background: #f1f5ff; + border-color: #b6c8ff; +} +.iw-job-card.active { + background: #e7f1ff; + border-color: #0d6efd; + color: #0d6efd; +} +.iw-container-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 0.5rem; +} +.iw-container-card { + border: 1px solid #dee2e6; + padding: 0.5rem; + border-radius: 0.375rem; + cursor: pointer; + text-align: center; + font-size: 0.85rem; +} +.iw-container-card.active { + background: #e7f1ff; + border-color: #0d6efd; + color: #0d6efd; +} +/**/ +#iwScheduleDate, +#iwPhotos, +#iwRequestDate, +#iwRequestBy, +#iwSelectCapacity, +#iwRequestNote, +#iwCsNote, +#kwCustomer, +#iwMapDate +{ + margin-bottom: 0px !important; +} +#iwSelectCapacity { + width: 75px; +} +#iwRequestBy{ + padding-top: 0.875em; + padding-bottom: 0.875em; +} +.iw-action-row { + display: flex; + gap: 12px; + margin-bottom: 12px; +} +.iw-action-row .task-action-box { + flex: 1; +} +.iw-add-btn { + min-width: 70px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; +} +.iw-name, .customer-name { + display: inline-block; + max-width: 140px; /* 이름 최대 폭 */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; +} +.iw-header { + background-color: var(--color-primary); + color: var(--color-white); +} +.iw-ui-control { + height: 36px; + display: flex; + align-items: center; +} +.iw-checkbox-wrap { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + cursor: pointer; +} +.iw-checkbox-wrap input { + transform: scale(1.3); + cursor: pointer; +} +.task-action-box { + flex: 1; + padding: 8px; + border: 1px solid #dee2e6; + border-radius: 6px; + background: #f8f9fa; +} +.task-action-title { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 4px; +} +.task-action-title.pickup { + color: #dc3545; /* bootstrap danger */ +} +.task-action-title.install { + color: #198754; /* bootstrap success */ +} +@media (max-width: 768px) { + .task-actions { + flex-direction: column; + } +} +#filterTwoPerson.active { + background-color: #212529; + color: #fff; +} +.container-card { + border:1px solid #ddd; + border-radius:6px; + padding:10px; + margin-bottom:8px; + background:#fff; +} +.container-card.inactive { + background:#f7f7f7; + opacity:0.85; +} +#containerCardWrap { + display: grid; + gap: 10px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +@media (min-width: 1200px) { + #containerCardWrap { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} +@media (max-width: 768px) { + #containerCardWrap { grid-template-columns: repeat(1, minmax(0, 1fr)); } +} +.flag-marker { + position: relative; + padding: 4px 8px; + border-radius: 6px; + font-size: 13px; + font-weight: 700; + white-space: nowrap; + box-shadow: 0 2px 6px rgba(0,0,0,.25); +} +.flag-body { + display: flex; + align-items: center; + gap: 6px; +} +.flag-tail { + position: absolute; + left: 50%; + bottom: -8px; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-top: 8px solid; + border-top-color: var(--flag-bg); +} +.flag-order { + font-weight: 800; +} +.flag-icon { + font-size: 12px; +} +.install-map-legend{ + position:absolute; + top:12px; + right:12px; + z-index:10; + min-width:140px; + max-width:220px; + background:rgba(255,255,255,0.95); + border:1px solid #dcdcdc; + border-radius:10px; + box-shadow:0 2px 10px rgba(0,0,0,0.08); + padding:10px 12px; + font-size:13px; +} + +.install-map-legend-title{ + font-weight:700; + margin-bottom:8px; +} + +.install-map-legend-item{ + display:flex; + align-items:center; + gap:8px; + margin-bottom:6px; +} +.install-map-legend-item:last-child{ + margin-bottom:0; +} + +.install-map-legend-swatch{ + width:14px; + height:14px; + border-radius:50%; + flex:0 0 14px; + border:1px solid rgba(0,0,0,0.15); +} +.reorder-selected-row { + outline: 2px solid red; +} +.summary-bar { + position: relative; +} +.view-toggle { + display: flex; + border: 1px solid #ccc; + border-radius: 8px; + overflow: hidden; + background: #fff; + margin-left: 12px; +} +.view-toggle label { + margin: 0; + cursor: pointer; +} +.view-toggle input[type="radio"] { + display: none; +} +.view-toggle span { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 36px; + color: #666; + font-size: 18px; + background: #f1f1f1; +} +.view-toggle input[type="radio"]:checked + span { + background: #ffca1a; + color: #333; +} +tr.status-P { + background-color: #b9c2c9; + color: #343a42; + text-decoration: line-through; + opacity: 0.75; +} +tr.status-M { + background-color: #fff3cd; /* bootstrap warning light */ + border-left: 4px solid #ff9800; + text-decoration: line-through; + opacity: 0.75; +} +tr.status-X { + background-color: #f8d7da; /* bootstrap danger light */ + border-left: 4px solid #dc3545; + text-decoration: line-through; + opacity: 0.75; +} + +/* ============================ + photo + ============================ */ +.photo-card { + border: 1px solid #ddd; + border-radius: 10px; + padding: 10px; + background: #fafafa; + height: 100%; +} + +.photo-header { + font-weight: 600; + font-size: 13px; + margin-bottom: 6px; +} + +.photo-card.before .photo-header { + color: #6c757d; +} + +.photo-card.after .photo-header { + color: #0d6efd; +} + +.photo-body { + min-height: 80px; + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +/* 이미지 썸네일 */ +.photo-body img { + width: 60px; + height: 60px; + object-fit: cover; + border-radius: 6px; + border: 1px solid #ccc; +} + /* ============================ Segmented Radio Button ============================ */ @@ -1838,6 +2323,7 @@ table.table-search-report .tb-list th { .radio-segment span { display: block; + width: 100%; text-align: center; padding: 8px 0; background: #e7e7e7; @@ -1877,4 +2363,4 @@ table.table-search-report .tb-list th { .radio-segment label:last-child span { border-radius: 0 0 5px 5px; } -} \ No newline at end of file +} diff --git a/public_html/assets/css/mapMAPCSS.css b/public_html/assets/css/mapMAPCSS.css index 5f8f028..db68f2a 100644 --- a/public_html/assets/css/mapMAPCSS.css +++ b/public_html/assets/css/mapMAPCSS.css @@ -239,7 +239,9 @@ #map-modal-input .td-title-info, #map-modal-history .td-title-info, #map-modal-info .td-title-info, - #map-modal-note .td-title-info { + #map-modal-container-request .td-title-info, + #customerImageModal .td-title-info + { background-color: var(--color-sub); color: #fff; } @@ -430,7 +432,9 @@ #map-modal-history .modal-header, #map-modal-info .modal-header, #map-modal-note .modal-header, -#map-modal-input .modal-header{ +#map-modal-input .modal-header, +#map-modal-container-request .modal-header, +#customerImageModal .modal-header{ padding: 0px 15px !important; } @@ -438,7 +442,9 @@ #map-modal-history .modal-header h4, #map-modal-info .modal-header h4, #map-modal-note .modal-header h4, -#map-modal-input .modal-header h4{ +#map-modal-input .modal-header h4, +#map-modal-container-request .modal-header h4, +#customerImageModal .modal-header h4{ font-size: 18px !important; } @@ -446,7 +452,9 @@ #map-modal-input .modal-content, #map-modal-history .modal-content, #map-modal-info .modal-content, -#map-modal-note .modal-content{ +#map-modal-note .modal-content, +#map-modal-container-request .modal-content, +#customerImageModal .modal-content{ border-radius: 0.5em !important; } @@ -454,7 +462,9 @@ #map-modal-input, #map-modal-history, #map-modal-info, -#map-modal-note{ +#map-modal-note, +#map-modal-container-request, +#customerImageModal{ top: 55px; @@ -496,6 +506,12 @@ .btn-nearby-modal{ background-color: #e24400; } +.btn-container-request { + background-color: #af415c; +} +.btn-image-modal { + background-color: #4195af; +} .btn-info-modal{ background-color: var(--color-sub); } @@ -694,17 +710,17 @@ input#Liter{ } } -.map-grid-layout-col-3 { +.map-grid-layout-col-4 { max-width: 1000px; margin: 0 auto; display: grid; gap: .3rem; } @media (min-width: 300px) { - .map-grid-layout-col-3 { grid-template-columns: repeat(3, 1fr); } + .map-grid-layout-col-4 { grid-template-columns: repeat(4, 1fr); } } @media (max-width: 819px) { - .map-grid-layout-col-3 { + .map-grid-layout-col-4 { max-width: 1000px; margin: 0 auto; display: grid; diff --git a/public_html/assets/internal_api.php b/public_html/assets/internal_api.php index 7604ac5..b9a3ddc 100644 --- a/public_html/assets/internal_api.php +++ b/public_html/assets/internal_api.php @@ -1449,8 +1449,7 @@ class API extends CONF { WHERE td.d_customeruid = tc.c_uid AND td.d_customeruid='".$_POST['c_uid']."' - AND td.d_orderdate='".$_POST['orderdate']."' - AND td.d_jobtype ='".($_POST['jobtype'] ?? 'UCO')."'" + AND td.d_orderdate='".$_POST['orderdate']."'" ); $result = array(); diff --git a/public_html/doc/customer_detail.php b/public_html/doc/customer_detail.php index e549196..9870f7c 100644 --- a/public_html/doc/customer_detail.php +++ b/public_html/doc/customer_detail.php @@ -83,20 +83,37 @@ if ($mode == "update") { $c_form_euSTR = $func -> convertFormat ($c_form_eu, 3); $c_form_corsiaSTR = $func -> convertFormat ($c_form_corsia, 3); $c_contractdateSTR = $func -> convertFormat ($c_contractdate, 3); - if ($c_installdate != "N/A") $c_installdateSTR = $func -> convertFormat ($c_installdate, 3); - else $c_installdateSTR = "N/A"; $c_schedulebasicSTR = $func -> convertFormat ($c_schedulebasic, 3); $c_fpickupSTR = $func -> convertFormat ($c_fpickup, 3); - - if ($c_removaldate != "N/A") $c_removaldateSTR = $func -> convertFormat ($c_removaldate, 3); - else $c_removaldateSTR = "N/A"; + $c_lastpickupdateSTR = $func -> convertFormat ($c_lastpickupdate, 3); if ($c_inactivedate != "N/A") $c_inactivedateSTR = $func -> convertFormat ($c_inactivedate, 3); else $c_inactivedateSTR = "N/A"; if ($c_inactivedateSTR == "-") $c_inactivedateSTR = str_replace("-", "", $c_inactivedateSTR); - if ($c_exchangedate != "N/A") $c_exchangedateSTR = $func -> convertFormat ($c_exchangedate, 3); - else $c_exchangedateSTR = "N/A"; + if ($c_installdate != "N/A") $c_installdateSTR = $func -> convertFormat ($c_installdate, 3); + else $c_installdateSTR = "N/A"; + + // + function safeDate($val) { + if (!$val || $val === 'N/A') return null; + if (!preg_match('/^\d{8}$/', $val)) return null; + return $val; + } + + $dates = []; + $d1 = safeDate($c_installdate); + $d2 = safeDate($c_exchangedate); + $d3 = safeDate($c_removaldate); + + if ($d1) $dates[] = $d1; + if ($d2) $dates[] = $d2; + if ($d3) $dates[] = $d3; + + $lastWork = !empty($dates) ? max($dates) : null; + + $lastWorkSTR = $lastWork ? $func->convertFormat($lastWork, 3): 'N/A'; + // if ($c_switchformdate != "N/A") $c_switchformdateSTR = $func -> convertFormat ($c_switchformdate, 3); else $c_switchformdateSTR = "N/A"; @@ -105,6 +122,27 @@ if ($mode == "update") { if (is_numeric($c_salescommissiondate)) $c_salescommissiondateSTR = $func -> convertFormat ($c_salescommissiondate, 7); else $c_salescommissiondateSTR = $c_salescommissiondate; + + // =============================== + // Customer Containers + // =============================== + $isManualChecked = ($c_manualvolume == 'Y') ? 'checked' : ''; + + $containerList = []; + + $q2 = " + SELECT * + FROM tbl_customer_container + WHERE cc_customer_uid = '{$c_uid}' + ORDER BY cc_status, cc_uid DESC + "; + + $rlt = mysqli_query($jdb->DBConn, $q2); + + while ($row = mysqli_fetch_array($rlt, MYSQLI_ASSOC)) { + $containerList[] = $row; + } + // } else { $c_salescommissiondateSTR = "NOT YET"; @@ -277,7 +315,7 @@ if ($mode == "update") { } else { // 기존 daily 정보 조회 - $query = "SELECT * FROM tbl_daily WHERE d_customeruid = '$c_uid' AND d_orderdate = '".date("Ymd")."' AND d_jobtype = 'UCO'"; + $query = "SELECT * FROM tbl_daily WHERE d_customeruid = '$c_uid' AND d_orderdate = '".date("Ymd")."'"; $result = $jdb->fQuery($query, "fetch query error"); if (is_array($result)) { @@ -363,79 +401,73 @@ addLog ("add", "CUSTOMER DETAIL", "VIEW", $lguserid, $query, $c_uid); $(document).ready(function(){ $(function () { - $('#c_contractdate, #c_form_eu, #c_form_corsia, #c_installdate, #c_fpickup, #c_removaldate, #d_visitdate, #c_exchangedate, #c_inactivedate, #c_switchformdate').datepicker({ - - dateFormat: 'yy-mm-dd', - //minDate: "-10D", - //maxDate: "+1M", - //maxDate: "+1M +10D", - //showOn: "button", - //yearRange: '-50:+5', - buttonImage: "/images/cal_red.png", - //beforeShowDay: $.datepicker.noWeekends, - buttonImageOnly: true, - //showOn: "both", - changeMonth: true, - changeYear: true, - firstDay: 7 + $('#c_contractdate, #c_form_eu, #c_form_corsia, #c_fpickup, #c_removaldate, #d_visitdate, #c_inactivedate, #c_switchformdate').datepicker({ + dateFormat: 'yy-mm-dd', + buttonImage: "/images/cal_red.png", + buttonImageOnly: true, + changeMonth: true, + changeYear: true, + firstDay: 7 }); $('#c_salescommissiondate').datepicker({ - - dateFormat: 'yy-mm', - //minDate: "-10D", - //maxDate: "+1M", - //maxDate: "+1M +10D", - //showOn: "button", - //yearRange: '-50:+5', - buttonImage: "/images/cal_red.png", - //beforeShowDay: $.datepicker.noWeekends, - buttonImageOnly: true, - //showOn: "both", - changeMonth: true, - changeYear: true, - firstDay: 7 + dateFormat: 'yy-mm', + buttonImage: "/images/cal_red.png", + buttonImageOnly: true, + changeMonth: true, + changeYear: true, + firstDay: 7 }); - //var array = ["2023-05-27","2023-05-29"]; var array = []; - $('#r_requestdate').datepicker({ - - dateFormat: 'yy-mm-dd', - minDate: "0D", - //maxDate: "+5D", - //maxDate: "+1M +10D", - //showOn: "button", - //yearRange: '-50:+5', - buttonImage: "/images/cal_red.png", - //beforeShowDay: $.datepicker.noWeekends, - buttonImageOnly: true, - //showOn: "both", - changeMonth: true, - changeYear: true, - firstDay: 7, - beforeShowDay: function(date) { - // Sat, Sunday - // if(date.getDay()==6||date.getDay()==0) return [false,"","Not available"]; - - //if(date.getDay()==0) return [false,"","Not available"]; - - // Specific Date - if($.inArray($.datepicker.formatDate('yy-mm-dd', date ), array) > -1) - { - return [false,"","Not available"]; - } - else - { - return [true,'',"Available"]; - } - } + $('#r_requestdate').datepicker({ + dateFormat: 'yy-mm-dd', + minDate: "0D", + buttonImage: "/images/cal_red.png", + buttonImageOnly: true, + changeMonth: true, + changeYear: true, + firstDay: 7, + beforeShowDay: function(date) { + // Specific Date + if($.inArray($.datepicker.formatDate('yy-mm-dd', date ), array) > -1) return [false,"","Not available"]; + else return [true,'',"Available"]; + } }); }); +$('#containerModal').on('shown.bs.modal', function () { + + const $inputs = $('#cm_install_date, #cm_pickup_date'); + + $inputs.each(function(){ + if ($(this).hasClass('hasDatepicker')) { + $(this).datepicker('destroy'); + } + }); + + $inputs.datepicker({ + dateFormat: 'yy-mm-dd', + changeMonth: true, + changeYear: true, + firstDay: 7, + beforeShow: function(input, inst) { + setTimeout(function(){ + const rect = input.getBoundingClientRect(); + + inst.dpDiv.css({ + position: 'fixed', // + top: rect.bottom + 'px', // viewport 기준 + left: rect.left + 'px', + zIndex: 2147483647 + }); + }, 0); + } + }); + +}); }); - - - - + + @@ -1046,9 +1331,9 @@ $(document).ready(function()
| Main Container | ++ + | + +Main Volume
+ Manual >
+ |
+ + + | +
| First Install Date | ++ + | + +Last Container Work Date | ++ + | +
| Container Show Inactive
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No containers found.
+
+
+
+
+
+
+
+
+
+
+
+ |
+ ||
ACTUAL : =number_format($d_quantitySUM)?> L