Files
dbx-main/app/modules/dispatch/templates/dispatch/detail.html
T
king 4a07ef139d feat(dispatch): 작업 2단계로 축소 + J&T 조회 URL 수정 + Order ID 확대
- 택배스캔 단계 제거 → 라벨부착 / Kagayaku 전달 2단계
- 버튼 라벨에서 '완료' 제거(라벨부착 / Kagayaku 전달)
- 완료 판정/배치목록 완료수를 label_attached AND handed_to_kagayaku 기준으로
- J&T 송장 조회 딥링크를 gzquery.html?bills= 로 교체(billcode 미입력 문제)
- Order ID 27px 로 확대, 송장 링크 밑줄 강조(굵기/오프셋)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:09:16 +09:00

191 lines
8.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "erp_base.html" %}
{% block head_extra %}
<style>
.dsp-toolbar { display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:12px 0; }
.dsp-filter { display:flex;gap:6px;flex-wrap:wrap; }
.dsp-search { flex:1 1 220px;min-width:180px; }
.dsp-cards { display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:14px; }
.dsp-card { border:1px solid #e2e8f0;border-radius:12px;padding:14px;background:#fff;display:flex;flex-direction:column;gap:10px; }
.dsp-card { position:relative; }
.dsp-card.is-done { border-color:#86efac;background:#f0fdf4; }
.dsp-no { font-size:20px;font-weight:700;color:#0f172a; }
.dsp-courier-text { position:absolute;top:12px;right:14px; }
.dsp-meta { font-size:13px;line-height:1.5;color:#334155;word-break:break-all; }
.dsp-meta b { color:#0f172a; }
.dsp-order { font-size:15px;line-height:1.3;color:#334155;word-break:break-all; }
.dsp-order b { display:block;font-size:11px;font-weight:600;color:#64748b;letter-spacing:.5px; }
.dsp-order span { font-size:27px;font-weight:800;color:#0f172a;letter-spacing:.2px; }
.dsp-track-link { color:#2563eb;font-weight:700;text-decoration:underline;
text-decoration-thickness:2px;text-underline-offset:3px;cursor:pointer; }
.dsp-items { list-style:none;margin:0;padding:8px 0;border-top:1px dashed #e2e8f0;border-bottom:1px dashed #e2e8f0; }
.dsp-items li { font-size:16px;font-weight:600;color:#0f172a;padding:2px 0; }
.dsp-items .qty { color:#2563eb; }
.dsp-status { display:grid;grid-template-columns:1fr 1fr;gap:6px; }
.dsp-status .full { grid-column:1 / -1; }
.dsp-btn-status { border:1px solid #cbd5e1;border-radius:8px;padding:8px 6px;background:#f1f5f9;color:#475569;
font-weight:600;cursor:pointer;text-align:center;line-height:1.25;transition:background .12s,border-color .12s,color .12s; }
.dsp-btn-status small { display:block;font-size:10px;font-weight:500;opacity:.8; }
.dsp-btn-status.on { background:#16a34a;border-color:#16a34a;color:#fff; }
.dsp-btn-status:disabled { opacity:.6;cursor:wait; }
.dsp-courier { position:absolute;top:12px;right:14px;height:44px;width:auto;max-width:180px; }
.dsp-hidden { display:none !important; }
@media (max-width:480px){ .dsp-cards{grid-template-columns:1fr;} }
</style>
{% endblock %}
{% block content %}
<section class="dsp" data-batch="{{ batch.id }}">
{% include "dispatch/_nav.html" %}
<div class="erp-card">
<div class="cpg-card-head" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<h2>출고 작업 — 박스 {{ parcels|length }}개</h2>
<span class="erp-muted" id="dsp-done-label">완료 {{ done_count }} / {{ parcels|length }}</span>
</div>
<div class="dsp-toolbar">
<div class="dsp-filter" id="dsp-filter">
<button class="erp-btn erp-btn-primary" data-filter="all" type="button">전체</button>
<button class="erp-btn erp-btn-outline" data-filter="incomplete" type="button">미완료</button>
<button class="erp-btn erp-btn-outline" data-filter="label_attached" type="button">라벨부착</button>
<button class="erp-btn erp-btn-outline" data-filter="handed_to_kagayaku" type="button">Kagayaku 전달</button>
</div>
<input class="erp-input dsp-search" id="dsp-search" type="search"
placeholder="검색: Order ID / Tracking ID / Package ID / SKU" />
</div>
<div class="dsp-cards" id="dsp-cards">
{% for p in parcels %}
{% set is_done = p.label_attached and p.handed_to_kagayaku %}
<article class="dsp-card {% if is_done %}is-done{% endif %}"
data-parcel="{{ p.id }}"
data-label_attached="{{ p.label_attached|lower }}"
data-handed_to_kagayaku="{{ p.handed_to_kagayaku|lower }}"
data-search="{{ (p.order_id ~ ' ' ~ p.package_id ~ ' ' ~ p.tracking_id ~ ' ' ~ (p['items']|map(attribute='seller_sku')|join(' ')))|lower }}">
{% set logo = p.shipping_provider | courier_logo %}
{% if logo %}
<img class="dsp-courier" src="{{ logo }}" alt="{{ p.shipping_provider }}" title="{{ p.shipping_provider }}" />
{% elif p.shipping_provider %}
<span class="dsp-courier-text erp-badge">{{ p.shipping_provider }}</span>
{% endif %}
<div class="dsp-no">No. {{ p.seq }}</div>
<div class="dsp-order"><b>Order ID</b><span>{{ p.order_id or '—' }}</span></div>
<div class="dsp-meta">
<div><b>Package</b> {{ p.package_id or '—' }}</div>
<div><b>Tracking</b>
{% set turl = p.tracking_id | courier_track(p.shipping_provider) %}
{% if turl %}<a class="dsp-track-link" href="{{ turl }}" target="_blank" rel="noopener noreferrer">{{ p.tracking_id }}</a>
{% else %}{{ p.tracking_id or '—' }}{% endif %}
</div>
</div>
<ul class="dsp-items">
{% for it in p['items'] %}
<li>{{ it.seller_sku }} <span class="qty">× {{ it.quantity }}</span></li>
{% else %}
<li class="erp-muted" style="font-weight:400;">상품 없음</li>
{% endfor %}
</ul>
<div class="dsp-status">
{% for field in status_fields %}
<button type="button"
class="dsp-btn-status {% if (field == status_fields[-1]) and (status_fields|length % 2 == 1) %}full{% endif %} {% if p[field] %}on{% endif %}"
data-field="{{ field }}"
onclick="dspToggle(this)">
{{ status_labels[field].ko }}
<small>{{ status_labels[field].en }}</small>
</button>
{% endfor %}
</div>
</article>
{% endfor %}
{% if not parcels %}
<p class="erp-muted">이 배치에는 박스가 없습니다.</p>
{% endif %}
</div>
</div>
</section>
{% endblock %}
{% block scripts %}
<script>
(function () {
var STATUS_FIELDS = {{ status_fields|list|tojson }};
// ── 상태 토글 (즉시 DB 저장) ──
window.dspToggle = function (btn) {
var card = btn.closest('.dsp-card');
var parcelId = card.getAttribute('data-parcel');
var field = btn.getAttribute('data-field');
btn.disabled = true;
var body = new URLSearchParams();
body.set('field', field);
fetch('/dispatch/parcels/' + parcelId + '/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
})
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function (data) {
btn.classList.toggle('on', data.value);
card.setAttribute('data-' + field, data.value ? 'true' : 'false');
refreshDone(card);
applyFilter();
})
.catch(function () { alert('저장에 실패했습니다. 다시 시도하세요.'); })
.finally(function () { btn.disabled = false; });
};
function refreshDone(card) {
var done = STATUS_FIELDS.every(function (f) {
return card.getAttribute('data-' + f) === 'true';
});
card.classList.toggle('is-done', done);
updateDoneLabel();
}
function updateDoneLabel() {
var cards = document.querySelectorAll('.dsp-card');
var done = document.querySelectorAll('.dsp-card.is-done').length;
var label = document.getElementById('dsp-done-label');
if (label) label.textContent = '완료 ' + done + ' / ' + cards.length;
}
// ── 필터 ──
var currentFilter = 'all';
function matchesFilter(card) {
switch (currentFilter) {
case 'all': return true;
case 'incomplete':
return !STATUS_FIELDS.every(function (f) {
return card.getAttribute('data-' + f) === 'true';
});
default: return card.getAttribute('data-' + currentFilter) === 'true';
}
}
function applyFilter() {
var term = (document.getElementById('dsp-search').value || '').trim().toLowerCase();
document.querySelectorAll('.dsp-card').forEach(function (card) {
var ok = matchesFilter(card) &&
(term === '' || (card.getAttribute('data-search') || '').indexOf(term) !== -1);
card.classList.toggle('dsp-hidden', !ok);
});
}
document.getElementById('dsp-filter').addEventListener('click', function (e) {
var btn = e.target.closest('[data-filter]');
if (!btn) return;
currentFilter = btn.getAttribute('data-filter');
this.querySelectorAll('[data-filter]').forEach(function (b) {
b.classList.toggle('erp-btn-primary', b === btn);
b.classList.toggle('erp-btn-outline', b !== btn);
});
applyFilter();
});
document.getElementById('dsp-search').addEventListener('input', applyFilter);
})();
</script>
{% endblock %}