pharmacy-pos-qr-system/backend/templates/pmr.html
thug0bin fc2db78816 feat: PMR 효능효과(add_info) 추가
- CD_MC.PRINT_TYPE JOIN으로 효능 조회
- 약품 테이블에 효능 표시
- 라벨 미리보기에 효능 포함
2026-03-04 22:56:28 +09:00

578 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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-secondary" onclick="previewLabels()" style="background:#3b82f6;color:#fff;">👁️ 미리보기</button>
<button class="btn btn-primary" onclick="printLabels()">🖨️ 라벨 인쇄</button>
</div>
<!-- 미리보기 모달 -->
<div id="previewModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:1000;overflow-y:auto;">
<div style="max-width:400px;margin:50px auto;background:#fff;border-radius:12px;padding:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h3 style="margin:0;color:#4c1d95;">🏷️ 라벨 미리보기</h3>
<button onclick="closePreview()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;">×</button>
</div>
<div id="previewContent" style="display:flex;flex-direction:column;gap:15px;align-items:center;">
</div>
</div>
</div>
</div>
</div>
<script>
let currentPrescriptionId = null;
// HTML 이스케이프
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 초기화
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 data-add-info="${escapeHtml(m.add_info || '')}">
<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>
${m.add_info ? `<div style="font-size:0.75rem;color:#6b7280;">${escapeHtml(m.add_info)}</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;
}
// 라벨 미리보기
async function previewLabels() {
const checkboxes = document.querySelectorAll('.med-check:checked');
if (checkboxes.length === 0) {
alert('미리보기할 약품을 선택하세요');
return;
}
const container = document.getElementById('previewContent');
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
document.getElementById('previewModal').style.display = 'block';
// 현재 선택된 환자명
const patientName = document.getElementById('detailName').textContent;
container.innerHTML = '';
for (const checkbox of checkboxes) {
const tr = checkbox.closest('tr');
const cells = tr.querySelectorAll('td');
// 약품명: 두 번째 셀의 .med-name
const medName = tr.querySelector('.med-name')?.textContent?.trim() || '';
const addInfo = tr.dataset.addInfo || '';
// 용량: 네 번째 셀 (index 3)
const dosageText = cells[3]?.textContent?.replace(/[^0-9.]/g, '') || '0';
const dosage = parseFloat(dosageText) || 0;
// 횟수: 다섯 번째 셀 (index 4)
const freqText = cells[4]?.textContent?.replace(/[^0-9]/g, '') || '0';
const frequency = parseInt(freqText) || 0;
// 일수: 여섯 번째 셀 (index 5)
const durText = cells[5]?.textContent?.replace(/[^0-9]/g, '') || '0';
const duration = parseInt(durText) || 0;
console.log('Preview data:', { patientName, medName, addInfo, dosage, frequency, duration });
try {
const res = await fetch('/pmr/api/label/preview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
patient_name: patientName,
med_name: medName,
add_info: addInfo,
dosage: dosage,
frequency: frequency,
duration: duration,
unit: '정'
})
});
const data = await res.json();
console.log('Preview response:', data.success, data.error);
if (data.success && data.image) {
const img = document.createElement('img');
img.src = data.image;
img.style.cssText = 'max-width:100%;border:1px solid #ddd;border-radius:8px;';
container.appendChild(img);
} else {
console.error('Preview failed:', data.error);
}
} catch (err) {
console.error('Preview error:', err);
}
}
if (container.children.length === 0) {
container.innerHTML = '<p style="color:#999;">미리보기 생성 실패 - 콘솔(F12) 확인</p>';
}
}
function closePreview() {
document.getElementById('previewModal').style.display = 'none';
}
// 라벨 인쇄 (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>