fix: rx-usage 쿼리에 PS_Type!=9 조건 추가 (실제 조제된 약만 집계)

- patient_query: 대체조제 원본 처방 제외
- rx_query: 대체조제 원본 처방 제외
- PS_Type=9는 대체조제시 원래 처방된 약(조제 안됨)
- 기타 배치 스크립트 및 문서 추가
This commit is contained in:
thug0bin
2026-03-09 21:54:32 +09:00
parent f92abf94c8
commit e470deaefc
10 changed files with 1909 additions and 121 deletions

View File

@@ -939,7 +939,7 @@
loadOrderData(); // 수인약품 주문량 로드
});
// ──────────────── 도매상 주문량 조회 (지오영 + 수인 + 백제 합산) ────────────────
// ──────────────── 도매상 주문량 조회 (지오영 + 수인 + 백제 + 동원 합산) ────────────────
async function loadOrderData() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
@@ -948,11 +948,12 @@
orderDataByKd = {};
try {
// 지오영 + 수인 + 백제 병렬 조회
const [geoRes, sooinRes, baekjeRes] = await Promise.all([
// 지오영 + 수인 + 백제 + 동원 병렬 조회
const [geoRes, sooinRes, baekjeRes, dongwonRes] = await Promise.all([
fetch(`/api/geoyoung/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })),
fetch(`/api/sooin/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })),
fetch(`/api/baekje/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false }))
fetch(`/api/baekje/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })),
fetch(`/api/dongwon/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false }))
]);
let totalOrders = 0;
@@ -999,7 +1000,21 @@
console.log('💉 백제 주문량:', Object.keys(baekjeRes.by_kd_code).length, '품목,', baekjeRes.order_count, '건');
}
console.log('📦 3사 합산 주문량:', Object.keys(orderDataByKd).length, '품목,', totalOrders, '건 주문');
// 동원 데이터 합산
if (dongwonRes.success && dongwonRes.by_kd_code) {
for (const [kd, data] of Object.entries(dongwonRes.by_kd_code)) {
if (!orderDataByKd[kd]) {
orderDataByKd[kd] = { product_name: data.product_name, spec: data.spec, boxes: 0, units: 0, sources: [] };
}
orderDataByKd[kd].boxes += data.boxes || 0;
orderDataByKd[kd].units += data.units || 0;
orderDataByKd[kd].sources.push('동원');
}
totalOrders += dongwonRes.order_count || 0;
console.log('🟠 동원 주문량:', Object.keys(dongwonRes.by_kd_code).length, '품목,', dongwonRes.order_count, '건');
}
console.log('📦 4사 합산 주문량:', Object.keys(orderDataByKd).length, '품목,', totalOrders, '건 주문');
} catch(err) {
console.warn('주문량 조회 실패:', err);
@@ -1269,6 +1284,16 @@
gradient: 'linear-gradient(135deg, #d97706, #f59e0b)',
filterFn: (item) => item.supplier === '백제약품' || item.wholesaler === 'baekje',
getCode: (item) => item.baekje_code || item.drug_code
},
dongwon: {
id: 'dongwon',
name: '동원약품',
icon: '🏥',
logo: '/static/img/logo_dongwon.png',
color: '#22c55e',
gradient: 'linear-gradient(135deg, #16a34a, #22c55e)',
filterFn: (item) => item.supplier === '동원약품' || item.wholesaler === 'dongwon',
getCode: (item) => item.dongwon_code || item.internal_code || item.drug_code
}
};
@@ -2043,9 +2068,9 @@
if (e.key === 'Enter') loadUsageData();
});
// ──────────────── 도매상 재고 조회 (지오영 + 수인 + 백제) ────────────────
// ──────────────── 도매상 재고 조회 (지오영 + 수인 + 백제 + 동원) ────────────────
let currentWholesaleItem = null;
window.wholesaleItems = { geoyoung: [], sooin: [], baekje: [] };
window.wholesaleItems = { geoyoung: [], sooin: [], baekje: [], dongwon: [] };
function openWholesaleModal(idx) {
const item = usageData[idx];
@@ -2065,7 +2090,7 @@
document.getElementById('geoResultBody').innerHTML = `
<div class="geo-loading">
<div class="loading-spinner"></div>
<div>도매상 재고 조회 중... (지오영 + 수인 + 백제)</div>
<div>도매상 재고 조회 중... (지오영 + 수인 + 백제 + 동원)</div>
</div>`;
document.getElementById('geoSearchKeyword').style.display = 'none';
@@ -2081,22 +2106,24 @@
async function searchAllWholesalers(kdCode, productName) {
const resultBody = document.getElementById('geoResultBody');
// 도매상 동시 호출
const [geoResult, sooinResult, baekjeResult] = await Promise.all([
// 도매상 동시 호출
const [geoResult, sooinResult, baekjeResult, dongwonResult] = await Promise.all([
searchGeoyoungAPI(kdCode, productName),
searchSooinAPI(kdCode),
searchBaekjeAPI(kdCode)
searchBaekjeAPI(kdCode),
searchDongwonAPI(kdCode, productName)
]);
// 결과 저장
window.wholesaleItems = {
geoyoung: geoResult.items || [],
sooin: sooinResult.items || [],
baekje: baekjeResult.items || []
baekje: baekjeResult.items || [],
dongwon: dongwonResult.items || []
};
// 통합 렌더링
renderWholesaleResults(geoResult, sooinResult, baekjeResult);
renderWholesaleResults(geoResult, sooinResult, baekjeResult, dongwonResult);
}
async function searchGeoyoungAPI(kdCode, productName) {
@@ -2136,18 +2163,42 @@
return { success: false, error: err.message, items: [] };
}
}
async function searchDongwonAPI(kdCode, productName) {
try {
// 1차: KD코드(보험코드)로 검색 (searchType=0)
let response = await fetch(`/api/dongwon/stock?keyword=${encodeURIComponent(kdCode)}`);
let data = await response.json();
// 결과 없으면 제품명으로 재검색
if (data.success && data.count === 0 && productName) {
// 제품명 정제: "휴니즈레바미피드정_(0.1g/1정)" → "휴니즈레바미피드정"
let cleanName = productName.split('_')[0].split('(')[0].trim();
if (cleanName) {
response = await fetch(`/api/dongwon/stock?keyword=${encodeURIComponent(cleanName)}`);
data = await response.json();
}
}
return data;
} catch (err) {
return { success: false, error: err.message, items: [] };
}
}
function renderWholesaleResults(geoResult, sooinResult, baekjeResult) {
function renderWholesaleResults(geoResult, sooinResult, baekjeResult, dongwonResult) {
const resultBody = document.getElementById('geoResultBody');
const geoItems = geoResult.items || [];
const sooinItems = sooinResult.items || [];
const baekjeItems = (baekjeResult && baekjeResult.items) || [];
const dongwonItems = (dongwonResult && dongwonResult.items) || [];
// 재고 있는 것 먼저 정렬
geoItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
sooinItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
baekjeItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
dongwonItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
let html = '';
@@ -2260,6 +2311,44 @@
}
html += '</div>';
// ═══════ 동원약품 섹션 ═══════
html += `<div class="ws-section">
<div class="ws-header dongwon">
<span class="ws-logo">🏥</span>
<span class="ws-name">동원약품</span>
<span class="ws-count">${dongwonItems.length}건</span>
</div>`;
if (dongwonItems.length > 0) {
html += `<table class="geo-table">
<thead><tr><th>제품명</th><th>규격</th><th>단가</th><th>재고</th><th></th></tr></thead>
<tbody>`;
dongwonItems.forEach((item, idx) => {
const hasStock = item.stock > 0;
// 동원: code=KD코드(보험코드), internal_code=내부코드(주문용)
const displayCode = item.code || item.internal_code || '';
html += `
<tr class="${hasStock ? '' : 'no-stock'}">
<td>
<div class="geo-product">
<span class="geo-name">${escapeHtml(item.name)}</span>
<span class="geo-code">${displayCode} · ${item.manufacturer || ''}</span>
</div>
</td>
<td class="geo-spec">${item.spec || '-'}</td>
<td class="geo-price">${item.price ? item.price.toLocaleString() + '원' : '-'}</td>
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
<td>${hasStock ? `<button class="geo-add-btn dongwon" onclick="addToCartFromWholesale('dongwon', ${idx})">담기</button>` : ''}</td>
</tr>`;
});
html += '</tbody></table>';
} else {
html += `<div class="ws-empty">📭 검색 결과 없음</div>`;
}
html += '</div>';
resultBody.innerHTML = html;
}
@@ -2277,7 +2366,7 @@
const needed = currentWholesaleItem.total_dose;
const suggestedQty = Math.ceil(needed / specQty);
const supplierNames = { geoyoung: '지오영', sooin: '수인약품', baekje: '백제약품' };
const supplierNames = { geoyoung: '지오영', sooin: '수인약품', baekje: '백제약품', dongwon: '동원약품' };
const supplierName = supplierNames[wholesaler] || wholesaler;
const productName = wholesaler === 'geoyoung' ? item.product_name : item.name;
@@ -2298,6 +2387,7 @@
geoyoung_code: wholesaler === 'geoyoung' ? item.insurance_code : null,
sooin_code: wholesaler === 'sooin' ? item.code : null,
baekje_code: wholesaler === 'baekje' ? item.internal_code : null,
dongwon_code: wholesaler === 'dongwon' ? item.internal_code : null, // 동원: 내부코드로 주문
unit_price: unitPrice // 💰 단가 추가
};
@@ -2542,6 +2632,12 @@
.geo-add-btn.baekje:hover {
background: #d97706;
}
.geo-add-btn.dongwon {
background: #22c55e;
}
.geo-add-btn.dongwon:hover {
background: #16a34a;
}
.geo-price {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
@@ -2576,6 +2672,10 @@
background: linear-gradient(135deg, rgba(245, 158, 11, 0.2), rgba(217, 119, 6, 0.1));
border-left: 3px solid var(--accent-amber);
}
.ws-header.dongwon {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(22, 163, 74, 0.1));
border-left: 3px solid #22c55e;
}
.ws-logo {
width: 24px;
height: 24px;
@@ -2778,6 +2878,9 @@
.multi-ws-card.baekje {
border-left: 3px solid var(--accent-amber);
}
.multi-ws-card.dongwon {
border-left: 3px solid #22c55e;
}
.multi-ws-card.other {
border-left: 3px solid var(--text-muted);
opacity: 0.7;
@@ -3077,9 +3180,16 @@
color: '#a855f7',
balanceApi: '/api/sooin/balance',
salesApi: '/api/sooin/monthly-sales'
},
dongwon: {
id: 'dongwon', name: '동원약품', icon: '🏥',
logo: '/static/img/logo_dongwon.png',
color: '#22c55e',
balanceApi: '/api/dongwon/balance',
salesApi: '/api/dongwon/monthly-orders'
}
};
const WHOLESALER_ORDER = ['baekje', 'geoyoung', 'sooin'];
const WHOLESALER_ORDER = ['baekje', 'geoyoung', 'sooin', 'dongwon'];
async function loadBalances() {
const content = document.getElementById('balanceContent');