- [INSTALL WAIT LIST] 카테고리 적용 가능하도록 수정.
- [INSTALL ORDER] Install / Pickup 칸 분리.
This commit is contained in:
Hyojin Ahn 2026-05-07 08:09:46 -04:00
parent d242cf7fc6
commit 0833b3e460
6 changed files with 278 additions and 82 deletions

View File

@ -446,6 +446,7 @@ trait InstallAPI {
$person_cnt = $_POST['person_cnt'] ?? null; // null or 2
$person_reason= $_POST['person_reason'] ?? null;
$category = intval($_POST['category'] ?? 0);
$scheduled = intval($_POST['scheduled'] ?? 0);
$cycle = intval($_POST['scheduled_cycle'] ?? 0);
@ -480,6 +481,7 @@ trait InstallAPI {
iw_scheduled = '{$scheduled}',
iw_scheduled_cycle = '{$cycle}',
iw_need_oil_pickup = '{$need_oil}',
iw_category = '{$category}',
iw_status = 'WAITING'
WHERE iw_uid = '{$iw_uid}'
@ -1586,6 +1588,7 @@ trait InstallAPI {
iw_work_type,
iw_status,
iw_priority,
iw_category,
iw_created_by,
iw_scheduled,
iw_scheduled_cycle,
@ -1611,6 +1614,7 @@ trait InstallAPI {
'".mysqli_real_escape_string($GLOBALS['conn'], $wait['iw_work_type'])."',
'WAITING',
'{$wait['iw_priority']}',
'{$wait['iw_category']}',
'{$wait['iw_created_by']}',
1,
'{$cycleDays}',

View File

@ -2363,6 +2363,61 @@ tr.status-X {
background: #ffca1a;
color: #333;
}
.iw-cat-box {
display:flex;
gap:10px;
}
.cat-filter-box {
display:inline-flex;
align-items:center;
padding:6px 10px;
border-radius:10px;
border:1px solid #dcdcdc;
background:#f8f9fa;
gap:6px;
}
.cat-filter-dot-wrap input {
display:none;
}
.cat-filter-dot {
width:24px;
height:24px;
border-radius:50%;
display:inline-block;
background:var(--cat-color);
border:2px solid #999;
cursor:pointer;
opacity:.6;
transition:0.15s;
}
.cat-filter-dot-wrap input:checked + .cat-filter-dot {
opacity:1;
border-color:#333;
transform:scale(1.1);
}
.cat-filter-dot[style*="transparent"] {
background: transparent;
border:2px dashed #aaa;
}
.category-filter {
display:flex;
gap:8px;
}
.iw-cat-btn {
cursor:pointer;
opacity:0.4;
transition:0.15s;
border-color:#333;
}
.iw-cat-btn.active {
opacity:1;
transform:scale(1.25);
border-color:#000000;
}
.iw-cat-btn[data-value="0"] {
background:transparent !important;
border:1px dashed #000000 !important;
}
/* mobile */
@media (max-width: 768px) {

View File

@ -121,7 +121,7 @@ if (isset($_GET['ajax']) && $_GET['ajax'] === 'work_images') {
'i_sourceuid' => $img['i_sourceuid'],
'i_note' => $img['i_note'],
'i_createddate' => $img['i_createddate'],
'images' => $images // ⭐ 핵심
'images' => $images
]
]);
exit;
@ -1418,7 +1418,8 @@ function renderList(){
<col style="width:2%">
<col style="width:10%">
<col style="width:12%">
<col style="width:26%">
<col style="width:13%"> <!-- INSTALL -->
<col style="width:13%"> <!-- PICKUP -->
<col style="width:24%">
<col style="width:26%">
</colgroup>
@ -1440,6 +1441,7 @@ function renderList(){
const req = (row.di_request_note || '').trim();
const work = (row.di_work_note || '').trim();
const cs = (row.di_cs_note || '').trim();
const split = renderContainersSplit(row.containers);
html += `
<tr data-uid="${row.di_uid}" class="status-${row.di_status}">
@ -1455,7 +1457,26 @@ function renderList(){
${escapeHtml(row.di_customer_name || '')}
</div>
</td>
<td>${renderContainers(row.containers)}</td>
<td class="align-middle">
${split.install ? `
<div class="container-chips inline-chips">
<div class="task-action-title install">
<i class="bi bi-plus-circle fs-6"></i>
</div>
${split.install}
</div>
` : ''}
</td>
<td class="align-middle">
${split.pickup ? `
<div class="container-chips inline-chips">
<div class="task-action-title pickup">
<i class="bi bi-dash-circle fs-6"></i>
</div>
${split.pickup}
</div>
` : ''}
</td>
<td class="small text-muted">
<div>
${escapeHtml(row.di_address)},
@ -1592,13 +1613,15 @@ function renderWorkType(type){
return `<span class="badge ${badgeClass}">${type || '-'}</span>`;
}
function renderContainers(containers){
if(!containers || containers.length === 0) return '';
function renderContainersSplit(containers){
let installChips = '';
let pickupChips = '';
if(!containers || containers.length === 0){
return { install:'', pickup:'' };
}
containers.forEach(c => {
let chipClass = 'chip chip-company';
@ -1627,30 +1650,10 @@ function renderContainers(containers){
}
});
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;
return {
install: installChips,
pickup: pickupChips
};
}
function toggleReorderMode(driverUid, btn){

View File

@ -8,6 +8,20 @@ if (!empty($kw_customer)) $where[] = "(iw_customer_name LIKE '%{$kw_cu
if (!empty($kw_work)) $where[] = "(iw_work_type = '{$kw_work}')";
if (!empty($_GET['kw_two_person'])) $where[] = "(iw_required_person_cnt IS NOT NULL AND iw_required_person_cnt > 0)";
$kw_categories = $_GET['kw_category'] ?? [];
if (!is_array($kw_categories)) $kw_categories = [$kw_categories];
$kw_categories = array_filter(array_map('intval', $kw_categories), function($v){
return $v >= 0 && $v <= 5;
});
if ($kw_categories) {
$hasZero = in_array(0, $kw_categories);
$cats = array_filter($kw_categories, fn($v) => $v > 0);
$cond = [];
if ($cats) $cond[] = "iw_category IN (" . implode(',', $cats) . ")";
if ($hasZero) $cond[] = "(iw_category = 0 OR iw_category IS NULL)";
$where[] = '(' . implode(' OR ', $cond) . ')';
}
$statusList = [];
if (!empty($_GET['kw_waiting'])) $statusList[] = "'WAITING'";
if (!empty($_GET['kw_draft'])) $statusList[] = "'DRAFT'";
@ -216,6 +230,17 @@ function formatPhone($phone) {
return $phone;
}
function getWaitCategoryColor($cat) {
switch ((int)$cat) {
case 1: return '#ff4d4f'; // red
case 2: return '#1677ff'; // blue
case 3: return '#52c41a'; // green
case 4: return '#fa9614'; // orange
case 5: return '#9254de'; // purple
default: return 'transparent';
}
}
?>
<!-- drag and drop -->
@ -436,12 +461,16 @@ function formatPhone($phone) {
btnNeedOilPickup.classList.add('iw-toggle-on');
}
InstallWait.category = parseInt(data.iw_category || 0, 10);
// job label 갱신
document.getElementById('iwStep3Title').innerText = `Step 3 of 3 - ${data.iw_work_type} Detail`;
// action box 다시 그림
setStep3Title();
renderStep3ActionBoxes();
renderCategorySelector();
bindCategoryEvents();
loadCurrentContainersIfNeeded(() => {
currentReady = true;
});
@ -538,7 +567,7 @@ function formatPhone($phone) {
<input type="hidden" name="kw_two_person" id="kw_two_person">
</div>
<div class="col-md-3 d-flex align-items-stretch">
<div class="col-md-2 d-flex align-items-stretch">
<div class="radio-segment w-100">
<label>
<input type="checkbox" name="kw_draft" id="kw_draft" checked>
@ -557,6 +586,24 @@ function formatPhone($phone) {
</div>
</div>
<!-- Category -->
<div class="col-md-3">
<div class="cat-filter-box w-100 h-100">
<div class="category-filter">
<?php
$allCats = [0,1,2,3,4,5];
foreach ($allCats as $i):
$checked = in_array($i, $kw_categories ?? []) ? 'checked' : '';
?>
<label class="cat-filter-dot-wrap">
<input type="checkbox" name="kw_category[]" value="<?=$i?>" <?=$checked?>>
<span class="cat-filter-dot" style="--cat-color:<?=getWaitCategoryColor($i)?>"></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Search -->
<div class="col-md-3">
<div class="input-group h-100 search-container">
@ -568,9 +615,9 @@ function formatPhone($phone) {
</div>
<!-- Add -->
<div class="col-md-3 d-flex justify-content-end">
<div class="col-md-1 d-flex justify-content-end">
<button type="button" class="btn-add" onclick="openAddInstallWaitModal()">
Add Wait List
Add
</button>
</div>
@ -648,7 +695,9 @@ function formatPhone($phone) {
data-pickup-d="<?= $pickup_d ?>"
data-pickup-b="<?= $pickup_b ?>"
data-install-d="<?= $install_d ?>"
data-install-b="<?= $install_b ?>">
data-install-b="<?= $install_b ?>"
data-category="<?= (int)($row['iw_category'] ?? 0) ?>"
>
<!-- Top -->
<div class="d-flex align-items-center mb-1">
@ -701,6 +750,16 @@ function formatPhone($phone) {
<!-- RIGHT -->
<div class="ms-auto d-flex align-items-center gap-2">
<?php if (!empty($row['iw_category'])): ?>
<span style="
width:16px;
height:16px;
border-radius:50%;
display:inline-block;
background:<?=getWaitCategoryColor($row['iw_category'])?>;
" title="Category <?=$row['iw_category']?>"></span>
<?php endif; ?>
<small class="text-muted me-2">
<?php if (!empty($row['di_install_date'])): ?>
<i class="bi bi-link-45deg text-secondary" title="Assigned"></i>
@ -1122,6 +1181,10 @@ function formatPhone($phone) {
loadDailyInstallPoints();
});
$(document).on('change', 'input[name="kw_category[]"]', function () {
searchWaitlist();
});
function resetTrialSelectionUI() {
// trial 데이터 초기화
iwTrialRows = {};
@ -1260,7 +1323,7 @@ function formatPhone($phone) {
tr.dataset.orderSeq = r.di_order_seq;
}
tr.innerHTML = `
<td>
<td class="iw-drag-handle" style="cursor:grab;">
${r.is_trial ? '' : `
<label class="iw-checkbox-wrap">
<input type="checkbox" class="iw-checkbox" value="${r.di_uid}">
@ -1269,16 +1332,16 @@ function formatPhone($phone) {
${r.di_lock_date == 1 ? '<i class="bi bi-lock text-secondary" title="Preferred Date"></i>' : ''}
`}
</td>
<td>${r.di_order_seq}</td>
<td class="iw-drag-handle" style="cursor:grab; max-width: 220px;">
<td class="iw-drag-handle" style="cursor:grab;">${r.di_order_seq}</td>
<td style="max-width: 220px;">
<span class="text-muted me-2" title="Drag to reorder">
${r.di_accountno || ''}</span>
<span class="iw-name" title="${r.di_customer_name || ''}">
<span style="cursor:grab;" class="iw-drag-handle iw-name" title="${r.di_customer_name || ''}">
<b>${r.di_customer_name || ''}</b>
</span>
</td>
<td>${r.di_customer_driver_initial}</td>
<td>${r.di_address || ''}<br><span class="text-muted small">${r.di_city || ''}</span></td>
<td class="iw-drag-handle" style="cursor:grab;">${r.di_address || ''}<br><span class="text-muted small">${r.di_city || ''}</span></td>
<td><span class="badge ${getWorkTypeBadge(r.di_work_type)}" title="${r.di_work_type || ''}"> ${(r.di_work_type || '').substring(0, 2)}</span></td>
<td>${renderContainerSummary(r.pickup_d_cnt, r.pickup_b_cnt)}</td>
<td>${renderContainerSummary(r.install_d_cnt, r.install_b_cnt)}</td>

View File

@ -2,11 +2,21 @@ window.InstallWait = window.InstallWait || {
step: 1,
customer: null,
job: null,
category: 0,
pickup: [],
schedule: { date:'', note:'' },
photos: []
};
const IW_CATEGORY_COLORS = {
0: 'transparent', // 없음
1: '#ff4d4f',
2: '#1677ff',
3: '#52c41a',
4: '#fa9614',
5: '#9254de'
};
/* 슬라이드 이동 (100vw 기준) */
function goInstallWaitStep(step) {
const wrapper = document.querySelector('.install-wait-steps-wrapper');
@ -292,7 +302,8 @@ function initInstallWaitStep3() {
person_reason: twoOn ? document.getElementById('iwTwoPersonReason').value : null,
scheduled: recOn ? 1 : 0,
scheduled_cycle: recOn ? document.getElementById('iwRecurringDays').value: 0,
need_oil: oilOn ? 1 : 0
need_oil: oilOn ? 1 : 0,
category: InstallWait.category || 0
};
btnStep3Complete.disabled = true;
@ -344,10 +355,12 @@ function initInstallWaitStep3() {
};
InstallWait.pickup = InstallWait.pickup || [];
InstallWait.category = 0;
setStep3Title();
renderStep3ActionBoxes();
loadCurrentContainersIfNeeded();
renderCategorySelector();
bindStep3Toggles();
fillRequestBySelect('Customer');
initInstallWaitDatePickers();
@ -431,6 +444,30 @@ function renderActionBox(action) {
`;
}
function renderCategorySelector() {
const wrap = document.getElementById('iwCategorySelectorWrap');
if (!wrap) return;
wrap.innerHTML = `
<div class="iw-cat-box">
${[0,1,2,3,4,5].map(i => `
<button type="button"
class="iw-cat-btn ${InstallWait.category == i ? 'active' : ''}"
data-value="${i}"
style="
width:18px;
height:18px;
border-radius:50%;
border:1px solid;
background:${IW_CATEGORY_COLORS[i]};
">
</button>
`).join('')}
</div>
`;
}
function countTaskChips(action) {
// action: 'PICKUP' | 'INSTALL'
const wrap = document.querySelector(`.container-chips[data-action="${action}"]`);
@ -937,6 +974,22 @@ function bindStep3Toggles() {
btnOil.onclick = () => {
btnOil.classList.toggle('iw-toggle-on');
};
bindCategoryEvents();
}
function bindCategoryEvents() {
document.querySelectorAll('.iw-cat-btn').forEach(btn => {
btn.onclick = function() {
document.querySelectorAll('.iw-cat-btn')
.forEach(b => b.classList.remove('active'));
this.classList.add('active');
InstallWait.category = parseInt(this.dataset.value, 10);
};
});
}
function getContainerIconByType(type) {

View File

@ -187,59 +187,77 @@
<label class="form-label small">Other Options</label>
<!-- Two-person row -->
<div class="row g-2 align-items-center mb-2">
<div class="col-md-4">
<button type="button"
id="btnTwoPerson"
class="btn btn-sm btn-outline-secondary w-100 d-flex align-items-center justify-content-center gap-2 iw-inline-ctrl">
<i class="bi bi-people"></i>
Two-person
</button>
</div>
<div class="col-md-8">
<div class="input-group input-group-sm iw-inline-ctrl">
<input id="iwTwoPersonReason"
class="form-control"
placeholder="Reason (required if enabled)"
disabled>
<div class="col-md-4">
<button type="button"
id="btnTwoPerson"
class="btn btn-sm btn-outline-secondary w-100 d-flex align-items-center justify-content-center gap-2 iw-inline-ctrl">
<i class="bi bi-people"></i>
Two-person
</button>
</div>
<div class="col-md-8">
<div class="input-group input-group-sm iw-inline-ctrl">
<input id="iwTwoPersonReason"
class="form-control"
placeholder="Reason (required if enabled)"
disabled>
</div>
</div>
</div>
</div>
<!-- Recurring row -->
<div class="row g-2 align-items-center mb-2">
<div class="col-md-4">
<button type="button"
id="btnRecurring"
class="btn btn-sm btn-outline-secondary w-100 d-flex align-items-center justify-content-center gap-2 iw-inline-ctrl">
<i class="bi bi-arrow-repeat"></i>
Recurring
</button>
</div>
<div class="col-md-8">
<div class="input-group input-group-sm iw-inline-ctrl">
<input type="number"
id="iwRecurringDays"
class="form-control"
placeholder="Cycle (days)"
min="1"
disabled>
<div class="col-md-4">
<button type="button"
id="btnRecurring"
class="btn btn-sm btn-outline-secondary w-100 d-flex align-items-center justify-content-center gap-2 iw-inline-ctrl">
<i class="bi bi-arrow-repeat"></i>
Recurring
</button>
</div>
<div class="col-md-8">
<div class="input-group input-group-sm iw-inline-ctrl">
<input type="number"
id="iwRecurringDays"
class="form-control"
placeholder="Cycle (days)"
min="1"
disabled>
</div>
</div>
</div>
</div>
<!-- Need Oil Pickup row -->
<div class="row g-2 align-items-center">
<div class="col-md-4">
<button type="button"
id="btnNeedOilPickup"
class="btn btn-sm btn-outline-secondary w-100 d-flex align-items-center justify-content-center gap-2 iw-inline-ctrl">
<i class="bi bi-droplet"></i>
Need Oil Pickup
</button>
</div>
<div class="col-md-4">
<button type="button"
id="btnNeedOilPickup"
class="btn btn-sm btn-outline-secondary w-100 d-flex align-items-center justify-content-center gap-2 iw-inline-ctrl">
<i class="bi bi-droplet"></i>
Need Oil Pickup
</button>
</div>
<div class="col-md-8">
<div class="d-flex align-items-center gap-2">
<button type="button"
class="btn btn-sm btn-outline-secondary d-flex align-items-center gap-2 iw-inline-ctrl" disabled>
<i class="bi bi-tags"></i>
Category
</button>
<!-- 컬러 선택 -->
<div class="cat-filter-box flex-grow-1">
<div id="iwCategorySelectorWrap"></div>
</div>
</div>
</div>
</div>
</div>
</div>