feat: 회원 상세 - 전체 구매이력 + 조제이력 탭 추가
- 전화번호 → CD_PERSON(CUSCODE) 매핑 - 구매 탭: SALE_MAIN/SALE_SUB (전체 POS 구매) - 조제 탭: PS_main/PS_sub_pharm (처방전 조제) - 병원명, 의사명, 투약일수, 처방 약품 표시 - POS 미등록 회원 안내 메시지 추가
This commit is contained in:
parent
8c3bcb525d
commit
02e56b9413
136
backend/app.py
136
backend/app.py
@ -3010,6 +3010,8 @@ def api_member_history(phone):
|
|||||||
회원 구매 이력 통합 조회 API
|
회원 구매 이력 통합 조회 API
|
||||||
- 마일리지 적립/사용 내역 (SQLite)
|
- 마일리지 적립/사용 내역 (SQLite)
|
||||||
- transaction_id로 POS 품목 조회 (MSSQL)
|
- transaction_id로 POS 품목 조회 (MSSQL)
|
||||||
|
- 전체 구매 이력 (SALE_MAIN)
|
||||||
|
- 조제 이력 (PS_main)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 전화번호 정규화
|
# 전화번호 정규화
|
||||||
@ -3019,7 +3021,9 @@ def api_member_history(phone):
|
|||||||
'success': True,
|
'success': True,
|
||||||
'phone': phone,
|
'phone': phone,
|
||||||
'mileage': None,
|
'mileage': None,
|
||||||
'purchases': []
|
'purchases': [], # 전체 구매 이력
|
||||||
|
'prescriptions': [], # 조제 이력
|
||||||
|
'pos_customer': None
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction_ids = [] # 적립된 거래번호 수집
|
transaction_ids = [] # 적립된 거래번호 수집
|
||||||
@ -3082,7 +3086,29 @@ def api_member_history(phone):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"마일리지 조회 실패: {e}")
|
logging.warning(f"마일리지 조회 실패: {e}")
|
||||||
|
|
||||||
# 2. transaction_id로 POS 품목 조회 (MSSQL)
|
# 2. 전화번호로 POS 고객코드 조회 (MSSQL)
|
||||||
|
cuscode = None
|
||||||
|
try:
|
||||||
|
base_session = db_manager.get_session('PM_BASE')
|
||||||
|
cuscode_query = text("""
|
||||||
|
SELECT TOP 1 CUSCODE, PANAME
|
||||||
|
FROM CD_PERSON
|
||||||
|
WHERE REPLACE(REPLACE(PHONE, '-', ''), ' ', '') = :phone
|
||||||
|
OR REPLACE(REPLACE(TEL_NO, '-', ''), ' ', '') = :phone
|
||||||
|
OR REPLACE(REPLACE(PHONE2, '-', ''), ' ', '') = :phone
|
||||||
|
""")
|
||||||
|
cus_row = base_session.execute(cuscode_query, {'phone': phone}).fetchone()
|
||||||
|
|
||||||
|
if cus_row:
|
||||||
|
cuscode = cus_row.CUSCODE
|
||||||
|
result['pos_customer'] = {
|
||||||
|
'cuscode': cuscode,
|
||||||
|
'name': cus_row.PANAME
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"POS 고객 조회 실패: {e}")
|
||||||
|
|
||||||
|
# 3. transaction_id로 POS 품목 조회 (마일리지 적립 내역)
|
||||||
if transaction_ids and result['mileage']:
|
if transaction_ids and result['mileage']:
|
||||||
try:
|
try:
|
||||||
pres_session = db_manager.get_session('PM_PRES')
|
pres_session = db_manager.get_session('PM_PRES')
|
||||||
@ -3118,6 +3144,112 @@ def api_member_history(phone):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"POS 품목 조회 실패: {e}")
|
logging.warning(f"POS 품목 조회 실패: {e}")
|
||||||
|
|
||||||
|
# 4. 전체 구매 이력 조회 (고객코드 기준)
|
||||||
|
if cuscode:
|
||||||
|
try:
|
||||||
|
pres_session = db_manager.get_session('PM_PRES')
|
||||||
|
|
||||||
|
# 최근 60일 구매 이력
|
||||||
|
purchase_query = text("""
|
||||||
|
SELECT TOP 30
|
||||||
|
M.SL_NO_order as order_no,
|
||||||
|
M.SL_DT_appl as order_date,
|
||||||
|
M.SL_MY_total as total_amount
|
||||||
|
FROM SALE_MAIN M
|
||||||
|
WHERE M.SL_CD_custom = :cuscode
|
||||||
|
ORDER BY M.SL_DT_appl DESC, M.SL_NO_order DESC
|
||||||
|
""")
|
||||||
|
orders = pres_session.execute(purchase_query, {'cuscode': cuscode}).fetchall()
|
||||||
|
|
||||||
|
purchases = []
|
||||||
|
for order in orders:
|
||||||
|
# 품목 조회
|
||||||
|
items_query = text("""
|
||||||
|
SELECT
|
||||||
|
S.DrugCode,
|
||||||
|
G.GoodsName,
|
||||||
|
S.QUAN as quantity,
|
||||||
|
S.SL_TOTAL_PRICE as price
|
||||||
|
FROM SALE_SUB S
|
||||||
|
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
|
||||||
|
WHERE S.SL_NO_order = :order_no
|
||||||
|
""")
|
||||||
|
items = pres_session.execute(items_query, {'order_no': order.order_no}).fetchall()
|
||||||
|
|
||||||
|
purchases.append({
|
||||||
|
'order_no': order.order_no,
|
||||||
|
'date': order.order_date,
|
||||||
|
'total': float(order.total_amount) if order.total_amount else 0,
|
||||||
|
'items': [{
|
||||||
|
'drug_code': item.DrugCode,
|
||||||
|
'name': item.GoodsName or '알 수 없음',
|
||||||
|
'quantity': float(item.quantity) if item.quantity else 1,
|
||||||
|
'price': float(item.price) if item.price else 0
|
||||||
|
} for item in items]
|
||||||
|
})
|
||||||
|
|
||||||
|
result['purchases'] = purchases
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"전체 구매 이력 조회 실패: {e}")
|
||||||
|
|
||||||
|
# 5. 조제 이력 조회 (고객코드 기준)
|
||||||
|
if cuscode:
|
||||||
|
try:
|
||||||
|
pres_session = db_manager.get_session('PM_PRES')
|
||||||
|
|
||||||
|
# 최근 조제 이력 (최대 30건)
|
||||||
|
rx_query = text("""
|
||||||
|
SELECT TOP 30
|
||||||
|
P.PreSerial,
|
||||||
|
P.Indate,
|
||||||
|
P.Paname,
|
||||||
|
P.Drname,
|
||||||
|
P.OrderName,
|
||||||
|
P.TDAYS
|
||||||
|
FROM PS_main P
|
||||||
|
WHERE P.CusCode = :cuscode
|
||||||
|
ORDER BY P.Indate DESC, P.PreSerial DESC
|
||||||
|
""")
|
||||||
|
rxs = pres_session.execute(rx_query, {'cuscode': cuscode}).fetchall()
|
||||||
|
|
||||||
|
prescriptions = []
|
||||||
|
for rx in rxs:
|
||||||
|
# 처방 품목 조회
|
||||||
|
items_query = text("""
|
||||||
|
SELECT
|
||||||
|
S.DrugCode,
|
||||||
|
G.GoodsName,
|
||||||
|
S.Days,
|
||||||
|
S.QUAN,
|
||||||
|
S.QUAN_TIME
|
||||||
|
FROM PS_sub_pharm S
|
||||||
|
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
|
||||||
|
WHERE S.PreSerial = :pre_serial
|
||||||
|
""")
|
||||||
|
items = pres_session.execute(items_query, {'pre_serial': rx.PreSerial}).fetchall()
|
||||||
|
|
||||||
|
prescriptions.append({
|
||||||
|
'pre_serial': rx.PreSerial,
|
||||||
|
'date': rx.Indate,
|
||||||
|
'patient_name': rx.Paname,
|
||||||
|
'doctor': rx.Drname,
|
||||||
|
'hospital': rx.OrderName,
|
||||||
|
'total_days': rx.TDAYS,
|
||||||
|
'items': [{
|
||||||
|
'drug_code': item.DrugCode,
|
||||||
|
'name': item.GoodsName or '알 수 없음',
|
||||||
|
'days': float(item.Days) if item.Days else 0,
|
||||||
|
'quantity': float(item.QUAN) if item.QUAN else 0,
|
||||||
|
'times_per_day': float(item.QUAN_TIME) if item.QUAN_TIME else 0
|
||||||
|
} for item in items]
|
||||||
|
})
|
||||||
|
|
||||||
|
result['prescriptions'] = prescriptions
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"조제 이력 조회 실패: {e}")
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -610,8 +610,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-tabs">
|
<div class="detail-tabs">
|
||||||
<div class="detail-tab active" data-tab="mileage" onclick="switchDetailTab('mileage')">📊 적립 내역</div>
|
<div class="detail-tab active" data-tab="mileage" onclick="switchDetailTab('mileage')">📊 적립</div>
|
||||||
<div class="detail-tab" data-tab="purchase" onclick="switchDetailTab('purchase')">🛒 구매 이력</div>
|
<div class="detail-tab" data-tab="purchase" onclick="switchDetailTab('purchase')">🛒 구매</div>
|
||||||
|
<div class="detail-tab" data-tab="prescription" onclick="switchDetailTab('prescription')">💊 조제</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-content" id="detailContent">
|
<div class="detail-content" id="detailContent">
|
||||||
<div class="detail-loading">데이터를 불러오는 중...</div>
|
<div class="detail-loading">데이터를 불러오는 중...</div>
|
||||||
@ -888,8 +889,10 @@
|
|||||||
|
|
||||||
if (currentDetailTab === 'mileage') {
|
if (currentDetailTab === 'mileage') {
|
||||||
renderMileageTab(content);
|
renderMileageTab(content);
|
||||||
} else {
|
} else if (currentDetailTab === 'purchase') {
|
||||||
renderPurchaseTab(content);
|
renderPurchaseTab(content);
|
||||||
|
} else if (currentDetailTab === 'prescription') {
|
||||||
|
renderPrescriptionTab(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -941,26 +944,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPurchaseTab(container) {
|
function renderPurchaseTab(container) {
|
||||||
// 적립 내역 중 품목이 있는 것들만 추출
|
// POS 전체 구매 이력 (고객코드 기준)
|
||||||
if (!detailData.mileage || !detailData.mileage.transactions) {
|
if (!detailData.purchases || detailData.purchases.length === 0) {
|
||||||
container.innerHTML = '<div class="detail-empty">📭 구매 이력이 없습니다</div>';
|
if (!detailData.pos_customer) {
|
||||||
return;
|
container.innerHTML = '<div class="detail-empty">📭 POS 회원으로 등록되지 않았습니다<br><small style="color:#94a3b8;">전화번호가 POS에 등록되면 구매 이력이 표시됩니다</small></div>';
|
||||||
}
|
} else {
|
||||||
|
container.innerHTML = '<div class="detail-empty">📭 구매 이력이 없습니다</div>';
|
||||||
const purchases = detailData.mileage.transactions.filter(tx =>
|
}
|
||||||
tx.items && tx.items.length > 0 && tx.points > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (purchases.length === 0) {
|
|
||||||
container.innerHTML = '<div class="detail-empty">📭 QR 적립된 구매 이력이 없습니다</div>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = purchases.map(p => {
|
container.innerHTML = detailData.purchases.map(p => {
|
||||||
// 날짜 포맷
|
// 날짜 포맷
|
||||||
const date = p.created_at ? new Date(p.created_at).toLocaleString('ko-KR', {
|
const dateStr = p.date || '';
|
||||||
year: 'numeric', month: 'short', day: 'numeric'
|
let formattedDate = dateStr;
|
||||||
}) : '';
|
if (dateStr.length === 8) {
|
||||||
|
formattedDate = `${dateStr.slice(0,4)}.${dateStr.slice(4,6)}.${dateStr.slice(6,8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
// 품목 렌더링
|
// 품목 렌더링
|
||||||
const itemsHtml = (p.items || []).map(item => `
|
const itemsHtml = (p.items || []).map(item => `
|
||||||
@ -974,10 +974,9 @@
|
|||||||
return `
|
return `
|
||||||
<div class="purchase-card">
|
<div class="purchase-card">
|
||||||
<div class="purchase-header">
|
<div class="purchase-header">
|
||||||
<span class="purchase-date">📅 ${date}</span>
|
<span class="purchase-date">📅 ${formattedDate}</span>
|
||||||
<span class="purchase-total">${(p.total_amount || 0).toLocaleString()}원</span>
|
<span class="purchase-total">${(p.total || 0).toLocaleString()}원</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:12px;color:#10b981;margin-bottom:8px;">+${p.points.toLocaleString()}P 적립</div>
|
|
||||||
${p.items && p.items.length > 0 ? `
|
${p.items && p.items.length > 0 ? `
|
||||||
<div class="purchase-items">${itemsHtml}</div>
|
<div class="purchase-items">${itemsHtml}</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
@ -985,6 +984,50 @@
|
|||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPrescriptionTab(container) {
|
||||||
|
// 조제 이력 (고객코드 기준)
|
||||||
|
if (!detailData.prescriptions || detailData.prescriptions.length === 0) {
|
||||||
|
if (!detailData.pos_customer) {
|
||||||
|
container.innerHTML = '<div class="detail-empty">📭 POS 회원으로 등록되지 않았습니다<br><small style="color:#94a3b8;">전화번호가 POS에 등록되면 조제 이력이 표시됩니다</small></div>';
|
||||||
|
} else {
|
||||||
|
container.innerHTML = '<div class="detail-empty">📭 조제 이력이 없습니다</div>';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = detailData.prescriptions.map(rx => {
|
||||||
|
// 날짜 포맷
|
||||||
|
const dateStr = rx.date || '';
|
||||||
|
let formattedDate = dateStr;
|
||||||
|
if (dateStr.length === 8) {
|
||||||
|
formattedDate = `${dateStr.slice(0,4)}.${dateStr.slice(4,6)}.${dateStr.slice(6,8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 처방 품목 렌더링
|
||||||
|
const itemsHtml = (rx.items || []).map(item => `
|
||||||
|
<div class="purchase-item">
|
||||||
|
<span class="purchase-item-name">${escapeHtml(item.name)}</span>
|
||||||
|
<span class="purchase-item-qty" style="min-width:70px;">${item.days}일 x${item.times_per_day}회</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="purchase-card" style="border-left: 3px solid #6366f1;">
|
||||||
|
<div class="purchase-header">
|
||||||
|
<span class="purchase-date">📅 ${formattedDate}</span>
|
||||||
|
<span style="font-size:12px;color:#6366f1;">${rx.total_days || ''}일분</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:#64748b;margin-bottom:8px;">
|
||||||
|
🏥 ${escapeHtml(rx.hospital || '')} · ${escapeHtml(rx.doctor || '')}
|
||||||
|
</div>
|
||||||
|
${rx.items && rx.items.length > 0 ? `
|
||||||
|
<div class="purchase-items">${itemsHtml}</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
function openSendFromDetail() {
|
function openSendFromDetail() {
|
||||||
if (!currentDetailMember) return;
|
if (!currentDetailMember) return;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user