feat(dispatch): 받는 사람 이름도 검색 + 검색창 컴팩트 드롭다운
- search_parcels: recipient_name ILIKE 추가(이름·주문·패키지·송장 4개 필드) - batches.html: 큰 카드 → 한 줄 입력창(🔎+clear), 결과는 절대위치 드롭다운으로 본문 안 밀리게. 바깥 클릭·Esc 닫기, 이름 매칭 강조 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -359,9 +359,9 @@ class DispatchStore:
|
||||
)
|
||||
|
||||
def search_parcels(self, *, query: str, limit: int = 50) -> list[dict[str, Any]]:
|
||||
"""주문번호/패키지번호/송장번호(부분 일치)로 박스를 찾는다.
|
||||
"""주문번호/패키지번호/송장번호/받는 사람 이름(부분 일치)로 박스를 찾는다.
|
||||
|
||||
3개 식별자 어디든 query 가 포함되면 매칭. 받는 사람(이름/전화/주소)과
|
||||
4개 필드 어디든 query 가 포함되면 매칭. 받는 사람(이름/전화/주소)과
|
||||
소속 배치(날짜/플랫폼/배치명/id) + 박스 seq 를 함께 돌려준다.
|
||||
최신 출고일/배치 우선. query 가 비면 빈 리스트.
|
||||
"""
|
||||
@@ -389,9 +389,10 @@ class DispatchStore:
|
||||
b.batch_name
|
||||
FROM dispatch_parcels p
|
||||
JOIN dispatch_batches b ON b.id = p.batch_id
|
||||
WHERE p.order_id ILIKE %(like)s
|
||||
OR p.package_id ILIKE %(like)s
|
||||
OR p.tracking_id ILIKE %(like)s
|
||||
WHERE p.order_id ILIKE %(like)s
|
||||
OR p.package_id ILIKE %(like)s
|
||||
OR p.tracking_id ILIKE %(like)s
|
||||
OR p.recipient_name ILIKE %(like)s
|
||||
ORDER BY b.dispatch_date DESC, b.id DESC, p.seq ASC
|
||||
LIMIT %(limit)s
|
||||
""",
|
||||
|
||||
@@ -20,20 +20,25 @@
|
||||
.dsp-cal-day.has { color:#0f172a;background:#eef2ff;font-weight:700;cursor:pointer;border:1px solid #c7d2fe; }
|
||||
.dsp-cal-day.has:hover { background:#e0e7ff; }
|
||||
.dsp-cal-day.sel { background:#16a34a !important;border-color:#16a34a !important;color:#fff !important; }
|
||||
/* 검색 */
|
||||
.dsp-search { margin:0 0 14px; }
|
||||
.dsp-search-box { display:flex;gap:8px;align-items:center;flex-wrap:wrap; }
|
||||
.dsp-search-box input { flex:1 1 320px;min-width:220px;padding:10px 12px;border:1px solid #cbd5e1;border-radius:8px;font-size:15px; }
|
||||
.dsp-search-note { font-size:12px;color:#94a3b8;margin:6px 2px 0; }
|
||||
.dsp-search-results { margin-top:10px;display:flex;flex-direction:column;gap:8px; }
|
||||
.dsp-hit { border:1px solid #e2e8f0;border-radius:10px;padding:10px 12px;background:#fff;display:flex;justify-content:space-between;gap:12px;align-items:flex-start; }
|
||||
.dsp-hit-name { font-weight:700;font-size:15px;color:#0f172a; }
|
||||
.dsp-hit-meta { font-size:13px;color:#475569;margin-top:2px;line-height:1.5; }
|
||||
/* 검색: 한 줄 입력 + 결과는 절대위치 드롭다운(본문 안 밀림) */
|
||||
.dsp-search { position:relative;margin:0 0 12px;max-width:520px; }
|
||||
.dsp-search-box { position:relative; }
|
||||
.dsp-search-box .dsp-q-ico { position:absolute;left:11px;top:50%;transform:translateY(-50%);font-size:14px;color:#94a3b8;pointer-events:none; }
|
||||
.dsp-search-box input { width:100%;padding:9px 30px 9px 32px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;box-sizing:border-box; }
|
||||
.dsp-search-box input:focus { outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15); }
|
||||
.dsp-q-clear { position:absolute;right:8px;top:50%;transform:translateY(-50%);border:0;background:transparent;color:#94a3b8;font-size:18px;cursor:pointer;line-height:1;padding:0 4px;display:none; }
|
||||
.dsp-search-results { position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:40;background:#fff;border:1px solid #e2e8f0;border-radius:10px;box-shadow:0 8px 24px rgba(15,23,42,.12);max-height:64vh;overflow:auto;display:none;padding:6px; }
|
||||
.dsp-search-results.open { display:block; }
|
||||
.dsp-hit { border-radius:8px;padding:9px 10px;display:flex;justify-content:space-between;gap:12px;align-items:flex-start; }
|
||||
.dsp-hit + .dsp-hit { border-top:1px solid #f1f5f9; }
|
||||
.dsp-hit:hover { background:#f8fafc; }
|
||||
.dsp-hit-name { font-weight:700;font-size:14px;color:#0f172a; }
|
||||
.dsp-hit-meta { font-size:12.5px;color:#475569;margin-top:2px;line-height:1.5; }
|
||||
.dsp-hit-meta code { background:#f1f5f9;padding:1px 5px;border-radius:4px;font-size:12px; }
|
||||
.dsp-hit mark { background:#fde68a;padding:0 1px;border-radius:2px; }
|
||||
.dsp-hit-batch { font-size:12px;color:#64748b;margin-top:4px; }
|
||||
.dsp-hit-batch { font-size:11.5px;color:#64748b;margin-top:3px; }
|
||||
.dsp-hit-go { white-space:nowrap;flex:0 0 auto;align-self:center; }
|
||||
.dsp-hit-empty { color:#94a3b8;font-size:14px;padding:8px 2px; }
|
||||
.dsp-hit-empty { color:#94a3b8;font-size:13.5px;padding:10px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -41,17 +46,15 @@
|
||||
<section class="dsp">
|
||||
{% include "dispatch/_nav.html" %}
|
||||
|
||||
<div class="erp-card dsp-search">
|
||||
<div class="cpg-card-head"><h3 style="margin:0;" data-ko="🔎 받는 사람 찾기" data-en="🔎 Find Recipient">🔎 받는 사람 찾기</h3></div>
|
||||
<div class="dsp-search" id="dsp-search">
|
||||
<div class="dsp-search-box">
|
||||
<span class="dsp-q-ico">🔎</span>
|
||||
<input type="text" id="dsp-q" autocomplete="off"
|
||||
data-ko-ph="주문번호 · 패키지번호 · 송장번호 (일부만 입력해도 됨)"
|
||||
data-en-ph="Order / Package / Tracking number (partial OK)"
|
||||
placeholder="주문번호 · 패키지번호 · 송장번호 (일부만 입력해도 됨)" />
|
||||
data-ko-ph="이름 · 주문번호 · 패키지번호 · 송장번호로 받는 사람 찾기"
|
||||
data-en-ph="Find recipient by name / order / package / tracking no."
|
||||
placeholder="이름 · 주문번호 · 패키지번호 · 송장번호로 받는 사람 찾기" />
|
||||
<button type="button" class="dsp-q-clear" id="dsp-q-clear" aria-label="clear">×</button>
|
||||
</div>
|
||||
<div class="dsp-search-note"
|
||||
data-ko="번호 일부만 입력하면 전체 출고 배치에서 해당 박스의 받는 사람을 찾아줍니다. (2글자 이상)"
|
||||
data-en="Type any part of a number to find the recipient across all batches. (2+ chars)">번호 일부만 입력하면 전체 출고 배치에서 해당 박스의 받는 사람을 찾아줍니다. (2글자 이상)</div>
|
||||
<div class="dsp-search-results" id="dsp-search-results"></div>
|
||||
</div>
|
||||
|
||||
@@ -264,13 +267,18 @@
|
||||
render();
|
||||
})();
|
||||
|
||||
// ── 받는 사람 찾기(주문/패키지/송장 번호 부분 일치) ──
|
||||
// ── 받는 사람 찾기(이름/주문/패키지/송장 번호 부분 일치) ──
|
||||
(function () {
|
||||
var wrap = document.getElementById('dsp-search');
|
||||
var input = document.getElementById('dsp-q');
|
||||
var box = document.getElementById('dsp-search-results');
|
||||
var clearBtn = document.getElementById('dsp-q-clear');
|
||||
if (!input || !box) return;
|
||||
var timer = null, lastQ = '';
|
||||
|
||||
function open() { box.classList.add('open'); }
|
||||
function close() { box.classList.remove('open'); }
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
@@ -291,7 +299,9 @@
|
||||
if (r.order_id) ids.push('Order <code>' + hi(r.order_id, q) + '</code>');
|
||||
if (r.package_id) ids.push('Pkg <code>' + hi(r.package_id, q) + '</code>');
|
||||
if (r.tracking_id) ids.push((en() ? 'Track ' : '송장 ') + '<code>' + hi(r.tracking_id, q) + '</code>');
|
||||
var name = r.recipient_name || (en() ? '(no recipient name)' : '(받는 사람 정보 없음)');
|
||||
var name = r.recipient_name
|
||||
? hi(r.recipient_name, q)
|
||||
: esc(en() ? '(no recipient name)' : '(받는 사람 정보 없음)');
|
||||
var contact = [];
|
||||
if (r.recipient_phone) contact.push(esc(r.recipient_phone));
|
||||
if (r.recipient_address) contact.push(esc(r.recipient_address));
|
||||
@@ -301,7 +311,7 @@
|
||||
+ (done ? ' · <span style="color:#16a34a;font-weight:600;">✓</span>' : '');
|
||||
return '<div class="dsp-hit">'
|
||||
+ '<div>'
|
||||
+ '<div class="dsp-hit-name">' + esc(name) + '</div>'
|
||||
+ '<div class="dsp-hit-name">' + name + '</div>'
|
||||
+ (contact.length ? '<div class="dsp-hit-meta">' + contact.join(' · ') + '</div>' : '')
|
||||
+ '<div class="dsp-hit-meta">' + ids.join(' ') + '</div>'
|
||||
+ '<div class="dsp-hit-batch">' + batchLine + '</div>'
|
||||
@@ -320,24 +330,38 @@
|
||||
if (!rs.length) {
|
||||
box.innerHTML = '<div class="dsp-hit-empty">'
|
||||
+ (en() ? 'No match.' : '일치하는 박스가 없습니다.') + '</div>';
|
||||
return;
|
||||
} else {
|
||||
box.innerHTML = rs.map(function (r) { return row(r, q); }).join('');
|
||||
}
|
||||
box.innerHTML = rs.map(function (r) { return row(r, q); }).join('');
|
||||
open();
|
||||
})
|
||||
.catch(function () {
|
||||
box.innerHTML = '<div class="dsp-hit-empty">'
|
||||
+ (en() ? 'Search failed.' : '검색 실패.') + '</div>';
|
||||
open();
|
||||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
box.innerHTML = ''; close(); lastQ = '';
|
||||
if (clearBtn) clearBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
var q = input.value.trim();
|
||||
if (clearBtn) clearBtn.style.display = q ? 'block' : 'none';
|
||||
if (timer) clearTimeout(timer);
|
||||
if (q.length < 2) { box.innerHTML = ''; lastQ = ''; return; }
|
||||
if (q === lastQ) return;
|
||||
if (q.length < 2) { box.innerHTML = ''; close(); lastQ = ''; return; }
|
||||
if (q === lastQ) { open(); return; }
|
||||
lastQ = q;
|
||||
timer = setTimeout(function () { run(q); }, 250);
|
||||
});
|
||||
// 결과가 있으면 포커스 시 다시 펼친다.
|
||||
input.addEventListener('focus', function () { if (box.innerHTML) open(); });
|
||||
if (clearBtn) clearBtn.addEventListener('click', function () { input.value = ''; reset(); input.focus(); });
|
||||
input.addEventListener('keydown', function (e) { if (e.key === 'Escape') { close(); input.blur(); } });
|
||||
// 바깥 클릭 시 닫기.
|
||||
document.addEventListener('click', function (e) { if (!wrap.contains(e.target)) close(); });
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user