pharmacy-pos-qr-system/backend/templates/pmr.html
thug0bin 75448ffdc5 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 엔드포인트 수정
2026-03-04 22:44:54 +09:00

481 lines
18 KiB
HTML

<!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>