feat: PMR 조제관리 - MSSQL(PharmaIT3000) 연동
- pmr_api.py: 192.168.0.4 MSSQL 연결 - /pmr/api/prescriptions: 일별 처방전 목록 - /pmr/api/prescription/<id>: 처방전 상세 - /pmr/api/stats: 당일 통계 - /pmr/api/test: DB 연결 테스트 - pmr.html: API 엔드포인트 수정
This commit is contained in:
parent
1054a9ed17
commit
75448ffdc5
@ -50,6 +50,12 @@ app.config['SESSION_COOKIE_SECURE'] = not app.debug # HTTPS 전용 (로컬 개
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # QR 스캔 시 쿠키 전송 허용
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=90) # 3개월 유지
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Blueprint 등록
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
from pmr_api import pmr_bp
|
||||
app.register_blueprint(pmr_bp)
|
||||
|
||||
# 데이터베이스 매니저
|
||||
db_manager = DatabaseManager()
|
||||
|
||||
|
||||
322
backend/pmr_api.py
Normal file
322
backend/pmr_api.py
Normal file
@ -0,0 +1,322 @@
|
||||
# pmr_api.py - 조제관리(PMR) Blueprint API
|
||||
# PharmaIT3000 MSSQL 연동 (192.168.0.4)
|
||||
|
||||
from flask import Blueprint, jsonify, request, render_template
|
||||
import pyodbc
|
||||
from datetime import datetime, date
|
||||
import logging
|
||||
|
||||
pmr_bp = Blueprint('pmr', __name__, url_prefix='/pmr')
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# MSSQL 연결 설정 (PharmaIT3000 - 192.168.0.4)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
MSSQL_CONFIG = {
|
||||
'server': '192.168.0.4\\PM2014',
|
||||
'username': 'sa',
|
||||
'password': 'tmddls214!%(',
|
||||
'driver': 'ODBC Driver 17 for SQL Server'
|
||||
}
|
||||
|
||||
def get_mssql_connection(database='PM_PRES'):
|
||||
"""MSSQL 연결 획득"""
|
||||
conn_str = (
|
||||
f"DRIVER={{{MSSQL_CONFIG['driver']}}};"
|
||||
f"SERVER={MSSQL_CONFIG['server']};"
|
||||
f"DATABASE={database};"
|
||||
f"UID={MSSQL_CONFIG['username']};"
|
||||
f"PWD={MSSQL_CONFIG['password']};"
|
||||
"TrustServerCertificate=yes;"
|
||||
"Connection Timeout=10"
|
||||
)
|
||||
return pyodbc.connect(conn_str, timeout=10)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 조제관리 페이지
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@pmr_bp.route('/')
|
||||
def pmr_index():
|
||||
"""조제관리 메인 페이지"""
|
||||
return render_template('pmr.html')
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# API: 날짜별 처방전 목록
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@pmr_bp.route('/api/prescriptions', methods=['GET'])
|
||||
def get_prescriptions_by_date():
|
||||
"""
|
||||
날짜별 처방전 목록 조회
|
||||
Query Params:
|
||||
- date: YYYY-MM-DD (기본값: 오늘)
|
||||
"""
|
||||
try:
|
||||
date_str = request.args.get('date', date.today().strftime('%Y-%m-%d'))
|
||||
# YYYYMMDD 형식으로 변환
|
||||
date_yyyymmdd = date_str.replace('-', '')
|
||||
|
||||
conn = get_mssql_connection('PM_PRES')
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
PreSerial,
|
||||
Day_Serial,
|
||||
PassDay,
|
||||
Paname,
|
||||
PaNum,
|
||||
CusCode,
|
||||
InsName,
|
||||
Drname,
|
||||
PresTime,
|
||||
PreGubun,
|
||||
PRICE_T,
|
||||
PRICE_P,
|
||||
PRICE_C
|
||||
FROM PS_MAIN
|
||||
WHERE PassDay = ?
|
||||
ORDER BY Day_Serial ASC
|
||||
""", (date_yyyymmdd,))
|
||||
|
||||
prescriptions = []
|
||||
for row in cursor.fetchall():
|
||||
# 주민번호에서 나이/성별 추출
|
||||
panum = row.PaNum or ''
|
||||
age = None
|
||||
gender = None
|
||||
if len(panum) >= 7:
|
||||
try:
|
||||
birth_year = int(panum[:2])
|
||||
gender_code = panum[6] if len(panum) > 6 else ''
|
||||
|
||||
# 성별 및 세기 판단
|
||||
if gender_code in ['1', '2', '5', '6']:
|
||||
birth_year += 1900
|
||||
elif gender_code in ['3', '4', '7', '8']:
|
||||
birth_year += 2000
|
||||
else:
|
||||
birth_year += 1900
|
||||
|
||||
gender = '남' if gender_code in ['1', '3', '5', '7'] else '여'
|
||||
age = datetime.now().year - birth_year
|
||||
except:
|
||||
pass
|
||||
|
||||
prescriptions.append({
|
||||
'prescription_id': row.PreSerial,
|
||||
'order_number': row.Day_Serial,
|
||||
'date': row.PassDay,
|
||||
'patient_name': row.Paname,
|
||||
'patient_id': row.PaNum[:6] + '******' if row.PaNum and len(row.PaNum) > 6 else row.PaNum,
|
||||
'patient_code': row.CusCode,
|
||||
'hospital': row.InsName,
|
||||
'doctor': row.Drname,
|
||||
'time': row.PresTime,
|
||||
'type': '급여' if row.PreGubun == '0' else '비급여' if row.PreGubun == '9' else row.PreGubun,
|
||||
'age': age,
|
||||
'gender': gender,
|
||||
'price_total': row.PRICE_T,
|
||||
'price_patient': row.PRICE_P,
|
||||
'price_claim': row.PRICE_C
|
||||
})
|
||||
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'date': date_str,
|
||||
'count': len(prescriptions),
|
||||
'prescriptions': prescriptions
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"PMR 처방전 목록 조회 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# API: 처방전 상세 (약품 목록)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@pmr_bp.route('/api/prescription/<prescription_id>', methods=['GET'])
|
||||
def get_prescription_detail(prescription_id):
|
||||
"""
|
||||
처방전 상세 정보 (약품 목록 포함)
|
||||
"""
|
||||
try:
|
||||
conn = get_mssql_connection('PM_PRES')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 처방전 기본 정보
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
PreSerial,
|
||||
Day_Serial,
|
||||
PassDay,
|
||||
Paname,
|
||||
PaNum,
|
||||
CusCode,
|
||||
InsName,
|
||||
Drname,
|
||||
PresTime,
|
||||
PreGubun,
|
||||
PRICE_T,
|
||||
PRICE_P,
|
||||
PRICE_C
|
||||
FROM PS_MAIN
|
||||
WHERE PreSerial = ?
|
||||
""", (prescription_id,))
|
||||
|
||||
rx_row = cursor.fetchone()
|
||||
if not rx_row:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '처방전을 찾을 수 없습니다'}), 404
|
||||
|
||||
# 처방 약품 목록 (PS_sub_pharm 테이블 확인 필요)
|
||||
cursor.execute("""
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'PS_sub_pharm'
|
||||
""")
|
||||
sub_columns = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# 약품 목록 조회
|
||||
medications = []
|
||||
if 'PreSerial' in sub_columns:
|
||||
cursor.execute("""
|
||||
SELECT *
|
||||
FROM PS_sub_pharm
|
||||
WHERE PreSerial = ?
|
||||
ORDER BY 1
|
||||
""", (prescription_id,))
|
||||
|
||||
med_columns = [col[0] for col in cursor.description]
|
||||
for row in cursor.fetchall():
|
||||
med_dict = dict(zip(med_columns, row))
|
||||
medications.append({
|
||||
'medication_code': med_dict.get('GoodsCode') or med_dict.get('PCODE') or '',
|
||||
'med_name': med_dict.get('GoodsName') or med_dict.get('PNAME') or '',
|
||||
'dosage': med_dict.get('Once_Qty') or med_dict.get('POESSION') or 0,
|
||||
'frequency': med_dict.get('Times') or med_dict.get('PTIMES') or 0,
|
||||
'duration': med_dict.get('Days') or med_dict.get('PDAY') or 0,
|
||||
'total_qty': med_dict.get('Total_Qty') or 0,
|
||||
'type': med_dict.get('PS_Type', '0')
|
||||
})
|
||||
|
||||
conn.close()
|
||||
|
||||
# 나이/성별 계산
|
||||
panum = rx_row.PaNum or ''
|
||||
age = None
|
||||
gender = None
|
||||
if len(panum) >= 7:
|
||||
try:
|
||||
birth_year = int(panum[:2])
|
||||
gender_code = panum[6]
|
||||
if gender_code in ['1', '2', '5', '6']:
|
||||
birth_year += 1900
|
||||
elif gender_code in ['3', '4', '7', '8']:
|
||||
birth_year += 2000
|
||||
gender = '남' if gender_code in ['1', '3', '5', '7'] else '여'
|
||||
age = datetime.now().year - birth_year
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'prescription': {
|
||||
'prescription_id': rx_row.PreSerial,
|
||||
'order_number': rx_row.Day_Serial,
|
||||
'date': rx_row.PassDay,
|
||||
'time': rx_row.PresTime,
|
||||
'hospital': rx_row.InsName,
|
||||
'doctor': rx_row.Drname,
|
||||
'type': '급여' if rx_row.PreGubun == '0' else '비급여',
|
||||
'price_total': rx_row.PRICE_T,
|
||||
'price_patient': rx_row.PRICE_P
|
||||
},
|
||||
'patient': {
|
||||
'name': rx_row.Paname,
|
||||
'code': rx_row.CusCode,
|
||||
'age': age,
|
||||
'gender': gender
|
||||
},
|
||||
'medications': medications,
|
||||
'medication_count': len(medications)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"PMR 처방전 상세 조회 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# API: 통계 (당일 요약)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@pmr_bp.route('/api/stats', methods=['GET'])
|
||||
def get_daily_stats():
|
||||
"""당일 조제 통계"""
|
||||
try:
|
||||
date_str = request.args.get('date', date.today().strftime('%Y-%m-%d'))
|
||||
date_yyyymmdd = date_str.replace('-', '')
|
||||
|
||||
conn = get_mssql_connection('PM_PRES')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 처방전 수
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as cnt
|
||||
FROM PS_MAIN
|
||||
WHERE PassDay = ?
|
||||
""", (date_yyyymmdd,))
|
||||
total_prescriptions = cursor.fetchone()[0]
|
||||
|
||||
# 총 금액
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
ISNULL(SUM(PRICE_T), 0) as total,
|
||||
ISNULL(SUM(PRICE_P), 0) as patient,
|
||||
ISNULL(SUM(PRICE_C), 0) as claim
|
||||
FROM PS_MAIN
|
||||
WHERE PassDay = ?
|
||||
""", (date_yyyymmdd,))
|
||||
price_row = cursor.fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'date': date_str,
|
||||
'stats': {
|
||||
'total_prescriptions': total_prescriptions,
|
||||
'total_amount': price_row[0] if price_row else 0,
|
||||
'patient_amount': price_row[1] if price_row else 0,
|
||||
'claim_amount': price_row[2] if price_row else 0
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"PMR 통계 조회 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# API: DB 연결 테스트
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@pmr_bp.route('/api/test', methods=['GET'])
|
||||
def test_connection():
|
||||
"""DB 연결 테스트"""
|
||||
try:
|
||||
conn = get_mssql_connection('PM_PRES')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT @@VERSION")
|
||||
version = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'server': MSSQL_CONFIG['server'],
|
||||
'version': version[:100] + '...'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
480
backend/templates/pmr.html
Normal file
480
backend/templates/pmr.html
Normal file
@ -0,0 +1,480 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>조제관리 - 청춘라벨</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Malgun Gothic', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 헤더 */
|
||||
.header {
|
||||
background: rgba(255,255,255,0.95);
|
||||
padding: 15px 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
color: #4c1d95;
|
||||
}
|
||||
.header h1 span { font-size: 0.9rem; color: #6b7280; margin-left: 10px; }
|
||||
|
||||
/* 날짜 선택 & 통계 */
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
.date-picker {
|
||||
padding: 8px 15px;
|
||||
border: 2px solid #8b5cf6;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
color: #4c1d95;
|
||||
cursor: pointer;
|
||||
}
|
||||
.stats-box {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
.stat-item {
|
||||
background: #f3e8ff;
|
||||
padding: 8px 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-item .num { font-size: 1.3rem; font-weight: bold; color: #7c3aed; }
|
||||
.stat-item .label { font-size: 0.75rem; color: #6b7280; }
|
||||
|
||||
/* 메인 컨테이너 */
|
||||
.main-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 80px);
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 왼쪽: 환자 목록 */
|
||||
.patient-list {
|
||||
width: 380px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.patient-list-header {
|
||||
background: #4c1d95;
|
||||
color: #fff;
|
||||
padding: 15px 20px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.patient-list-header .count {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.patient-items {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
.patient-card {
|
||||
background: #f8fafc;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 10px;
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.patient-card:hover { background: #ede9fe; border-color: #c4b5fd; }
|
||||
.patient-card.active { background: #ddd6fe; border-color: #8b5cf6; }
|
||||
.patient-card .top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
||||
.patient-card .name { font-size: 1.1rem; font-weight: 600; color: #1e1b4b; }
|
||||
.patient-card .order {
|
||||
background: #8b5cf6;
|
||||
color: #fff;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.patient-card .info { font-size: 0.85rem; color: #64748b; }
|
||||
.patient-card .hospital { font-size: 0.8rem; color: #8b5cf6; margin-top: 4px; }
|
||||
|
||||
/* 오른쪽: 처방 상세 */
|
||||
.prescription-detail {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.detail-header {
|
||||
background: linear-gradient(135deg, #7c3aed, #a855f7);
|
||||
color: #fff;
|
||||
padding: 20px 25px;
|
||||
}
|
||||
.detail-header .patient-name { font-size: 1.5rem; font-weight: 700; }
|
||||
.detail-header .patient-info { font-size: 0.9rem; opacity: 0.9; margin-top: 5px; }
|
||||
.detail-header .rx-info {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 12px;
|
||||
font-size: 0.85rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.detail-header .rx-info span {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 12px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
/* 약품 목록 */
|
||||
.medication-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.med-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.med-table th {
|
||||
background: #f1f5f9;
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
.med-table td {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.med-table tr:hover { background: #f8fafc; }
|
||||
.med-name { font-weight: 600; color: #1e293b; }
|
||||
.med-code { font-size: 0.75rem; color: #94a3b8; }
|
||||
.med-dosage {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
}
|
||||
.med-form {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* 빈 상태 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.empty-state .icon { font-size: 4rem; margin-bottom: 15px; }
|
||||
.empty-state .text { font-size: 1.1rem; }
|
||||
|
||||
/* 액션 버튼 */
|
||||
.action-bar {
|
||||
background: #f8fafc;
|
||||
padding: 15px 25px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 25px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary { background: #7c3aed; color: #fff; }
|
||||
.btn-primary:hover { background: #6d28d9; }
|
||||
.btn-secondary { background: #e2e8f0; color: #475569; }
|
||||
.btn-secondary:hover { background: #cbd5e1; }
|
||||
|
||||
/* 로딩 */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #e2e8f0;
|
||||
border-top-color: #7c3aed;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 헤더 -->
|
||||
<header class="header">
|
||||
<h1>💊 조제관리 <span>청춘라벨 v2</span></h1>
|
||||
<div class="controls">
|
||||
<input type="date" id="dateSelect" class="date-picker">
|
||||
<div class="stats-box">
|
||||
<div class="stat-item">
|
||||
<div class="num" id="statPrescriptions">-</div>
|
||||
<div class="label">처방</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="num" id="statPatients">-</div>
|
||||
<div class="label">환자</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="num" id="statMedications">-</div>
|
||||
<div class="label">금액</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 메인 -->
|
||||
<div class="main-container">
|
||||
<!-- 왼쪽: 환자 목록 -->
|
||||
<div class="patient-list">
|
||||
<div class="patient-list-header">
|
||||
<span>📋 환자 목록</span>
|
||||
<span class="count" id="patientCount">0명</span>
|
||||
</div>
|
||||
<div class="patient-items" id="patientItems">
|
||||
<div class="loading"><div class="spinner"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 오른쪽: 처방 상세 -->
|
||||
<div class="prescription-detail">
|
||||
<div class="detail-header" id="detailHeader" style="display:none;">
|
||||
<div class="patient-name" id="detailName">-</div>
|
||||
<div class="patient-info" id="detailInfo">-</div>
|
||||
<div class="rx-info" id="rxInfo"></div>
|
||||
</div>
|
||||
<div class="medication-list" id="medicationList">
|
||||
<div class="empty-state">
|
||||
<div class="icon">👈</div>
|
||||
<div class="text">환자를 선택하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-bar" id="actionBar" style="display:none;">
|
||||
<button class="btn btn-secondary" onclick="selectAll()">전체 선택</button>
|
||||
<button class="btn btn-primary" onclick="printLabels()">🖨️ 라벨 인쇄</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPrescriptionId = null;
|
||||
|
||||
// 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('dateSelect').value = today;
|
||||
loadPatients(today);
|
||||
loadStats(today);
|
||||
});
|
||||
|
||||
// 날짜 변경
|
||||
document.getElementById('dateSelect').addEventListener('change', (e) => {
|
||||
loadPatients(e.target.value);
|
||||
loadStats(e.target.value);
|
||||
clearDetail();
|
||||
});
|
||||
|
||||
// 처방전 목록 로드
|
||||
async function loadPatients(date) {
|
||||
const container = document.getElementById('patientItems');
|
||||
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/pmr/api/prescriptions?date=${date}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.prescriptions.length > 0) {
|
||||
container.innerHTML = data.prescriptions.map(p => `
|
||||
<div class="patient-card" onclick="selectPatient('${p.prescription_id}', this)">
|
||||
<div class="top">
|
||||
<span class="name">${p.patient_name || '이름없음'}</span>
|
||||
<span class="order">${p.order_number || '-'}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
${p.age ? p.age + '세' : ''} ${p.gender || ''}
|
||||
${p.time ? '• ' + p.time : ''}
|
||||
</div>
|
||||
<div class="hospital">${p.hospital || ''} ${p.doctor ? '(' + p.doctor + ')' : ''}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('patientCount').textContent = data.count + '명';
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="icon">📭</div>
|
||||
<div class="text">해당 날짜에 처방이 없습니다</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('patientCount').textContent = '0명';
|
||||
}
|
||||
} catch (err) {
|
||||
container.innerHTML = `<div class="empty-state"><div class="text">오류: ${err.message}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 로드
|
||||
async function loadStats(date) {
|
||||
try {
|
||||
const res = await fetch(`/pmr/api/stats?date=${date}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
document.getElementById('statPatients').textContent = '-';
|
||||
document.getElementById('statPrescriptions').textContent = data.stats.total_prescriptions;
|
||||
document.getElementById('statMedications').textContent = Math.round(data.stats.total_amount / 10000) + '만';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Stats error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 환자 선택
|
||||
async function selectPatient(prescriptionId, element) {
|
||||
// UI 활성화
|
||||
document.querySelectorAll('.patient-card').forEach(c => c.classList.remove('active'));
|
||||
element.classList.add('active');
|
||||
currentPrescriptionId = prescriptionId;
|
||||
|
||||
const medList = document.getElementById('medicationList');
|
||||
medList.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/pmr/api/prescription/${prescriptionId}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
// 헤더 업데이트
|
||||
document.getElementById('detailHeader').style.display = 'block';
|
||||
document.getElementById('detailName').textContent = data.patient.name || '이름없음';
|
||||
document.getElementById('detailInfo').textContent =
|
||||
`${data.patient.age || '-'}세 / ${data.patient.gender || '-'} / ${data.patient.birthdate || '-'}`;
|
||||
|
||||
document.getElementById('rxInfo').innerHTML = `
|
||||
<span>🏥 ${data.prescription.hospital || '-'}</span>
|
||||
<span>👨⚕️ ${data.prescription.doctor || '-'}</span>
|
||||
<span>📅 ${data.prescription.date}</span>
|
||||
<span>💊 ${data.medication_count}종</span>
|
||||
`;
|
||||
|
||||
// 약품 테이블
|
||||
if (data.medications.length > 0) {
|
||||
medList.innerHTML = `
|
||||
<table class="med-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;"><input type="checkbox" id="checkAll" onchange="toggleAll(this)"></th>
|
||||
<th>약품명</th>
|
||||
<th>제형</th>
|
||||
<th>용량</th>
|
||||
<th>횟수</th>
|
||||
<th>일수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${data.medications.map(m => `
|
||||
<tr>
|
||||
<td><input type="checkbox" class="med-check" data-code="${m.medication_code}" ${m.is_auto_print ? 'checked' : ''}></td>
|
||||
<td>
|
||||
<div class="med-name">${m.med_name || m.medication_code}</div>
|
||||
<div class="med-code">${m.medication_code}</div>
|
||||
</td>
|
||||
<td>${m.formulation ? `<span class="med-form">${m.formulation}</span>` : '-'}</td>
|
||||
<td><span class="med-dosage">${m.dosage || '-'}</span></td>
|
||||
<td>${m.frequency || '-'}회</td>
|
||||
<td>${m.duration || '-'}일</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} else {
|
||||
medList.innerHTML = '<div class="empty-state"><div class="text">처방 약품이 없습니다</div></div>';
|
||||
}
|
||||
|
||||
document.getElementById('actionBar').style.display = 'flex';
|
||||
}
|
||||
} catch (err) {
|
||||
medList.innerHTML = `<div class="empty-state"><div class="text">오류: ${err.message}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 상세 초기화
|
||||
function clearDetail() {
|
||||
document.getElementById('detailHeader').style.display = 'none';
|
||||
document.getElementById('actionBar').style.display = 'none';
|
||||
document.getElementById('medicationList').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="icon">👈</div>
|
||||
<div class="text">환자를 선택하세요</div>
|
||||
</div>
|
||||
`;
|
||||
currentPrescriptionId = null;
|
||||
}
|
||||
|
||||
// 전체 선택 토글
|
||||
function toggleAll(checkbox) {
|
||||
document.querySelectorAll('.med-check').forEach(c => c.checked = checkbox.checked);
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
document.querySelectorAll('.med-check').forEach(c => c.checked = true);
|
||||
const checkAll = document.getElementById('checkAll');
|
||||
if (checkAll) checkAll.checked = true;
|
||||
}
|
||||
|
||||
// 라벨 인쇄 (TODO: 구현)
|
||||
function printLabels() {
|
||||
const selected = Array.from(document.querySelectorAll('.med-check:checked')).map(c => c.dataset.code);
|
||||
if (selected.length === 0) {
|
||||
alert('인쇄할 약품을 선택하세요');
|
||||
return;
|
||||
}
|
||||
alert(`선택된 약품 ${selected.length}개 인쇄 기능은 추후 구현 예정입니다.\n\n${selected.join('\n')}`);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user