Files
dbx-main/app/modules/dispatch/templates/dispatch/detail.html
T
king 46d23a3323 feat(dispatch): 일괄 버튼을 토글로 변경(전부 완료면 해제)
- 모두 라벨부착/모두 Kagayaku 전달 버튼이 현재 상태를 보고 토글
  · 전 박스 완료면 → 모두 해제, 아니면 → 모두 완료
- 버튼 색으로 현재 상태 표시(전부 완료 시 초록), 개별 토글/로드 시 동기화
- bulk 라우터에 value 파라미터 추가(완료/해제)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:33:37 +09:00

254 lines
12 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: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 %}