46d23a3323
- 모두 라벨부착/모두 Kagayaku 전달 버튼이 현재 상태를 보고 토글 · 전 박스 완료면 → 모두 해제, 아니면 → 모두 완료 - 버튼 색으로 현재 상태 표시(전부 완료 시 초록), 개별 토글/로드 시 동기화 - bulk 라우터에 value 파라미터 추가(완료/해제) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
254 lines
12 KiB
HTML
254 lines
12 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:0 1 240px;min-width:150px; }
|
||
.dsp-bulk { margin-left:auto;display:flex;gap:8px;flex-wrap:wrap; }
|
||
.dsp-bulk .erp-btn:disabled { opacity:.6;cursor:wait; }
|
||
.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:14px;font-weight:700;color:#475569;letter-spacing:.5px; }
|
||
.dsp-order span { font-size:27px;font-weight:800;color:#0f172a;letter-spacing:.2px; }
|
||
.dsp-recipient { border-top:1px dashed #e2e8f0;padding-top:8px; }
|
||
.dsp-rname { display:block;font-size:22px;font-weight:800;color:#0f172a;line-height:1.2;word-break:break-word; }
|
||
.dsp-raddr { font-size:14px;line-height:1.45;color:#334155;white-space:normal;word-break:break-word;margin-top:3px; }
|
||
.dsp-rphone { font-size:13px;color:#475569;margin-top:2px; }
|
||
.dsp-track-link { color:#2563eb !important;font-size:18px;font-weight:800;
|
||
text-decoration:underline !important;text-decoration-line:underline !important;
|
||
text-decoration-thickness:2px;text-underline-offset:3px;cursor:pointer; }
|
||
.dsp-track-link:hover { color:#1d4ed8 !important; }
|
||
.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;object-fit:contain; }
|
||
/* SPX 로고는 가로로 넓어 같은 높이에서 더 커 보인다 — 틱톡 로고와 시각 크기 맞춤 */
|
||
.dsp-courier[src*="spx"] { height:30px;top:18px; }
|
||
.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 / 받는 사람 / SKU" />
|
||
<div class="dsp-bulk">
|
||
<button class="erp-btn erp-btn-outline" type="button" data-bulk="label_attached" onclick="dspBulk(this,'label_attached')">모두 라벨 부착</button>
|
||
<button class="erp-btn erp-btn-outline" type="button" data-bulk="handed_to_kagayaku" onclick="dspBulk(this,'handed_to_kagayaku')">모두 Kagayaku 전달</button>
|
||
</div>
|
||
</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.tracking_id ~ ' ' ~ p.recipient_name ~ ' ' ~ (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>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>
|
||
<div class="dsp-recipient">
|
||
<span class="dsp-rname">{{ p.recipient_name or '—' }}</span>
|
||
{% if p.recipient_address %}<div class="dsp-raddr">{{ p.recipient_address }}</div>{% endif %}
|
||
{% if p.recipient_phone %}<div class="dsp-rphone">{{ p.recipient_phone }}</div>{% endif %}
|
||
</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 }};
|
||
var BATCH_ID = document.querySelector('.dsp').getAttribute('data-batch');
|
||
|
||
// ── 일괄 토글 처리 (전부 완료면 해제, 아니면 완료) ──
|
||
var BULK_LABELS = { label_attached: '라벨 부착', handed_to_kagayaku: 'Kagayaku 전달' };
|
||
|
||
function allOn(field) {
|
||
var cards = document.querySelectorAll('.dsp-card');
|
||
if (!cards.length) return false;
|
||
return Array.prototype.every.call(cards, function (c) {
|
||
return c.getAttribute('data-' + field) === 'true';
|
||
});
|
||
}
|
||
|
||
// 버튼 상태(초록/외곽선) 동기화. 전부 완료면 on.
|
||
function refreshBulkButtons() {
|
||
document.querySelectorAll('.dsp-bulk [data-bulk]').forEach(function (btn) {
|
||
var on = allOn(btn.getAttribute('data-bulk'));
|
||
btn.classList.toggle('erp-btn-primary', on);
|
||
btn.classList.toggle('erp-btn-outline', !on);
|
||
});
|
||
}
|
||
|
||
window.dspBulk = function (btn, field) {
|
||
var target = !allOn(field); // 전부 완료면 해제, 아니면 완료
|
||
var verb = target ? '완료로 표시' : '완료 해제';
|
||
if (!confirm('이 배치의 모든 박스를 "' + (BULK_LABELS[field] || field) + '" ' + verb + '할까요?')) return;
|
||
document.querySelectorAll('.dsp-bulk .erp-btn').forEach(function (b) { b.disabled = true; });
|
||
var body = new URLSearchParams();
|
||
body.set('field', field);
|
||
body.set('value', target ? 'true' : 'false');
|
||
fetch('/dispatch/batches/' + BATCH_ID + '/bulk', {
|
||
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 () { location.reload(); })
|
||
.catch(function () {
|
||
alert('일괄 처리에 실패했습니다. 다시 시도하세요.');
|
||
document.querySelectorAll('.dsp-bulk .erp-btn').forEach(function (b) { b.disabled = false; });
|
||
});
|
||
};
|
||
|
||
// ── 상태 토글 (즉시 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();
|
||
refreshBulkButtons();
|
||
}
|
||
|
||
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);
|
||
|
||
refreshBulkButtons(); // 초기 버튼 상태(전부 완료면 초록)
|
||
})();
|
||
</script>
|
||
{% endblock %}
|