feat: 상세 패널에 키오스크/라벨출력 액션 버튼 추가
- 상세 패널 상단에 2열 액션 버튼 배치 - 📺 키오스크: 해당 건 즉시 전송 - 🏷️ 라벨출력: QR 생성 + Brother QL 출력 - 버튼에 예상 적립 포인트 표시 - 호버 효과 + 로딩 상태 표시 - QR 발행 여부, 적립 완료 정보 표시
This commit is contained in:
parent
f1e609ba9f
commit
695c1f707f
@ -582,6 +582,61 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── 상세 패널 액션 버튼 ── */
|
||||||
|
.detail-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.detail-action-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.detail-action-btn.kiosk {
|
||||||
|
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.detail-action-btn.kiosk:hover {
|
||||||
|
background: linear-gradient(135deg, #4f46e5, #4338ca);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
|
||||||
|
}
|
||||||
|
.detail-action-btn.qr {
|
||||||
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.detail-action-btn.qr:hover {
|
||||||
|
background: linear-gradient(135deg, #d97706, #b45309);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
.detail-action-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.action-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.action-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.action-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.85;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 반응형 ── */
|
/* ── 반응형 ── */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.control-section {
|
.control-section {
|
||||||
@ -921,8 +976,32 @@
|
|||||||
document.getElementById('overlay').classList.add('visible');
|
document.getElementById('overlay').classList.add('visible');
|
||||||
document.getElementById('detailPanel').classList.add('open');
|
document.getElementById('detailPanel').classList.add('open');
|
||||||
|
|
||||||
|
const expectedPoints = Math.floor(sale.amount * 0.03);
|
||||||
|
const qrStatus = sale.qr_issued
|
||||||
|
? `<span style="color:#10b981">✓ QR 발행됨</span>`
|
||||||
|
: `<span style="color:#94a3b8">미발행</span>`;
|
||||||
|
const claimedInfo = sale.claimed_name
|
||||||
|
? `<div style="margin-top:8px; padding:10px; background:#dcfce7; border-radius:8px; font-size:13px;">
|
||||||
|
✓ <strong>${sale.claimed_name}</strong>님 적립 완료
|
||||||
|
</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
// 기본 정보 표시
|
// 기본 정보 표시
|
||||||
document.getElementById('detailContent').innerHTML = `
|
document.getElementById('detailContent').innerHTML = `
|
||||||
|
<!-- 액션 버튼 -->
|
||||||
|
<div class="detail-actions">
|
||||||
|
<button class="detail-action-btn kiosk" onclick="triggerKioskSingle('${orderNo}', ${sale.amount})">
|
||||||
|
<span class="action-icon">📺</span>
|
||||||
|
<span class="action-text">키오스크</span>
|
||||||
|
<span class="action-sub">${expectedPoints}P</span>
|
||||||
|
</button>
|
||||||
|
<button class="detail-action-btn qr" onclick="triggerQrPrintSingle('${orderNo}', ${sale.amount}, ${sale.qr_issued})">
|
||||||
|
<span class="action-icon">🏷️</span>
|
||||||
|
<span class="action-text">${sale.qr_issued ? '재출력' : '라벨출력'}</span>
|
||||||
|
<span class="action-sub">${expectedPoints}P</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="detail-info">
|
<div class="detail-info">
|
||||||
<div class="detail-info-item">
|
<div class="detail-info-item">
|
||||||
<div class="detail-info-label">거래번호</div>
|
<div class="detail-info-label">거래번호</div>
|
||||||
@ -942,13 +1021,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="detail-info-item">
|
<div class="detail-info-item">
|
||||||
<div class="detail-info-label">판매금액</div>
|
<div class="detail-info-label">판매금액</div>
|
||||||
<div class="detail-info-value">₩${Math.floor(sale.amount).toLocaleString()}</div>
|
<div class="detail-info-value" style="font-weight:700; color:#8b5cf6">₩${Math.floor(sale.amount).toLocaleString()}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-info-item">
|
<div class="detail-info-item">
|
||||||
<div class="detail-info-label">할인</div>
|
<div class="detail-info-label">QR 상태</div>
|
||||||
<div class="detail-info-value">₩${Math.floor(sale.discount).toLocaleString()}</div>
|
<div class="detail-info-value">${qrStatus}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${claimedInfo}
|
||||||
<div class="detail-items-title">📦 품목 목록</div>
|
<div class="detail-items-title">📦 품목 목록</div>
|
||||||
<div id="itemsList">
|
<div id="itemsList">
|
||||||
<div class="loading"><div class="spinner"></div>품목 로딩 중...</div>
|
<div class="loading"><div class="spinner"></div>품목 로딩 중...</div>
|
||||||
@ -981,6 +1061,81 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// 상세 패널에서 단일 건 처리
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
async function triggerKioskSingle(orderNo, amount) {
|
||||||
|
const btn = event.target.closest('.detail-action-btn');
|
||||||
|
const originalHtml = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="action-icon">⏳</span><span class="action-text">전송 중...</span>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/kiosk/trigger', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ transaction_id: orderNo, amount: amount })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
btn.innerHTML = `<span class="action-icon">✅</span><span class="action-text">전송 완료!</span><span class="action-sub">${data.points}P</span>`;
|
||||||
|
showToast(`키오스크 전송 완료! (${data.points}P)`, 'success');
|
||||||
|
setTimeout(() => loadSales(), 1500);
|
||||||
|
} else {
|
||||||
|
btn.innerHTML = originalHtml;
|
||||||
|
btn.disabled = false;
|
||||||
|
showToast(data.message || '전송 실패', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
btn.innerHTML = originalHtml;
|
||||||
|
btn.disabled = false;
|
||||||
|
showToast(`오류: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerQrPrintSingle(orderNo, amount, isReprint) {
|
||||||
|
const btn = event.target.closest('.detail-action-btn');
|
||||||
|
const originalHtml = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="action-icon">⏳</span><span class="action-text">출력 중...</span>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// QR 미발행이면 먼저 생성
|
||||||
|
if (!isReprint) {
|
||||||
|
const genRes = await fetch('/api/admin/qr/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ order_no: orderNo, amount: amount, preview: false })
|
||||||
|
});
|
||||||
|
const genData = await genRes.json();
|
||||||
|
if (!genData.success) throw new Error(genData.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프린터 출력
|
||||||
|
const res = await fetch('/api/admin/qr/print', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ order_no: orderNo, printer: 'brother' })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
btn.innerHTML = '<span class="action-icon">✅</span><span class="action-text">출력 완료!</span>';
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
setTimeout(() => loadSales(), 1500);
|
||||||
|
} else {
|
||||||
|
btn.innerHTML = originalHtml;
|
||||||
|
btn.disabled = false;
|
||||||
|
showToast(data.error || '출력 실패', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
btn.innerHTML = originalHtml;
|
||||||
|
btn.disabled = false;
|
||||||
|
showToast(`오류: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// 키오스크 전송 (선택된 건 중 첫 번째만 - 키오스크는 1건씩)
|
// 키오스크 전송 (선택된 건 중 첫 번째만 - 키오스크는 1건씩)
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user