4a07ef139d
- 택배스캔 단계 제거 → 라벨부착 / 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>
191 lines
8.6 KiB
HTML
191 lines
8.6 KiB
HTML
{% 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 %}
|