feat: 실시간 POS 품목별 OTC 용법 라벨 인쇄 버튼 추가
POS 상세 패널:
- 품목 목록에 💊 인쇄 버튼 추가
- 프리셋 있으면 → 바로 인쇄
- 프리셋 없으면 → 새 창으로 등록 페이지 열기
API:
- /api/admin/pos-live/detail에 barcode 필드 추가
OTC 라벨 관리 페이지:
- URL 파라미터(barcode, name) 자동 처리
- POS에서 넘어올 때 자동으로 해당 약품 로드
This commit is contained in:
parent
c154537c87
commit
887aba3a03
@ -5070,14 +5070,15 @@ def api_admin_pos_live_detail(order_no):
|
|||||||
mssql_conn = mssql_engine.raw_connection()
|
mssql_conn = mssql_engine.raw_connection()
|
||||||
cursor = mssql_conn.cursor()
|
cursor = mssql_conn.cursor()
|
||||||
|
|
||||||
# 품목 상세 조회
|
# 품목 상세 조회 (바코드 포함)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
S.DrugCode AS drug_code,
|
S.DrugCode AS drug_code,
|
||||||
ISNULL(G.GoodsName, '(약품명 없음)') AS product_name,
|
ISNULL(G.GoodsName, '(약품명 없음)') AS product_name,
|
||||||
S.SL_NM_item AS quantity,
|
S.SL_NM_item AS quantity,
|
||||||
S.SL_NM_cost_a AS unit_price,
|
S.SL_NM_cost_a AS unit_price,
|
||||||
S.SL_TOTAL_PRICE AS total_price
|
S.SL_TOTAL_PRICE AS total_price,
|
||||||
|
G.Barcode AS barcode
|
||||||
FROM SALE_SUB S
|
FROM SALE_SUB S
|
||||||
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
|
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
|
||||||
WHERE S.SL_NO_order = ?
|
WHERE S.SL_NO_order = ?
|
||||||
@ -5092,7 +5093,8 @@ def api_admin_pos_live_detail(order_no):
|
|||||||
'product_name': row[1],
|
'product_name': row[1],
|
||||||
'quantity': int(row[2]) if row[2] else 0,
|
'quantity': int(row[2]) if row[2] else 0,
|
||||||
'unit_price': float(row[3]) if row[3] else 0,
|
'unit_price': float(row[3]) if row[3] else 0,
|
||||||
'total_price': float(row[4]) if row[4] else 0
|
'total_price': float(row[4]) if row[4] else 0,
|
||||||
|
'barcode': row[5] or ''
|
||||||
})
|
})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|||||||
@ -401,6 +401,30 @@
|
|||||||
debounceTimer = setTimeout(previewLabel, 500);
|
debounceTimer = setTimeout(previewLabel, 500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// URL 파라미터로 바코드/이름 전달 시 자동 로드
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const urlBarcode = params.get('barcode');
|
||||||
|
const urlName = params.get('name');
|
||||||
|
if (urlBarcode) {
|
||||||
|
currentBarcode = urlBarcode;
|
||||||
|
currentDrugName = urlName || urlBarcode;
|
||||||
|
document.getElementById('barcode').value = urlBarcode;
|
||||||
|
document.getElementById('searchInput').value = urlName || urlBarcode;
|
||||||
|
|
||||||
|
// 기존 프리셋 확인
|
||||||
|
fetch(`/api/admin/otc-labels/${urlBarcode}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.exists) {
|
||||||
|
document.getElementById('displayName').value = data.label.display_name || '';
|
||||||
|
document.getElementById('effect').value = data.label.effect || '';
|
||||||
|
document.getElementById('dosageInstruction').value = data.label.dosage_instruction || '';
|
||||||
|
document.getElementById('usageTip').value = data.label.usage_tip || '';
|
||||||
|
}
|
||||||
|
previewLabel();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 약품 검색 (MSSQL)
|
// 약품 검색 (MSSQL)
|
||||||
|
|||||||
@ -400,6 +400,24 @@
|
|||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.item-label-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.item-label-btn:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(245,158,11,0.4);
|
||||||
|
}
|
||||||
|
.item-label-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 오버레이 ── */
|
/* ── 오버레이 ── */
|
||||||
.overlay {
|
.overlay {
|
||||||
@ -1139,6 +1157,7 @@
|
|||||||
<span class="detail-item-name">${item.product_name}</span>
|
<span class="detail-item-name">${item.product_name}</span>
|
||||||
<span class="detail-item-qty">×${item.quantity}</span>
|
<span class="detail-item-qty">×${item.quantity}</span>
|
||||||
<span class="detail-item-price">₩${Math.floor(item.total_price).toLocaleString()}</span>
|
<span class="detail-item-price">₩${Math.floor(item.total_price).toLocaleString()}</span>
|
||||||
|
${item.barcode ? `<button class="item-label-btn" onclick="printOtcLabel('${item.barcode}', '${escapeHtml(item.product_name)}')">💊</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
document.getElementById('itemsList').innerHTML = itemsHtml;
|
document.getElementById('itemsList').innerHTML = itemsHtml;
|
||||||
@ -1150,6 +1169,61 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OTC 용법 라벨 인쇄
|
||||||
|
async function printOtcLabel(barcode, productName) {
|
||||||
|
const btn = event.target;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 프리셋 확인
|
||||||
|
const checkRes = await fetch(`/api/admin/otc-labels/${barcode}`);
|
||||||
|
const checkData = await checkRes.json();
|
||||||
|
|
||||||
|
if (checkData.exists) {
|
||||||
|
// 프리셋 있음 → 바로 인쇄
|
||||||
|
const preset = checkData.label;
|
||||||
|
const printRes = await fetch('/api/admin/otc-labels/print', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
barcode: barcode,
|
||||||
|
drug_name: preset.display_name || productName,
|
||||||
|
effect: preset.effect || '',
|
||||||
|
dosage_instruction: preset.dosage_instruction || '',
|
||||||
|
usage_tip: preset.usage_tip || ''
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const printData = await printRes.json();
|
||||||
|
|
||||||
|
if (printData.success) {
|
||||||
|
btn.textContent = '✓';
|
||||||
|
btn.style.background = '#10b981';
|
||||||
|
showToast(`용법 라벨 인쇄 완료!`, 'success');
|
||||||
|
} else {
|
||||||
|
btn.textContent = '💊';
|
||||||
|
btn.disabled = false;
|
||||||
|
showToast(printData.error || '인쇄 실패', 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 프리셋 없음 → 새 창으로 등록 페이지 열기
|
||||||
|
btn.textContent = '💊';
|
||||||
|
btn.disabled = false;
|
||||||
|
showToast('프리셋 미등록 → 등록 페이지로 이동', 'info');
|
||||||
|
window.open(`/admin/otc-labels?barcode=${barcode}&name=${encodeURIComponent(productName)}`, '_blank', 'width=1200,height=800');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
btn.textContent = '💊';
|
||||||
|
btn.disabled = false;
|
||||||
|
showToast('오류: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]);
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// 상세 패널에서 단일 건 처리
|
// 상세 패널에서 단일 건 처리
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user