feat: 주문 모달 한도/매출 표시 및 UI 개선
- 도매상 한도 API 추가 (wholesaler_limits 테이블) - 다중 도매상 모달: 월 한도 + 실제 월 매출 표시 - 주문 후 예상 사용량 계산 및 경고 표시 - 이모지 대신 로고 이미지 사용 - 약가 → 매출액 헤더 변경 - 매출액 계산: SUM(DRUPRICE)
This commit is contained in:
parent
29597d55fa
commit
846883cbfa
@ -4096,7 +4096,7 @@ def api_rx_usage():
|
|||||||
ISNULL(G.SplName, '') as supplier,
|
ISNULL(G.SplName, '') as supplier,
|
||||||
SUM(ISNULL(P.QUAN, 1)) as total_qty,
|
SUM(ISNULL(P.QUAN, 1)) as total_qty,
|
||||||
SUM(ISNULL(P.QUAN, 1) * ISNULL(P.Days, 1)) as total_dose,
|
SUM(ISNULL(P.QUAN, 1) * ISNULL(P.Days, 1)) as total_dose,
|
||||||
SUM(ISNULL(P.DRUPRICE, 0) * ISNULL(P.QUAN, 1) * ISNULL(P.Days, 1)) as total_amount,
|
SUM(ISNULL(P.DRUPRICE, 0)) as total_amount, -- DRUPRICE 합계
|
||||||
COUNT(DISTINCT P.PreSerial) as prescription_count,
|
COUNT(DISTINCT P.PreSerial) as prescription_count,
|
||||||
COALESCE(NULLIF(G.BARCODE, ''), '') as barcode,
|
COALESCE(NULLIF(G.BARCODE, ''), '') as barcode,
|
||||||
ISNULL(IT.IM_QT_sale_debit, 0) as current_stock,
|
ISNULL(IT.IM_QT_sale_debit, 0) as current_stock,
|
||||||
|
|||||||
@ -1056,3 +1056,125 @@ def api_ai_order_pattern(drug_code):
|
|||||||
'pattern': None,
|
'pattern': None,
|
||||||
'message': '주문 이력이 없습니다'
|
'message': '주문 이력이 없습니다'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# 도매상 한도 관리 API
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
@order_bp.route('/wholesaler/limits', methods=['GET'])
|
||||||
|
def api_wholesaler_limits():
|
||||||
|
"""
|
||||||
|
전체 도매상 한도 조회 (현재 월 사용량 포함)
|
||||||
|
|
||||||
|
GET /api/order/wholesaler/limits
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 절대 경로 사용
|
||||||
|
db_path = r'c:\Users\청춘약국\source\pharmacy-pos-qr-system\backend\db\orders.db'
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# 현재 월
|
||||||
|
year_month = datetime.now().strftime('%Y-%m')
|
||||||
|
|
||||||
|
# 한도 정보 조회
|
||||||
|
cur.execute('SELECT * FROM wholesaler_limits WHERE is_active = 1 ORDER BY priority')
|
||||||
|
limits = cur.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for row in limits:
|
||||||
|
ws_id = row['wholesaler_id']
|
||||||
|
monthly_limit = row['monthly_limit']
|
||||||
|
|
||||||
|
# 이번 달 실제 주문 금액 조회 (성공한 것만)
|
||||||
|
cur.execute('''
|
||||||
|
SELECT COALESCE(SUM(oi.unit_price * oi.order_qty), 0) as total_amount
|
||||||
|
FROM order_items oi
|
||||||
|
JOIN orders o ON oi.order_id = o.id
|
||||||
|
WHERE o.wholesaler_id = ?
|
||||||
|
AND strftime('%Y-%m', o.order_date) = ?
|
||||||
|
AND o.status IN ('submitted', 'success', 'confirmed')
|
||||||
|
''', (ws_id, year_month))
|
||||||
|
|
||||||
|
usage_row = cur.fetchone()
|
||||||
|
current_usage = usage_row['total_amount'] if usage_row else 0
|
||||||
|
|
||||||
|
usage_percent = (current_usage / monthly_limit * 100) if monthly_limit > 0 else 0
|
||||||
|
remaining = monthly_limit - current_usage
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'wholesaler_id': ws_id,
|
||||||
|
'monthly_limit': monthly_limit,
|
||||||
|
'current_usage': current_usage,
|
||||||
|
'remaining': remaining,
|
||||||
|
'usage_percent': round(usage_percent, 1),
|
||||||
|
'warning_threshold': row['warning_threshold'],
|
||||||
|
'is_warning': usage_percent >= (row['warning_threshold'] * 100),
|
||||||
|
'priority': row['priority']
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'year_month': year_month,
|
||||||
|
'limits': result
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@order_bp.route('/wholesaler/limits/<wholesaler_id>', methods=['PUT'])
|
||||||
|
def api_update_wholesaler_limit(wholesaler_id):
|
||||||
|
"""
|
||||||
|
도매상 한도 수정
|
||||||
|
|
||||||
|
PUT /api/order/wholesaler/limits/geoyoung
|
||||||
|
{
|
||||||
|
"monthly_limit": 30000000,
|
||||||
|
"warning_threshold": 0.85
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
db_path = r'c:\Users\청춘약국\source\pharmacy-pos-qr-system\backend\db\orders.db'
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
updates = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if 'monthly_limit' in data:
|
||||||
|
updates.append('monthly_limit = ?')
|
||||||
|
params.append(data['monthly_limit'])
|
||||||
|
|
||||||
|
if 'warning_threshold' in data:
|
||||||
|
updates.append('warning_threshold = ?')
|
||||||
|
params.append(data['warning_threshold'])
|
||||||
|
|
||||||
|
if 'priority' in data:
|
||||||
|
updates.append('priority = ?')
|
||||||
|
params.append(data['priority'])
|
||||||
|
|
||||||
|
if updates:
|
||||||
|
updates.append("updated_at = datetime('now')")
|
||||||
|
params.append(wholesaler_id)
|
||||||
|
|
||||||
|
cur.execute(f'''
|
||||||
|
UPDATE wholesaler_limits
|
||||||
|
SET {', '.join(updates)}
|
||||||
|
WHERE wholesaler_id = ?
|
||||||
|
''', params)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': f'{wholesaler_id} 한도 업데이트 완료'
|
||||||
|
})
|
||||||
|
|||||||
@ -774,7 +774,7 @@
|
|||||||
<div class="stat-card emerald">
|
<div class="stat-card emerald">
|
||||||
<div class="stat-icon">💰</div>
|
<div class="stat-icon">💰</div>
|
||||||
<div class="stat-value" id="statTotalAmount">-</div>
|
<div class="stat-value" id="statTotalAmount">-</div>
|
||||||
<div class="stat-label">총 약가</div>
|
<div class="stat-label">총 매출액</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card orange">
|
<div class="stat-card orange">
|
||||||
<div class="stat-icon">🛒</div>
|
<div class="stat-icon">🛒</div>
|
||||||
@ -807,7 +807,7 @@
|
|||||||
<th class="center">현재고</th>
|
<th class="center">현재고</th>
|
||||||
<th class="center">처방횟수</th>
|
<th class="center">처방횟수</th>
|
||||||
<th class="center">투약량</th>
|
<th class="center">투약량</th>
|
||||||
<th class="right">약가</th>
|
<th class="right">매출액</th>
|
||||||
<th class="center" style="width:90px">주문수량</th>
|
<th class="center" style="width:90px">주문수량</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -1156,14 +1156,55 @@
|
|||||||
// 다중 도매상 선택을 위한 전역 변수
|
// 다중 도매상 선택을 위한 전역 변수
|
||||||
let pendingWholesalerItems = {};
|
let pendingWholesalerItems = {};
|
||||||
let pendingOtherItems = [];
|
let pendingOtherItems = [];
|
||||||
|
let wholesalerLimits = {}; // 도매상 한도 캐시
|
||||||
|
|
||||||
function openWholesalerSelectModal(itemsByWholesaler, otherItems) {
|
async function openWholesalerSelectModal(itemsByWholesaler, otherItems) {
|
||||||
pendingWholesalerItems = itemsByWholesaler;
|
pendingWholesalerItems = itemsByWholesaler;
|
||||||
pendingOtherItems = otherItems;
|
pendingOtherItems = otherItems;
|
||||||
|
|
||||||
const modal = document.getElementById('multiWholesalerModal');
|
const modal = document.getElementById('multiWholesalerModal');
|
||||||
const body = document.getElementById('multiWholesalerBody');
|
const body = document.getElementById('multiWholesalerBody');
|
||||||
|
|
||||||
|
// 로딩 표시
|
||||||
|
body.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-muted);">📊 한도 및 월 매출 조회 중...</div>';
|
||||||
|
modal.classList.add('show');
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = now.getMonth() + 1;
|
||||||
|
|
||||||
|
// 1. 도매상 한도 정보 가져오기
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/order/wholesaler/limits');
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
data.limits.forEach(l => {
|
||||||
|
wholesalerLimits[l.wholesaler_id] = l;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('한도 조회 실패:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 실제 월 매출 가져오기 (도매상 API 호출)
|
||||||
|
const wholesalerConfigs = WHOLESALER_ORDER.map(id => WHOLESALER_CONFIG[id]);
|
||||||
|
await Promise.all(wholesalerConfigs.map(async ws => {
|
||||||
|
try {
|
||||||
|
const salesRes = await fetch(`${ws.salesApi}?year=${year}&month=${month}`);
|
||||||
|
const salesData = await salesRes.json();
|
||||||
|
if (salesData.success && wholesalerLimits[ws.id]) {
|
||||||
|
// 실제 월 매출로 current_usage 업데이트
|
||||||
|
wholesalerLimits[ws.id].current_usage = salesData.total_amount || 0;
|
||||||
|
wholesalerLimits[ws.id].usage_percent = wholesalerLimits[ws.id].monthly_limit > 0
|
||||||
|
? Math.round((salesData.total_amount || 0) / wholesalerLimits[ws.id].monthly_limit * 1000) / 10
|
||||||
|
: 0;
|
||||||
|
wholesalerLimits[ws.id].remaining = wholesalerLimits[ws.id].monthly_limit - (salesData.total_amount || 0);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`${ws.id} 월매출 조회 실패:`, e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
const wsIds = Object.keys(itemsByWholesaler);
|
const wsIds = Object.keys(itemsByWholesaler);
|
||||||
|
|
||||||
// 전체 총액 계산
|
// 전체 총액 계산
|
||||||
@ -1190,14 +1231,44 @@
|
|||||||
wsIds.forEach(wsId => {
|
wsIds.forEach(wsId => {
|
||||||
const ws = WHOLESALERS[wsId];
|
const ws = WHOLESALERS[wsId];
|
||||||
const items = itemsByWholesaler[wsId];
|
const items = itemsByWholesaler[wsId];
|
||||||
|
const limit = wholesalerLimits[wsId];
|
||||||
|
|
||||||
// 도매상별 소계
|
// 도매상별 소계
|
||||||
const wsTotal = items.reduce((sum, item) => sum + (item.unit_price || 0) * item.qty, 0);
|
const wsTotal = items.reduce((sum, item) => sum + (item.unit_price || 0) * item.qty, 0);
|
||||||
|
|
||||||
|
// 한도 정보 계산
|
||||||
|
let limitHtml = '';
|
||||||
|
if (limit) {
|
||||||
|
const afterOrder = limit.current_usage + wsTotal;
|
||||||
|
const afterPercent = (afterOrder / limit.monthly_limit * 100).toFixed(1);
|
||||||
|
const isOver = afterOrder > limit.monthly_limit;
|
||||||
|
const isWarning = afterPercent >= (limit.warning_threshold * 100);
|
||||||
|
|
||||||
|
limitHtml = `
|
||||||
|
<div style="margin-top:8px;padding:8px 12px;background:var(--bg-tertiary);border-radius:6px;font-size:12px;">
|
||||||
|
<div style="display:flex;justify-content:space-between;margin-bottom:4px;">
|
||||||
|
<span>월 한도</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace;">${(limit.monthly_limit/10000).toLocaleString()}만원</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;justify-content:space-between;margin-bottom:4px;">
|
||||||
|
<span>이번달 사용</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace;">${(limit.current_usage/10000).toLocaleString()}만원 (${limit.usage_percent}%)</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;justify-content:space-between;color:${isOver ? 'var(--accent-red)' : isWarning ? 'var(--accent-amber)' : 'var(--accent-emerald)'};">
|
||||||
|
<span>주문 후</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace;font-weight:600;">
|
||||||
|
${(afterOrder/10000).toLocaleString()}만원 (${afterPercent}%)
|
||||||
|
${isOver ? ' ⚠️ 초과!' : isWarning ? ' ⚠️' : ' ✓'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="multi-ws-card ${wsId}">
|
<div class="multi-ws-card ${wsId}">
|
||||||
<div class="multi-ws-header">
|
<div class="multi-ws-header">
|
||||||
<span class="multi-ws-icon">${ws.icon}</span>
|
<img src="${ws.logo}" alt="${ws.name}" style="width:24px;height:24px;object-fit:contain;margin-right:8px;">
|
||||||
<span class="multi-ws-name">${ws.name}</span>
|
<span class="multi-ws-name">${ws.name}</span>
|
||||||
<span class="multi-ws-count">${items.length}개 품목</span>
|
<span class="multi-ws-count">${items.length}개 품목</span>
|
||||||
${wsTotal > 0 ? `<span style="margin-left:auto;margin-right:12px;font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-cyan);">₩${wsTotal.toLocaleString()}</span>` : ''}
|
${wsTotal > 0 ? `<span style="margin-left:auto;margin-right:12px;font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-cyan);">₩${wsTotal.toLocaleString()}</span>` : ''}
|
||||||
@ -1213,6 +1284,7 @@
|
|||||||
}).join('')}
|
}).join('')}
|
||||||
${items.length > 3 ? `<div class="multi-ws-item more">... 외 ${items.length - 3}개</div>` : ''}
|
${items.length > 3 ? `<div class="multi-ws-item more">... 외 ${items.length - 3}개</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
${limitHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
@ -1446,7 +1518,7 @@
|
|||||||
const headerDiv = modal.querySelector('.order-modal-header');
|
const headerDiv = modal.querySelector('.order-modal-header');
|
||||||
|
|
||||||
// 도매상별 헤더 및 본문 텍스트 변경
|
// 도매상별 헤더 및 본문 텍스트 변경
|
||||||
document.getElementById('orderConfirmTitle').innerHTML = `${ws.icon} ${ws.name} 주문 확인`;
|
document.getElementById('orderConfirmTitle').innerHTML = `<img src="${ws.logo}" alt="${ws.name}" style="width:24px;height:24px;object-fit:contain;vertical-align:middle;margin-right:8px;">${ws.name} 주문 확인`;
|
||||||
document.getElementById('orderConfirmWholesaler').textContent = ws.name;
|
document.getElementById('orderConfirmWholesaler').textContent = ws.name;
|
||||||
headerDiv.style.background = ws.gradient;
|
headerDiv.style.background = ws.gradient;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user