kdrug-inventory-system/static/app.js
시골약사 2ca5622bbd feat: 의약품 마스터 DB 연동 및 한약재/OTC 구분 체계 구축
- herb_items 테이블에 product_type, standard_code 컬럼 추가
- POST /api/purchase-receipts/from-cart API 구현 (표준코드 기반 입고)
- 5개 API에 product_type/standard_code 필드 추가
- 프론트엔드 전역 구분 표시: 한약재/OTC 배지, 보험코드/표준코드 구분
- 경방신약 주문 매핑 문서 작성 (38건, 총액 1,561,800원)
- DB 스키마 백업 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 13:39:59 +00:00

4791 lines
209 KiB
JavaScript
Raw Permalink 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.

// 한약 재고관리 시스템 - Frontend JavaScript
// 원래 처방 구성 저장용 전역 변수
let originalFormulaIngredients = {};
// 로트 배분 관련 전역 변수
let currentLotAllocation = {
herbId: null,
requiredQty: 0,
row: null,
data: null
};
// 재고 계산 모드 (localStorage에 저장)
let inventoryCalculationMode = localStorage.getItem('inventoryMode') || 'all';
// ==================== 포맷팅 함수 ====================
// 전화번호 포맷팅 (010-1234-5678 형식)
function formatPhoneNumber(phone) {
if (!phone) return '-';
// 숫자만 추출
const cleaned = phone.replace(/\D/g, '');
// 길이에 따라 다른 포맷 적용
if (cleaned.length === 11) {
// 010-1234-5678
return cleaned.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
} else if (cleaned.length === 10) {
// 02-1234-5678 또는 031-123-4567
if (cleaned.startsWith('02')) {
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '$1-$2-$3');
} else {
return cleaned.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3');
}
} else if (cleaned.length === 9) {
// 02-123-4567
return cleaned.replace(/(\d{2})(\d{3})(\d{4})/, '$1-$2-$3');
}
return phone; // 포맷팅할 수 없는 경우 원본 반환
}
// 주민번호 포맷팅 (980520-1****** 형식)
function formatJuminNumber(jumin, masked = true) {
if (!jumin) return '-';
// 숫자만 추출
const cleaned = jumin.replace(/\D/g, '');
if (cleaned.length >= 13) {
const front = cleaned.substring(0, 6);
const back = cleaned.substring(6, 13);
if (masked) {
// 뒷자리 마스킹
return `${front.replace(/(\d{2})(\d{2})(\d{2})/, '$1$2$3')}-${back.charAt(0)}******`;
} else {
// 전체 표시 (편집 시)
return `${front.replace(/(\d{2})(\d{2})(\d{2})/, '$1$2$3')}-${back}`;
}
}
return jumin; // 포맷팅할 수 없는 경우 원본 반환
}
$(document).ready(function() {
// 페이지 네비게이션
$('.sidebar .nav-link').on('click', function(e) {
e.preventDefault();
const page = $(this).data('page');
// Active 상태 변경
$('.sidebar .nav-link').removeClass('active');
$(this).addClass('active');
// 페이지 전환
$('.main-content').removeClass('active');
$(`#${page}`).addClass('active');
// 페이지별 데이터 로드
loadPageData(page);
});
// 초기 데이터 로드
loadPageData('dashboard');
// 페이지별 데이터 로드 함수
function loadPageData(page) {
switch(page) {
case 'dashboard':
loadDashboard();
break;
case 'patients':
loadPatients();
break;
case 'purchase':
loadPurchaseReceipts();
loadSuppliersForSelect();
break;
case 'formulas':
loadFormulas();
break;
case 'compound':
loadCompounds();
loadPatientsForSelect();
loadFormulasForSelect();
break;
case 'inventory':
loadInventory();
break;
case 'herbs':
loadHerbs();
break;
case 'herb-info':
loadHerbInfo();
break;
case 'medicine-master':
if (typeof loadMedicineMaster === 'function') loadMedicineMaster();
break;
}
}
// 대시보드 데이터 로드
function loadDashboard() {
// 환자 수 + 총 마일리지
$.get('/api/patients', function(response) {
if (response.success) {
$('#totalPatients').text(response.data.length);
const totalMileage = response.data.reduce((sum, p) => sum + (p.mileage_balance || 0), 0);
$('#totalMileage').text(totalMileage.toLocaleString());
}
});
// 재고 현황 (저장된 모드 사용)
loadInventorySummary();
// 오늘 조제 수, 이번달 매출/마진, 최근 조제 내역
$.get('/api/compounds', function(response) {
if (response.success) {
const today = new Date().toISOString().split('T')[0];
const currentMonth = new Date().toISOString().slice(0, 7);
const todayCompounds = response.data.filter(c => c.compound_date === today);
$('#todayCompounds').text(todayCompounds.length);
// 이번달 매출/마진 계산 (자가소비/샘플/폐기 제외)
const monthData = response.data.filter(c =>
c.compound_date && c.compound_date.startsWith(currentMonth) &&
['PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED'].includes(c.status) &&
(!c.usage_type || c.usage_type === 'SALE')
);
const monthSales = monthData.reduce((sum, c) => sum + (c.actual_payment_amount || c.sell_price_total || 0), 0);
const monthCost = monthData.reduce((sum, c) => sum + (c.cost_total || 0), 0);
const monthProfit = monthSales - monthCost;
const profitRate = monthSales > 0 ? ((monthProfit / monthSales) * 100).toFixed(1) : 0;
$('#monthSales').text(monthSales.toLocaleString());
$('#monthProfit').text(monthProfit.toLocaleString());
$('#profitRate').text(profitRate + '%');
// 최근 조제 내역 (최근 5개)
const tbody = $('#recentCompounds');
tbody.empty();
const recentCompounds = response.data.slice(0, 5);
if (recentCompounds.length > 0) {
recentCompounds.forEach(compound => {
let statusBadge = '';
switch(compound.status) {
case 'PREPARED':
statusBadge = '<span class="badge bg-primary">조제완료</span>';
break;
case 'PAID':
statusBadge = '<span class="badge bg-success">결제완료</span>';
break;
case 'DELIVERED':
statusBadge = '<span class="badge bg-secondary">배송완료</span>';
break;
case 'COMPLETED':
statusBadge = '<span class="badge bg-dark">판매완료</span>';
break;
case 'CANCELLED':
statusBadge = '<span class="badge bg-danger">취소</span>';
break;
default:
statusBadge = '<span class="badge bg-secondary">대기</span>';
}
tbody.append(`
<tr>
<td>${compound.compound_date || '-'}</td>
<td><strong>${compound.patient_name || '직접조제'}</strong></td>
<td>${compound.formula_name || '직접조제'}</td>
<td>${compound.je_count}제</td>
<td>${compound.pouch_total}개</td>
<td>${statusBadge}</td>
</tr>
`);
});
} else {
tbody.html('<tr><td colspan="6" class="text-center text-muted">조제 내역이 없습니다.</td></tr>');
}
}
});
}
// 환자 목록 로드
function loadPatients() {
$.get('/api/patients', function(response) {
if (response.success) {
const tbody = $('#patientsList');
tbody.empty();
// 각 환자의 처방 횟수를 가져오기 위해 처방 데이터도 로드
$.get('/api/compounds', function(compoundsResponse) {
const compounds = compoundsResponse.success ? compoundsResponse.data : [];
// 환자별 처방 횟수 계산
const compoundCounts = {};
compounds.forEach(compound => {
if (compound.patient_id) {
compoundCounts[compound.patient_id] = (compoundCounts[compound.patient_id] || 0) + 1;
}
});
response.data.forEach(patient => {
const compoundCount = compoundCounts[patient.patient_id] || 0;
const mileageBalance = patient.mileage_balance || 0;
tbody.append(`
<tr>
<td><strong>${patient.name}</strong></td>
<td>${formatPhoneNumber(patient.phone)}</td>
<td>${patient.gender === 'M' ? '남' : patient.gender === 'F' ? '여' : '-'}</td>
<td>${patient.birth_date || '-'}</td>
<td>
<span class="badge bg-primary">${compoundCount}회</span>
</td>
<td>
<span class="text-primary fw-bold">${mileageBalance.toLocaleString()}P</span>
${mileageBalance > 0 ? '<i class="bi bi-piggy-bank-fill text-warning ms-1"></i>' : ''}
</td>
<td>${patient.notes || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-info view-patient-compounds"
data-id="${patient.patient_id}"
data-name="${patient.name}">
<i class="bi bi-file-medical"></i> 처방내역
</button>
<button class="btn btn-sm btn-outline-primary edit-patient"
data-id="${patient.patient_id}">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
`);
});
// 처방내역 버튼 이벤트
$('.view-patient-compounds').on('click', function() {
const patientId = $(this).data('id');
const patientName = $(this).data('name');
viewPatientCompounds(patientId, patientName);
});
// 편집 버튼 이벤트
$('.edit-patient').on('click', function() {
const patientId = $(this).data('id');
editPatient(patientId);
});
});
}
});
}
// 환자 정보 편집
function editPatient(patientId) {
$.get(`/api/patients/${patientId}`, function(response) {
if (response.success && response.data) {
const patient = response.data;
// 모달 제목 변경
$('#patientModal .modal-title').text('환자 정보 수정');
// 폼에 데이터 채우기
$('#patientName').val(patient.name);
$('#patientPhone').val(patient.phone);
$('#patientJumin').val(patient.jumin_no);
$('#patientGender').val(patient.gender);
$('#patientBirth').val(patient.birth_date);
$('#patientAddress').val(patient.address);
$('#patientNotes').val(patient.notes);
// 마일리지 섹션 표시 및 데이터 채우기
$('#mileageSection').show();
$('#patientMileageBalance').val(patient.mileage_balance || 0);
$('#patientMileageEarned').val(patient.total_mileage_earned || 0);
$('#patientMileageUsed').val(patient.total_mileage_used || 0);
// 저장 버튼에 patient_id 저장
$('#savePatientBtn').data('patient-id', patientId);
// 모달 표시
$('#patientModal').modal('show');
}
}).fail(function() {
alert('환자 정보를 불러오는데 실패했습니다.');
});
}
// 환자 등록/수정
$('#savePatientBtn').on('click', function() {
const patientId = $(this).data('patient-id'); // 편집 모드인지 확인
const patientData = {
name: $('#patientName').val(),
phone: $('#patientPhone').val(),
jumin_no: $('#patientJumin').val(),
gender: $('#patientGender').val(),
birth_date: $('#patientBirth').val(),
address: $('#patientAddress').val(),
notes: $('#patientNotes').val()
};
// 편집 모드인 경우 PUT, 새 등록인 경우 POST
const url = patientId ? `/api/patients/${patientId}` : '/api/patients';
const method = patientId ? 'PUT' : 'POST';
$.ajax({
url: url,
method: method,
contentType: 'application/json',
data: JSON.stringify(patientData),
success: function(response) {
if (response.success) {
alert(patientId ? '환자 정보가 수정되었습니다.' : '환자가 등록되었습니다.');
$('#patientModal').modal('hide');
$('#patientForm')[0].reset();
$('#mileageSection').hide(); // 마일리지 섹션 숨기기
$('#savePatientBtn').removeData('patient-id'); // patient-id 데이터 제거
loadPatients();
}
},
error: function(xhr) {
alert('오류: ' + (xhr.responseJSON?.error || '알 수 없는 오류'));
}
});
});
// 환자 모달이 닫힐 때 초기화
$('#patientModal').on('hidden.bs.modal', function() {
$('#patientForm')[0].reset();
$('#mileageSection').hide();
$('#savePatientBtn').removeData('patient-id');
$('#patientModal .modal-title').text('환자 등록'); // 제목을 기본값으로 복원
});
// 환자 처방 내역 조회
function viewPatientCompounds(patientId, patientName) {
// 환자 정보 가져오기
$.get(`/api/patients/${patientId}`, function(patientResponse) {
if (patientResponse.success) {
const patient = patientResponse.data;
// 환자 기본 정보 표시
$('#patientCompoundsName').text(patient.name);
$('#patientInfoName').text(patient.name);
$('#patientInfoPhone').text(patient.phone || '-');
$('#patientInfoGender').text(patient.gender === 'M' ? '남성' : patient.gender === 'F' ? '여성' : '-');
$('#patientInfoBirth').text(patient.birth_date || '-');
// 환자의 처방 내역 가져오기
$.get(`/api/patients/${patientId}/compounds`, function(compoundsResponse) {
if (compoundsResponse.success) {
const compounds = compoundsResponse.compounds || [];
// 통계 계산
const totalCompounds = compounds.length;
let totalJe = 0;
let totalAmount = 0;
let lastVisit = '-';
if (compounds.length > 0) {
compounds.forEach(c => {
totalJe += c.je_count || 0;
totalAmount += c.sell_price_total || 0;
});
lastVisit = compounds[0].compound_date || '-';
}
// 통계 표시
$('#patientTotalCompounds').text(totalCompounds + '회');
$('#patientLastVisit').text(lastVisit);
$('#patientTotalJe').text(totalJe + '제');
$('#patientTotalAmount').text(formatCurrency(totalAmount));
// 처방 내역 테이블 표시
const tbody = $('#patientCompoundsList');
tbody.empty();
if (compounds.length === 0) {
tbody.append(`
<tr>
<td colspan="10" class="text-center text-muted">처방 내역이 없습니다.</td>
</tr>
`);
} else {
compounds.forEach((compound, index) => {
// 상태 뱃지
let statusBadge = '';
switch(compound.status) {
case 'PREPARED':
statusBadge = '<span class="badge bg-success">조제완료</span>';
break;
case 'DISPENSED':
statusBadge = '<span class="badge bg-primary">출고완료</span>';
break;
case 'CANCELLED':
statusBadge = '<span class="badge bg-danger">취소</span>';
break;
default:
statusBadge = '<span class="badge bg-secondary">대기</span>';
}
const detailRowId = `compound-detail-${compound.compound_id}`;
// 처방명 표시 (가감방 여부 포함)
let formulaDisplay = compound.formula_name || '직접조제';
if (compound.is_custom && compound.formula_name) {
formulaDisplay = `${compound.formula_name} <span class="badge bg-warning ms-1">가감</span>`;
}
tbody.append(`
<tr class="compound-row" style="cursor: pointer;" data-compound-id="${compound.compound_id}">
<td>
<i class="bi bi-chevron-right toggle-icon"></i>
</td>
<td>${compound.compound_date || '-'}</td>
<td>
${formulaDisplay}
${compound.custom_summary ? `<br><small class="text-muted">${compound.custom_summary}</small>` : ''}
</td>
<td>${compound.je_count || 0}</td>
<td>${compound.cheop_total || 0}</td>
<td>${compound.pouch_total || 0}</td>
<td>${formatCurrency(compound.cost_total || 0)}</td>
<td>${formatCurrency(compound.sell_price_total || 0)}</td>
<td>${statusBadge}</td>
<td>${compound.prescription_no || '-'}</td>
</tr>
<tr id="${detailRowId}" class="collapse-row" style="display: none;">
<td colspan="10" class="p-0">
<div class="card m-2">
<div class="card-body">
<div class="row">
<div class="col-md-12">
<h6><i class="bi bi-capsule"></i> 구성 약재</h6>
<div id="ingredients-${compound.compound_id}" class="mb-3">
<div class="text-center text-muted">
<div class="spinner-border spinner-border-sm" role="status"></div>
로딩 중...
</div>
</div>
<h6><i class="bi bi-box-seam"></i> 재고 소비 내역</h6>
<div id="consumptions-${compound.compound_id}">
<div class="text-center text-muted">
<div class="spinner-border spinner-border-sm" role="status"></div>
로딩 중...
</div>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
`);
});
// 행 클릭 이벤트 - 상세 정보 토글
$('.compound-row').on('click', function() {
const compoundId = $(this).data('compound-id');
const detailRow = $(`#compound-detail-${compoundId}`);
const icon = $(this).find('.toggle-icon');
if (detailRow.is(':visible')) {
// 닫기
detailRow.slideUp();
icon.removeClass('bi-chevron-down').addClass('bi-chevron-right');
} else {
// 열기 - 다른 모든 행 닫기
$('.collapse-row').slideUp();
$('.toggle-icon').removeClass('bi-chevron-down').addClass('bi-chevron-right');
// 현재 행 열기
detailRow.slideDown();
icon.removeClass('bi-chevron-right').addClass('bi-chevron-down');
// 상세 정보 로드
loadCompoundDetailInline(compoundId);
}
});
}
// 모달 표시
$('#patientCompoundsModal').modal('show');
}
}).fail(function() {
alert('처방 내역을 불러오는데 실패했습니다.');
});
}
}).fail(function() {
alert('환자 정보를 불러오는데 실패했습니다.');
});
}
// 환자 처방 내역 모달 내에서 조제 상세 정보 로드 (인라인)
function loadCompoundDetailInline(compoundId) {
$.get(`/api/compounds/${compoundId}`, function(response) {
if (response.success && response.data) {
const data = response.data;
// 구성 약재 테이블
let ingredientsHtml = '<table class="table table-sm table-bordered"><thead class="table-light"><tr><th>약재명</th><th>보험코드</th><th>첩당용량</th><th>총용량</th></tr></thead><tbody>';
if (data.ingredients && data.ingredients.length > 0) {
data.ingredients.forEach(ing => {
ingredientsHtml += `
<tr>
<td><strong>${ing.herb_name}</strong></td>
<td>${ing.insurance_code || '-'}</td>
<td>${ing.grams_per_cheop}g</td>
<td>${ing.total_grams}g</td>
</tr>
`;
});
} else {
ingredientsHtml += '<tr><td colspan="4" class="text-center text-muted">약재 정보가 없습니다</td></tr>';
}
ingredientsHtml += '</tbody></table>';
$(`#ingredients-${compoundId}`).html(ingredientsHtml);
// 재고 소비 내역 테이블
let consumptionsHtml = '<table class="table table-sm table-bordered"><thead class="table-light"><tr><th>약재명</th><th>원산지</th><th>도매상</th><th>사용량</th><th>단가</th><th>원가</th></tr></thead><tbody>';
if (data.consumptions && data.consumptions.length > 0) {
let totalCost = 0;
data.consumptions.forEach(con => {
totalCost += con.cost_amount || 0;
consumptionsHtml += `
<tr>
<td><strong>${con.herb_name}</strong></td>
<td>${con.origin_country || '-'}</td>
<td>${con.supplier_name || '-'}</td>
<td>${con.quantity_used}g</td>
<td>${formatCurrency(con.unit_cost_per_g)}/g</td>
<td>${formatCurrency(con.cost_amount)}</td>
</tr>
`;
});
consumptionsHtml += `
<tr class="table-info">
<td colspan="5" class="text-end"><strong>총 원가:</strong></td>
<td><strong>${formatCurrency(totalCost)}</strong></td>
</tr>
`;
} else {
consumptionsHtml += '<tr><td colspan="6" class="text-center text-muted">재고 소비 내역이 없습니다</td></tr>';
}
consumptionsHtml += '</tbody></table>';
$(`#consumptions-${compoundId}`).html(consumptionsHtml);
}
}).fail(function() {
$(`#ingredients-${compoundId}`).html('<div class="alert alert-danger">데이터를 불러오는데 실패했습니다.</div>');
$(`#consumptions-${compoundId}`).html('<div class="alert alert-danger">데이터를 불러오는데 실패했습니다.</div>');
});
}
// 처방 목록 로드
function loadFormulas() {
// 100처방 이름 목록을 먼저 가져온 후 내 처방 렌더링
$.get('/api/official-formulas', function(offRes) {
const officialNames = new Map();
if (offRes.success) {
offRes.data.forEach(f => officialNames.set(f.formula_name, f.formula_number));
}
$.get('/api/formulas', function(response) {
if (response.success) {
const tbody = $('#formulasList');
tbody.empty();
response.data.forEach(formula => {
// 100처방 매칭: 1차 official_formula_id FK, 2차 이름 매칭 (원방명이 내 처방명에 포함)
let officialNum = null;
if (formula.official_formula_id) {
// FK로 연결된 경우 — official_name은 API에서 JOIN으로 내려옴
officialNum = officialNames.get(formula.official_name);
}
if (officialNum == null) {
officialNum = officialNames.get(formula.formula_name);
}
if (officialNum == null) {
for (const [name, num] of officialNames) {
if (formula.formula_name.includes(name)) {
officialNum = num;
break;
}
}
}
const officialBadge = officialNum != null
? ` <span class="badge bg-info">100처방 #${officialNum}</span>`
: '';
// 처방명 스타일링: "어울림" 접두어 색상 처리
let displayName = formula.formula_name;
if (displayName.startsWith('어울림 ')) {
displayName = `<span class="text-success fw-bold">어울림</span> ${displayName.substring(4)}`;
}
// 가감 정보 표시 (100처방 기반 처방)
let customInfo = '';
if (formula.official_formula_id && formula.is_custom) {
let details = [];
if (formula.custom_modified && formula.custom_modified.length > 0) {
details.push(...formula.custom_modified.map(m =>
`<span class="badge bg-primary bg-opacity-75 me-1">${m}</span>`
));
}
if (formula.custom_added && formula.custom_added.length > 0) {
details.push(...formula.custom_added.map(a =>
`<span class="badge bg-success bg-opacity-75 me-1">+${a}</span>`
));
}
if (formula.custom_removed && formula.custom_removed.length > 0) {
details.push(...formula.custom_removed.map(r =>
`<span class="badge bg-danger bg-opacity-75 me-1">-${r}</span>`
));
}
if (details.length > 0) {
customInfo = `<br><span class="d-inline-flex flex-wrap gap-1 mt-1">${details.join('')}</span>`;
}
} else if (formula.official_formula_id && !formula.is_custom) {
customInfo = ` <span class="badge bg-secondary">원방 그대로</span>`;
}
tbody.append(`
<tr>
<td>${formula.formula_code || '-'}</td>
<td>${displayName}${officialBadge}${customInfo}</td>
<td>${formula.base_cheop}첩</td>
<td>${formula.base_pouches}파우치</td>
<td>
<button class="btn btn-sm btn-outline-info view-formula-detail"
data-id="${formula.formula_id}"
data-name="${formula.formula_name}">
<i class="bi bi-eye"></i> 구성
</button>
</td>
<td>
<button class="btn btn-sm btn-outline-primary edit-formula"
data-id="${formula.formula_id}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger delete-formula"
data-id="${formula.formula_id}"
data-name="${formula.formula_name}">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`);
});
// 처방 상세 보기 버튼 이벤트
$('.view-formula-detail').on('click', function() {
const formulaId = $(this).data('id');
const formulaName = $(this).data('name');
showFormulaDetail(formulaId, formulaName);
});
// 처방 수정 버튼 이벤트
$('.edit-formula').on('click', function() {
const formulaId = $(this).data('id');
editFormula(formulaId);
});
// 처방 삭제 버튼 이벤트
$('.delete-formula').on('click', function() {
const formulaId = $(this).data('id');
const formulaName = $(this).data('name');
if(confirm(`'${formulaName}' 처방을 삭제하시겠습니까?`)) {
deleteFormula(formulaId);
}
});
}
// 내 처방 렌더링 완료 후 100처방 로드
loadOfficialFormulas();
});
}); // /api/official-formulas 콜백 닫기
}
// 100처방 원방 마스터 로드
function loadOfficialFormulas(search) {
const params = search ? `?search=${encodeURIComponent(search)}` : '';
$.get(`/api/official-formulas${params}`, function(response) {
if (response.success) {
const tbody = $('#officialFormulasList');
tbody.empty();
$('#officialFormulaCount').text(response.data.length);
response.data.forEach(formula => {
// 등록 여부: 백엔드에서 판정 (official_formula_id FK + 이름 fallback)
const isRegistered = formula.is_registered;
let statusBadge;
if (isRegistered && formula.registered_names && formula.registered_names.length > 0) {
const names = formula.registered_names.map(n => n.length > 12 ? n.substring(0, 12) + '…' : n).join(', ');
statusBadge = `<span class="badge bg-success" title="${formula.registered_names.join(', ')}">${formula.registered_names.length > 1 ? formula.registered_names.length + '개 등록' : '등록됨'}</span>`;
} else {
statusBadge = '<span class="badge bg-outline-secondary text-muted">미등록</span>';
}
const hasNotes = formula.reference_notes ? '<i class="bi bi-journal-text text-info ms-1" title="참고자료 있음"></i>' : '';
// 구성 약재 수 표시
const ingCount = formula.ingredient_count || 0;
const ingBadge = ingCount > 0
? `<span class="badge bg-success bg-opacity-75">${ingCount}종</span>`
: `<span class="badge bg-light text-muted">미입력</span>`;
tbody.append(`
<tr class="official-formula-row" style="cursor:pointer"
data-id="${formula.official_formula_id}"
data-number="${formula.formula_number}"
data-name="${formula.formula_name}"
data-hanja="${formula.formula_name_hanja || ''}"
data-source="${formula.source_text || ''}"
data-description="${(formula.description || '').replace(/"/g, '&quot;')}"
data-notes="${(formula.reference_notes || '').replace(/"/g, '&quot;')}">
<td class="text-center">${formula.formula_number}</td>
<td><strong>${formula.formula_name}</strong>${hasNotes}</td>
<td class="text-muted">${formula.formula_name_hanja || '-'}</td>
<td>${formula.source_text || '-'}</td>
<td class="text-center">${ingBadge}</td>
<td class="text-center">${statusBadge}</td>
</tr>
`);
});
if (response.data.length === 0) {
tbody.html('<tr><td colspan="6" class="text-center text-muted">검색 결과가 없습니다.</td></tr>');
}
}
});
}
// 100처방 검색 이벤트
let officialSearchTimer = null;
$(document).on('input', '#officialFormulaSearch', function() {
clearTimeout(officialSearchTimer);
const search = $(this).val().trim();
officialSearchTimer = setTimeout(() => {
loadOfficialFormulas(search);
}, 300);
});
// 100처방 행 클릭 → 상세/참고자료 모달
$(document).on('click', '.official-formula-row', function() {
const row = $(this);
const id = row.data('id');
$('#officialFormulaModal').data('formula-id', id);
$('#ofModalNumber').text(row.data('number'));
$('#ofModalName').text(row.data('name'));
$('#ofModalHanja').text(row.data('hanja') || '');
$('#ofModalSource').text(row.data('source') || '-');
$('#ofEditHanja').val(row.data('hanja') || '');
$('#ofEditDescription').val(row.data('description') || '');
$('#ofEditReferenceNotes').val(row.data('notes') || '');
// 원방 구성 약재 로드
$.get(`/api/official-formulas/${id}/ingredients`, function(res) {
const section = $('#ofIngredientsSection');
const tbody = $('#ofIngredientsList');
tbody.empty();
if (res.success && res.data.length > 0) {
let totalGrams = 0;
res.data.forEach((ing, idx) => {
totalGrams += ing.grams_per_cheop;
tbody.append(`
<tr>
<td class="text-muted">${idx + 1}</td>
<td><strong>${ing.herb_name}</strong> <small class="text-muted">${ing.herb_name_hanja || ''}</small></td>
<td class="text-end">${ing.grams_per_cheop}g</td>
<td><small class="text-muted">${ing.notes || '-'}</small></td>
</tr>
`);
});
$('#ofIngredientCount').text(res.data.length);
$('#ofTotalGrams').text(totalGrams.toFixed(1));
section.show();
} else {
section.hide();
}
});
$('#officialFormulaModal').modal('show');
});
// 100처방 참고자료 저장
$(document).on('click', '#saveOfficialFormulaBtn', function() {
const id = $('#officialFormulaModal').data('formula-id');
$.ajax({
url: `/api/official-formulas/${id}`,
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
formula_name_hanja: $('#ofEditHanja').val().trim(),
description: $('#ofEditDescription').val().trim(),
reference_notes: $('#ofEditReferenceNotes').val().trim()
}),
success: function(response) {
if (response.success) {
alert('저장되었습니다.');
$('#officialFormulaModal').modal('hide');
loadOfficialFormulas($('#officialFormulaSearch').val().trim());
}
},
error: function(xhr) {
alert(xhr.responseJSON?.error || '저장 중 오류가 발생했습니다.');
}
});
});
// 100처방 → 내 처방으로 등록
$(document).on('click', '#createFromOfficialBtn', function() {
const id = $('#officialFormulaModal').data('formula-id');
const name = $('#ofModalName').text();
const description = $('#ofEditDescription').val().trim();
// 100처방 모달 닫기
$('#officialFormulaModal').modal('hide');
// 처방 등록 모달 초기화 (신규 모드)
$('#formulaModal').data('edit-mode', false);
$('#formulaModal').data('formula-id', null);
$('#formulaModal .modal-title').text('처방 등록 (원방 기반)');
$('#formulaForm')[0].reset();
$('#formulaIngredients').empty();
// 기본값 세팅
$('#formulaName').val(`어울림 ${name}`);
$('#formulaType').val('CUSTOM');
$('#baseCheop').val(20);
$('#basePouches').val(30);
$('#formulaDescription').val(description);
$('#formulaModal').data('official-formula-id', id);
// 원방 구성 약재 로드
$.get(`/api/official-formulas/${id}/ingredients`, function(res) {
if (res.success && res.data.length > 0) {
formulaIngredientCount = 0;
res.data.forEach(ing => {
formulaIngredientCount++;
$('#formulaIngredients').append(`
<tr data-row="${formulaIngredientCount}">
<td>
<select class="form-control form-control-sm herb-select">
<option value="${ing.ingredient_code}" selected>${ing.herb_name}</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm grams-input"
min="0.1" step="0.1" value="${ing.grams_per_cheop}">
</td>
<td>
<input type="text" class="form-control form-control-sm notes-input" value="${ing.notes || ''}">
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-ingredient">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
// 약재 select에 전체 목록 로드 (현재 값 유지)
const selectEl = $(`#formulaIngredients tr[data-row="${formulaIngredientCount}"] .herb-select`);
loadHerbsForSelectWithCurrent(selectEl, ing.ingredient_code, ing.herb_name);
});
// 삭제 버튼 이벤트
$('.remove-ingredient').on('click', function() {
$(this).closest('tr').remove();
});
}
// 처방 등록 모달 열기
$('#formulaModal').modal('show');
});
});
// 처방 상세 정보 표시 함수
function showFormulaDetail(formulaId, formulaName) {
// 모달에 formulaId 저장
$('#formulaDetailModal').data('formula-id', formulaId);
// 모달 제목 설정
$('#formulaDetailName').text(formulaName);
// 처방 기본 정보 로드
$.get(`/api/formulas/${formulaId}`, function(response) {
if (response.success && response.data) {
const formula = response.data;
// 기본 정보 표시
$('#detailFormulaCode').text(formula.formula_code || '-');
$('#detailFormulaName').text(formula.formula_name);
$('#detailFormulaType').text(formula.formula_type === 'STANDARD' ? '표준처방' : '사용자정의');
$('#detailBaseCheop').text(formula.base_cheop + '첩');
$('#detailBasePouches').text(formula.base_pouches + '파우치');
$('#detailCreatedAt').text(formula.created_at ? new Date(formula.created_at).toLocaleDateString() : '-');
$('#detailDescription').text(formula.description || '설명이 없습니다.');
// 주요 효능 표시
if (formula.efficacy) {
$('#formulaEffects').html(`<p>${formula.efficacy}</p>`);
} else {
$('#formulaEffects').html('<p class="text-muted">처방의 주요 효능 정보가 등록되지 않았습니다.</p>');
}
// 처방 구성 약재 로드
loadFormulaIngredients(formulaId);
}
}).fail(function() {
alert('처방 정보를 불러오는데 실패했습니다.');
});
// 모달 표시
$('#formulaDetailModal').modal('show');
}
// 처방 구성 약재 로드
function loadFormulaIngredients(formulaId) {
$.get(`/api/formulas/${formulaId}/ingredients`, function(response) {
if (response.success) {
const tbody = $('#formulaDetailIngredients');
tbody.empty();
let totalGrams1 = 0;
let totalGrams1Je = 0; // 1제 기준 (20첩 = 30파우치)
let count = 0;
response.data.forEach((ingredient, index) => {
count++;
const gram1 = parseFloat(ingredient.grams_per_cheop) || 0;
const gram1Je = gram1 * 20; // 1제 = 20첩 = 30파우치
totalGrams1 += gram1;
totalGrams1Je += gram1Je;
// 재고 상태 표시 (stock_quantity 또는 total_available_stock 사용)
const stockQty = ingredient.stock_quantity || ingredient.total_available_stock || 0;
let stockStatus = '';
if (stockQty > 0) {
stockStatus = `<span class="badge bg-success">재고 ${stockQty.toFixed(1)}g</span>`;
} else {
stockStatus = `<span class="badge bg-danger">재고없음</span>`;
}
// 사용 가능한 제품 수 표시
if (ingredient.product_count > 0) {
stockStatus += `<br><small class="text-muted">${ingredient.product_count}개 제품</small>`;
}
tbody.append(`
<tr>
<td>${index + 1}</td>
<td>
${ingredient.herb_name}<br>
<small class="text-muted">${ingredient.ingredient_code}</small>
</td>
<td class="text-end">${gram1.toFixed(1)}g</td>
<td class="text-end">${gram1Je.toFixed(1)}g</td>
<td>${ingredient.notes || '-'}</td>
<td class="text-center">${stockStatus}</td>
</tr>
`);
});
// 합계 업데이트
$('#totalIngredientsCount').text(count + '개');
$('#totalGramsPerCheop').text(totalGrams1.toFixed(1) + 'g');
$('#totalGrams1Cheop').text(totalGrams1.toFixed(1) + 'g');
$('#totalGrams1Je').text(totalGrams1Je.toFixed(1) + 'g');
}
});
}
// 처방 수정 함수
function editFormula(formulaId) {
$.get(`/api/formulas/${formulaId}`, function(response) {
if (response.success && response.data) {
const formula = response.data;
// 수정 모달에 데이터 채우기
$('#formulaCode').val(formula.formula_code);
$('#formulaName').val(formula.formula_name);
$('#formulaType').val(formula.formula_type);
$('#baseCheop').val(formula.base_cheop);
$('#basePouches').val(formula.base_pouches);
$('#formulaDescription').val(formula.description);
$('#formulaEfficacy').val(formula.efficacy || '');
// 구성 약재 로드
$.get(`/api/formulas/${formulaId}/ingredients`, function(ingResponse) {
if (ingResponse.success) {
$('#formulaIngredients').empty();
formulaIngredientCount = 0;
ingResponse.data.forEach(ing => {
formulaIngredientCount++;
$('#formulaIngredients').append(`
<tr data-row="${formulaIngredientCount}">
<td>
<select class="form-control form-control-sm herb-select">
<option value="${ing.ingredient_code}" selected>${ing.herb_name}</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm grams-input"
min="0.1" step="0.1" value="${ing.grams_per_cheop}">
</td>
<td>
<input type="text" class="form-control form-control-sm notes-input" value="${ing.notes || ''}">
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-ingredient">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
// 약재 목록 로드 (현재 선택된 값 유지)
const selectElement = $(`#formulaIngredients tr[data-row="${formulaIngredientCount}"] .herb-select`);
const currentValue = ing.ingredient_code;
const currentText = ing.herb_name;
loadHerbsForSelectWithCurrent(selectElement, currentValue, currentText);
});
// 삭제 버튼 이벤트 바인딩
$('.remove-ingredient').on('click', function() {
$(this).closest('tr').remove();
});
}
});
// 수정 모드 설정 (data 속성 사용)
$('#formulaModal').data('edit-mode', true);
$('#formulaModal').data('formula-id', formulaId);
$('#formulaModal .modal-title').text('처방 수정');
$('#formulaModal').modal('show');
}
});
}
// 처방 삭제 함수
function deleteFormula(formulaId) {
$.ajax({
url: `/api/formulas/${formulaId}`,
method: 'DELETE',
success: function(response) {
if (response.success) {
alert('처방이 삭제되었습니다.');
loadFormulas();
}
},
error: function(xhr) {
alert('오류: ' + (xhr.responseJSON ? xhr.responseJSON.error : '삭제 실패'));
}
});
}
// 처방 상세 모달에서 수정 버튼 클릭
$('#editFormulaDetailBtn').on('click', function() {
const formulaId = $('#formulaDetailModal').data('formula-id');
$('#formulaDetailModal').modal('hide');
editFormula(formulaId);
});
// 처방 상세 모달에서 삭제 버튼 클릭
$('#deleteFormulaBtn').on('click', function() {
const formulaId = $('#formulaDetailModal').data('formula-id');
const formulaName = $('#formulaDetailName').text();
if(confirm(`'${formulaName}' 처방을 삭제하시겠습니까?`)) {
$('#formulaDetailModal').modal('hide');
deleteFormula(formulaId);
}
});
// 처방 구성 약재 추가 (모달)
let formulaIngredientCount = 0;
$('#addFormulaIngredientBtn').on('click', function() {
formulaIngredientCount++;
$('#formulaIngredients').append(`
<tr data-row="${formulaIngredientCount}">
<td>
<select class="form-control form-control-sm herb-select">
<option value="">약재 선택</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm grams-input"
min="0.1" step="0.1" placeholder="0.0">
</td>
<td>
<input type="text" class="form-control form-control-sm notes-input">
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-ingredient">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
// 약재 목록 로드
const selectElement = $(`#formulaIngredients tr[data-row="${formulaIngredientCount}"] .herb-select`);
loadHerbsForSelect(selectElement);
// 삭제 버튼 이벤트
$(`#formulaIngredients tr[data-row="${formulaIngredientCount}"] .remove-ingredient`).on('click', function() {
$(this).closest('tr').remove();
});
});
// 처방 저장
$('#saveFormulaBtn').on('click', function() {
const ingredients = [];
$('#formulaIngredients tr').each(function() {
const herbCode = $(this).find('.herb-select').val();
const grams = $(this).find('.grams-input').val();
if (herbCode && grams) {
ingredients.push({
ingredient_code: herbCode, // ingredient_code 사용
grams_per_cheop: parseFloat(grams),
notes: $(this).find('.notes-input').val()
});
}
});
const formulaData = {
formula_code: $('#formulaCode').val(),
formula_name: $('#formulaName').val(),
formula_type: $('#formulaType').val(),
base_cheop: parseInt($('#baseCheop').val()),
base_pouches: parseInt($('#basePouches').val()),
description: $('#formulaDescription').val(),
efficacy: $('#formulaEfficacy').val(),
ingredients: ingredients,
official_formula_id: $('#formulaModal').data('official-formula-id') || null
};
// 수정 모드인지 확인
const isEditMode = $('#formulaModal').data('edit-mode');
const formulaId = $('#formulaModal').data('formula-id');
const url = isEditMode ? `/api/formulas/${formulaId}` : '/api/formulas';
const method = isEditMode ? 'PUT' : 'POST';
$.ajax({
url: url,
method: method,
contentType: 'application/json',
data: JSON.stringify(formulaData),
success: function(response) {
if (response.success) {
const message = isEditMode ? '처방이 수정되었습니다.' : '처방이 등록되었습니다.';
alert(message);
$('#formulaModal').modal('hide');
$('#formulaForm')[0].reset();
$('#formulaIngredients').empty();
// 수정 모드 초기화
$('#formulaModal').data('edit-mode', false);
$('#formulaModal').data('formula-id', null);
$('#formulaModal').data('official-formula-id', null);
$('#formulaModal .modal-title').text('처방 등록');
loadFormulas();
}
},
error: function(xhr) {
alert('오류: ' + (xhr.responseJSON ? xhr.responseJSON.error : '알 수 없는 오류'));
}
});
});
// 조제 관리
$('#newCompoundBtn').on('click', function() {
$('#compoundForm').show();
$('#compoundEntryForm')[0].reset();
$('#compoundIngredients').empty();
$('#costPreview').hide();
// 제수 기본값(1)으로 첩수/파우치 초기화
$('#jeCount').val(1);
$('#cheopTotal').val(20);
$('#pouchTotal').val(30);
});
$('#cancelCompoundBtn').on('click', function() {
$('#compoundForm').hide();
$('#costPreview').hide();
});
// 제수 변경 시 첩수 자동 계산
$('#jeCount').on('input', function() {
const jeCount = parseFloat($(this).val()) || 0;
const cheopTotal = jeCount * 20;
const pouchTotal = jeCount * 30;
$('#cheopTotal').val(cheopTotal);
$('#pouchTotal').val(pouchTotal);
// 약재별 총 용량 재계산
updateIngredientTotals();
});
// 처방 선택 시 구성 약재 로드
$('#compoundFormula').on('change', function() {
const formulaId = $(this).val();
// 제수 기반 첩수/파우치 자동 계산 (초기값 반영)
const jeCount = parseFloat($('#jeCount').val()) || 0;
if (jeCount > 0 && !$('#cheopTotal').val()) {
$('#cheopTotal').val(jeCount * 20);
$('#pouchTotal').val(jeCount * 30);
}
// 원래 처방 구성 초기화
originalFormulaIngredients = {};
$('#customPrescriptionBadge').remove(); // 커스텀 뱃지 제거
if (!formulaId) {
$('#compoundIngredients').empty();
$('#costPreview').hide();
return;
}
// 직접조제인 경우
if (formulaId === 'custom') {
$('#compoundIngredients').empty();
// 빈 행 하나 추가
addEmptyIngredientRow();
return;
}
// 등록된 처방인 경우
$.get(`/api/formulas/${formulaId}/ingredients`, function(response) {
if (response.success) {
$('#compoundIngredients').empty();
// 원래 처방 구성 저장
response.data.forEach(ing => {
originalFormulaIngredients[ing.ingredient_code] = {
herb_name: ing.herb_name,
grams_per_cheop: ing.grams_per_cheop
};
});
response.data.forEach(ing => {
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
const totalGrams = ing.grams_per_cheop * cheopTotal;
// 제품 선택 옵션 생성
let productOptions = '<option value="">제품 선택</option>';
if (ing.available_products && ing.available_products.length > 0) {
ing.available_products.forEach(product => {
const specInfo = product.specification ? ` [${product.specification}]` : '';
productOptions += `<option value="${product.herb_item_id}">${product.herb_name}${specInfo} (재고: ${product.stock.toFixed(0)}g)</option>`;
});
}
$('#compoundIngredients').append(`
<tr data-ingredient-code="${ing.ingredient_code}" data-herb-id="">
<td>
${ing.herb_name}
${ing.total_available_stock > 0
? `<small class="text-success">(총 ${ing.total_available_stock.toFixed(0)}g 사용 가능)</small>`
: '<small class="text-danger">(재고 없음)</small>'}
</td>
<td>
<input type="number" class="form-control form-control-sm grams-per-cheop"
value="${ing.grams_per_cheop}" min="0.1" step="0.1">
</td>
<td class="total-grams">${totalGrams.toFixed(1)}</td>
<td class="origin-select-cell">
<div class="d-flex gap-1">
<select class="form-control form-control-sm product-select" style="flex: 1;" ${ing.available_products.length === 0 ? 'disabled' : ''}>
${productOptions}
</select>
<select class="form-control form-control-sm origin-select" style="flex: 1;" disabled>
<option value="">제품 먼저 선택</option>
</select>
</div>
</td>
<td class="stock-status">대기중</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
// 첫 번째 제품 자동 선택 및 원산지 로드
const tr = $(`tr[data-ingredient-code="${ing.ingredient_code}"]`);
if (ing.available_products && ing.available_products.length > 0) {
const firstProduct = ing.available_products[0];
tr.find('.product-select').val(firstProduct.herb_item_id);
tr.attr('data-herb-id', firstProduct.herb_item_id);
// 원산지/로트 옵션 로드
loadOriginOptions(firstProduct.herb_item_id, totalGrams);
}
});
// 재고 확인
checkStockForCompound();
// 제품 선택 변경 이벤트
$('.product-select').on('change', function() {
const herbId = $(this).val();
const row = $(this).closest('tr');
if (herbId) {
row.attr('data-herb-id', herbId);
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0;
const totalGrams = gramsPerCheop * cheopTotal;
// 원산지/로트 옵션 로드
loadOriginOptions(herbId, totalGrams);
} else {
row.attr('data-herb-id', '');
row.find('.origin-select').empty().append('<option value="">제품 먼저 선택</option>').prop('disabled', true);
row.find('.stock-status').text('대기중');
}
});
// 용량 변경 이벤트
$('.grams-per-cheop').on('input', function() {
updateIngredientTotals();
// 원산지 옵션 다시 로드
const row = $(this).closest('tr');
const herbId = row.attr('data-herb-id');
if (herbId) {
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
const gramsPerCheop = parseFloat($(this).val()) || 0;
const totalGrams = gramsPerCheop * cheopTotal;
loadOriginOptions(herbId, totalGrams);
}
});
// 삭제 버튼 이벤트
$('.remove-compound-ingredient').on('click', function() {
$(this).closest('tr').remove();
updateIngredientTotals(); // 총용량 재계산 및 커스텀 감지
});
}
});
});
// 약재별 총 용량 업데이트
let _stockCheckTimer = null;
function updateIngredientTotals() {
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
$('#compoundIngredients tr').each(function() {
const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val()) || 0;
const totalGrams = gramsPerCheop * cheopTotal;
$(this).find('.total-grams').text(totalGrams.toFixed(1));
});
// 커스텀 처방 감지 호출
checkCustomPrescription();
// 원가 미리보기 갱신 (즉시)
updateCostPreview();
// 재고 상태 갱신 (디바운스 300ms)
clearTimeout(_stockCheckTimer);
_stockCheckTimer = setTimeout(() => checkStockForCompound(), 300);
}
// 원가 미리보기 계산
function updateCostPreview() {
const rows = $('#compoundIngredients tr');
if (rows.length === 0) {
$('#costPreview').hide();
return;
}
const items = [];
let totalCost = 0;
let allHavePrice = true;
rows.each(function() {
// 약재명: 처방에서 로드된 행은 텍스트, 추가된 행은 select의 선택값
const firstTd = $(this).find('td:first');
const herbSelect = firstTd.find('.herb-select-compound');
const herbName = herbSelect.length > 0
? (herbSelect.find('option:selected').text().trim() || '미선택')
: firstTd.text().trim().split('(')[0].trim();
const totalGrams = parseFloat($(this).find('.total-grams').text()) || 0;
const originSelect = $(this).find('.origin-select');
const selectedOption = originSelect.find('option:selected');
const unitPrice = parseFloat(selectedOption.attr('data-price')) || 0;
// 수동 배분인 경우 data-lot-assignments에서 계산
const lotAssignmentsStr = $(this).attr('data-lot-assignments');
let itemCost = 0;
if (lotAssignmentsStr) {
try {
const assignments = JSON.parse(lotAssignmentsStr);
assignments.forEach(a => {
itemCost += (a.quantity || 0) * (a.unit_price || 0);
});
} catch(e) {
itemCost = totalGrams * unitPrice;
}
} else if (unitPrice > 0) {
itemCost = totalGrams * unitPrice;
} else {
allHavePrice = false;
}
totalCost += itemCost;
items.push({ name: herbName, grams: totalGrams, unitPrice, cost: itemCost });
});
// UI 렌더링
const tbody = $('#costPreviewItems');
tbody.empty();
items.forEach(item => {
const costText = item.cost > 0
? formatCurrency(Math.round(item.cost))
: '<span class="text-muted">-</span>';
const priceText = item.unitPrice > 0
? `${item.grams.toFixed(1)}g ×${item.unitPrice.toFixed(1)}`
: `${item.grams.toFixed(1)}g`;
tbody.append(`
<tr>
<td>${item.name} <small class="text-muted">${priceText}</small></td>
<td class="text-end">${costText}</td>
</tr>
`);
});
$('#costPreviewTotal').text(formatCurrency(Math.round(totalCost)));
const status = allHavePrice && items.length > 0
? '<span class="badge bg-success">확정</span>'
: '<span class="badge bg-warning text-dark">일부 미확정</span>';
$('#costPreviewStatus').html(status);
$('#costPreview').show();
}
// 커스텀 처방 감지 함수
function checkCustomPrescription() {
const formulaId = $('#compoundFormula').val();
// 처방이 선택되지 않았거나 직접조제인 경우 리턴
if (!formulaId || formulaId === 'custom' || Object.keys(originalFormulaIngredients).length === 0) {
$('#customPrescriptionBadge').remove();
return;
}
// 현재 약재 구성 수집
const currentIngredients = {};
$('#compoundIngredients tr').each(function() {
const ingredientCode = $(this).attr('data-ingredient-code');
const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val());
if (ingredientCode && gramsPerCheop > 0) {
currentIngredients[ingredientCode] = gramsPerCheop;
}
});
// 변경사항 감지
const customDetails = [];
let isCustom = false;
// 추가된 약재 확인
for (const code in currentIngredients) {
if (!originalFormulaIngredients[code]) {
const herbName = $(`tr[data-ingredient-code="${code}"] .herb-select-compound option:selected`).data('herb-name') || code;
customDetails.push(`${herbName} ${currentIngredients[code]}g 추가`);
isCustom = true;
}
}
// 삭제된 약재 확인
for (const code in originalFormulaIngredients) {
if (!currentIngredients[code]) {
customDetails.push(`${originalFormulaIngredients[code].herb_name} 제거`);
isCustom = true;
}
}
// 용량 변경된 약재 확인
for (const code in currentIngredients) {
if (originalFormulaIngredients[code]) {
const originalGrams = originalFormulaIngredients[code].grams_per_cheop;
const currentGrams = currentIngredients[code];
if (Math.abs(originalGrams - currentGrams) > 0.01) {
const herbName = originalFormulaIngredients[code].herb_name;
customDetails.push(`${herbName} ${originalGrams}g→${currentGrams}g`);
isCustom = true;
}
}
}
// 커스텀 뱃지 표시/숨기기
$('#customPrescriptionBadge').remove();
if (isCustom) {
const badgeHtml = `
<div id="customPrescriptionBadge" class="alert alert-warning mt-2">
<span class="badge bg-warning">가감방</span>
<small class="ms-2">${customDetails.join(' | ')}</small>
</div>
`;
// 처방 선택 영역 아래에 추가
$('#compoundFormula').closest('.col-md-6').append(badgeHtml);
}
}
// 재고 확인
function checkStockForCompound() {
// 각 약재의 재고 상태를 API로 갱신 (기존 선택 보존)
$('#compoundIngredients tr').each(function() {
const herbId = $(this).attr('data-herb-id');
if (!herbId) return;
const totalGrams = parseFloat($(this).find('.total-grams').text()) || 0;
const currentSelection = $(this).find('.origin-select').val();
const $stockStatus = $(this).find('.stock-status');
if (totalGrams > 0) {
$.get(`/api/herbs/${herbId}/available-lots`, function(response) {
if (response.success) {
const totalAvailable = response.data.total_quantity;
const origins = response.data.origins;
const altCount = origins.length;
const altBadge = altCount > 1 ? ` <span class="badge bg-outline-info text-info border border-info" style="font-size:0.65rem">${altCount}종</span>` : '';
if (totalAvailable >= totalGrams) {
$stockStatus.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
} else {
$stockStatus.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
}
}
});
}
});
}
// 조제 약재 추가
// 빈 약재 행 추가 함수
function addEmptyIngredientRow() {
const newRow = $(`
<tr data-ingredient-code="" data-herb-id="">
<td>
<select class="form-control form-control-sm herb-select-compound">
<option value="">약재 선택</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm grams-per-cheop"
min="0.1" step="0.1" placeholder="0.0">
</td>
<td class="total-grams">0.0</td>
<td class="origin-select-cell">
<div class="d-flex gap-1">
<select class="form-control form-control-sm product-select" style="flex: 1;" disabled>
<option value="">약재 선택 후 표시</option>
</select>
<select class="form-control form-control-sm origin-select" style="flex: 1;" disabled>
<option value="">제품 선택 후 표시</option>
</select>
</div>
</td>
<td class="stock-status">-</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
$('#compoundIngredients').append(newRow);
// 약재 목록 로드
const herbSelect = newRow.find('.herb-select-compound');
loadHerbsForSelect(herbSelect);
// 약재(마스터) 선택 시 제품 옵션 로드
newRow.find('.herb-select-compound').on('change', function() {
const ingredientCode = $(this).val();
const herbName = $(this).find('option:selected').data('herb-name');
if (ingredientCode) {
const row = $(this).closest('tr');
row.attr('data-ingredient-code', ingredientCode);
// 제품 목록 로드
loadProductOptions(row, ingredientCode, herbName);
// 제품 선택 활성화
row.find('.product-select').prop('disabled', false);
// 원산지 선택 초기화 및 비활성화
row.find('.origin-select').empty().append('<option value="">제품 선택 후 표시</option>').prop('disabled', true);
} else {
const row = $(this).closest('tr');
row.attr('data-ingredient-code', '');
row.attr('data-herb-id', '');
row.find('.product-select').empty().append('<option value="">약재 선택 후 표시</option>').prop('disabled', true);
row.find('.origin-select').empty().append('<option value="">제품 선택 후 표시</option>').prop('disabled', true);
}
});
// 제품 선택 이벤트
newRow.find('.product-select').on('change', function() {
const herbId = $(this).val();
const row = $(this).closest('tr');
if (herbId) {
row.attr('data-herb-id', herbId);
// 원산지 선택 활성화
row.find('.origin-select').prop('disabled', false);
// 원산지 옵션 로드
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0;
const totalGrams = gramsPerCheop * cheopTotal;
loadOriginOptions(herbId, totalGrams);
} else {
row.attr('data-herb-id', '');
row.find('.origin-select').empty().append('<option value="">제품 선택 후 표시</option>').prop('disabled', true);
}
});
// 이벤트 바인딩
newRow.find('.grams-per-cheop').on('input', function() {
updateIngredientTotals();
// 원산지 옵션 다시 로드
const herbId = $(this).closest('tr').attr('data-herb-id');
if (herbId) {
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
const gramsPerCheop = parseFloat($(this).val()) || 0;
const totalGrams = gramsPerCheop * cheopTotal;
loadOriginOptions(herbId, totalGrams);
}
});
newRow.find('.remove-compound-ingredient').on('click', function() {
$(this).closest('tr').remove();
updateIngredientTotals();
});
}
$('#addIngredientBtn').on('click', function() {
addEmptyIngredientRow();
});
// 기존 약재 추가 버튼 (기존 코드 삭제)
/*
$('#addIngredientBtn').on('click', function() {
const newRow = $(`
<tr>
<td>
<select class="form-control form-control-sm herb-select-compound">
<option value="">약재 선택</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm grams-per-cheop"
min="0.1" step="0.1" placeholder="0.0">
</td>
<td class="total-grams">0.0</td>
<td class="stock-status">-</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
$('#compoundIngredients').append(newRow);
// 약재 목록 로드
loadHerbsForSelect(newRow.find('.herb-select-compound'));
// 이벤트 바인딩
newRow.find('.grams-per-cheop').on('input', updateIngredientTotals);
newRow.find('.remove-compound-ingredient').on('click', function() {
$(this).closest('tr').remove();
updateIngredientTotals(); // 총용량 재계산 및 커스텀 감지
});
newRow.find('.herb-select-compound').on('change', function() {
const herbId = $(this).val();
$(this).closest('tr').attr('data-herb-id', herbId);
updateIngredientTotals();
});
});
*/
// 조제 실행
$('#compoundEntryForm').on('submit', function(e) {
e.preventDefault();
// getIngredientDataForCompound 함수 사용하여 lot_assignments 포함
const ingredients = getIngredientDataForCompound();
const compoundData = {
patient_id: $('#compoundPatient').val() ? parseInt($('#compoundPatient').val()) : null,
formula_id: $('#compoundFormula').val() ? parseInt($('#compoundFormula').val()) : null,
je_count: parseFloat($('#jeCount').val()),
cheop_total: parseFloat($('#cheopTotal').val()),
pouch_total: parseFloat($('#pouchTotal').val()),
usage_type: $('#compoundUsageType').val() || 'SALE',
ingredients: ingredients
};
$.ajax({
url: '/api/compounds',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(compoundData),
success: function(response) {
if (response.success) {
const usageType = $('#compoundUsageType').val();
const usageLabel = {SELF_USE: '자가소비', SAMPLE: '샘플', DISPOSAL: '폐기'}[usageType] || '판매';
alert(`조제가 완료되었습니다. [${usageLabel}]\n원가: ${formatCurrency(response.total_cost)}`);
$('#compoundForm').hide();
$('#compoundUsageType').val('SALE');
loadCompounds();
}
},
error: function(xhr) {
alert('오류: ' + xhr.responseJSON.error);
}
});
});
// 조제 내역 로드
function loadCompounds() {
$.get('/api/compounds', function(response) {
const tbody = $('#compoundsList');
tbody.empty();
if (response.success && response.data.length > 0) {
// 통계 업데이트
const today = new Date().toISOString().split('T')[0];
const currentMonth = new Date().toISOString().slice(0, 7);
let todayCount = 0;
let monthCount = 0;
response.data.forEach((compound, index) => {
// 통계 계산
if (compound.compound_date === today) todayCount++;
if (compound.compound_date && compound.compound_date.startsWith(currentMonth)) monthCount++;
// 상태 뱃지
let statusBadge = '';
switch(compound.status) {
case 'PREPARED':
statusBadge = '<span class="badge bg-primary">조제완료</span>';
break;
case 'PENDING_PAYMENT':
statusBadge = '<span class="badge bg-warning">결제대기</span>';
break;
case 'PAID':
statusBadge = '<span class="badge bg-success">결제완료</span>';
break;
case 'PENDING_DELIVERY':
statusBadge = '<span class="badge bg-info">배송대기</span>';
break;
case 'DELIVERED':
statusBadge = '<span class="badge bg-secondary">배송완료</span>';
break;
case 'COMPLETED':
statusBadge = '<span class="badge bg-dark">판매완료</span>';
break;
case 'OTC_CONVERTED':
statusBadge = '<span class="badge bg-purple">OTC전환</span>';
break;
case 'CANCELLED':
statusBadge = '<span class="badge bg-danger">취소</span>';
break;
case 'REFUNDED':
statusBadge = '<span class="badge bg-danger">환불</span>';
break;
default:
statusBadge = '<span class="badge bg-secondary">대기</span>';
}
// 용도 뱃지 (클릭으로 변경 가능)
const usageLabels = {SALE: '판매', SELF_USE: '자가소비', SAMPLE: '샘플', DISPOSAL: '폐기'};
const usageColors = {SALE: 'success', SELF_USE: 'warning text-dark', SAMPLE: 'info', DISPOSAL: 'secondary'};
const curUsage = compound.usage_type || 'SALE';
const usageBadge = `<span class="badge bg-${usageColors[curUsage]} change-usage" style="cursor:pointer" data-id="${compound.compound_id}" data-current="${curUsage}" title="클릭하여 용도 변경">${usageLabels[curUsage]}</span>`;
const isSale = curUsage === 'SALE';
const row = $(`
<tr>
<td>${response.data.length - index}</td>
<td>${compound.compound_date || ''}<br><small class="text-muted">${compound.created_at ? compound.created_at.split(' ')[1] : ''}</small></td>
<td><strong>${compound.patient_name || '직접조제'}</strong></td>
<td>${formatPhoneNumber(compound.patient_phone)}</td>
<td>${compound.formula_name || '직접조제'}</td>
<td>${compound.je_count || 0}</td>
<td>${compound.cheop_total || 0}</td>
<td>${compound.pouch_total || 0}</td>
<td>${formatCurrency(compound.cost_total || 0)}</td>
<td>${isSale ? formatCurrency(compound.sell_price_total || 0) : '-'}</td>
<td>${usageBadge} ${statusBadge}</td>
<td>${compound.prescription_no || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-info view-compound-detail" data-id="${compound.compound_id}">
<i class="bi bi-eye"></i> 상세
</button>
${compound.status === 'PREPARED' && isSale ? `
<button class="btn btn-sm btn-outline-success process-sale" data-id="${compound.compound_id}"
data-formula="${compound.formula_name || '직접조제'}"
data-patient="${compound.patient_name || '직접조제'}"
data-patient-id="${compound.patient_id || ''}"
data-cost="${compound.cost_total || 0}"
data-price="${compound.sell_price_total || 0}">
<i class="bi bi-cash-coin"></i> 판매
</button>
` : ''}
${compound.status === 'PREPARED' ? `
<button class="btn btn-sm btn-outline-danger cancel-compound" data-id="${compound.compound_id}">
<i class="bi bi-x-circle"></i> 취소
</button>
` : ''}
${compound.status === 'PAID' ? `
<button class="btn btn-sm btn-outline-primary process-delivery" data-id="${compound.compound_id}">
<i class="bi bi-truck"></i> 배송
</button>
` : ''}
</td>
</tr>
`);
tbody.append(row);
});
// 통계 업데이트
$('#todayCompoundCount').text(todayCount);
$('#monthCompoundCount').text(monthCount);
// 상세보기 버튼 이벤트
$('.view-compound-detail').on('click', function() {
const compoundId = $(this).data('id');
viewCompoundDetail(compoundId);
});
// 판매 처리 버튼 이벤트
$('.process-sale').on('click', function() {
const compoundId = $(this).data('id');
const formulaName = $(this).data('formula');
const patientName = $(this).data('patient');
const patientId = $(this).data('patient-id');
const costTotal = $(this).data('cost');
const priceTotal = $(this).data('price');
openSalesModal(compoundId, formulaName, patientName, patientId, costTotal, priceTotal);
});
// 배송 처리 버튼 이벤트
$('.process-delivery').on('click', function() {
const compoundId = $(this).data('id');
processDelivery(compoundId);
});
// 조제 취소 버튼 이벤트
$('.cancel-compound').on('click', function() {
const compoundId = $(this).data('id');
if (confirm('정말 취소하시겠습니까? 사용된 재고가 복원됩니다.')) {
$.ajax({
url: `/api/compounds/${compoundId}/status`,
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ status: 'CANCELLED', reason: '조제 취소 (재고 복원)' }),
success: function(response) {
if (response.success) {
alert('조제가 취소되었고 재고가 복원되었습니다.');
loadCompounds();
} else {
alert(response.error || '취소 처리 중 오류가 발생했습니다.');
}
},
error: function(xhr) {
const err = xhr.responseJSON;
alert(err?.error || '취소 처리 중 오류가 발생했습니다.');
}
});
}
});
// 용도 변경 뱃지 클릭 이벤트
$('.change-usage').on('click', function() {
const compoundId = $(this).data('id');
const current = $(this).data('current');
const options = {SALE: '판매', SELF_USE: '자가소비', SAMPLE: '샘플', DISPOSAL: '폐기'};
const choices = Object.entries(options)
.map(([k, v]) => `${k === current ? '● ' : ' '}${v}`)
.join('\n');
const input = prompt(`용도를 선택하세요 (현재: ${options[current]})\n\n1: 판매\n2: 자가소비\n3: 샘플\n4: 폐기`, current === 'SALE' ? '1' : current === 'SELF_USE' ? '2' : current === 'SAMPLE' ? '3' : '4');
if (!input) return;
const typeMap = {'1': 'SALE', '2': 'SELF_USE', '3': 'SAMPLE', '4': 'DISPOSAL'};
const newType = typeMap[input.trim()];
if (!newType) { alert('잘못된 입력입니다.'); return; }
if (newType === current) return;
$.ajax({
url: `/api/compounds/${compoundId}/usage-type`,
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify({ usage_type: newType }),
success: function(response) {
if (response.success) {
loadCompounds();
loadDashboard();
} else {
alert(response.error || '변경 실패');
}
},
error: function(xhr) {
alert(xhr.responseJSON?.error || '변경 실패');
}
});
});
} else {
tbody.html('<tr><td colspan="13" class="text-center text-muted">조제 내역이 없습니다.</td></tr>');
$('#todayCompoundCount').text(0);
$('#monthCompoundCount').text(0);
}
}).fail(function() {
$('#compoundsList').html('<tr><td colspan="13" class="text-center text-danger">데이터를 불러오는데 실패했습니다.</td></tr>');
});
}
// 조제 상세보기
function viewCompoundDetail(compoundId) {
$.get(`/api/compounds/${compoundId}`, function(response) {
if (response.success && response.data) {
const data = response.data;
// 환자 정보
$('#detailPatientName').text(data.patient_name || '직접조제');
$('#detailPatientPhone').text(formatPhoneNumber(data.patient_phone));
$('#detailCompoundDate').text(data.compound_date || '-');
// 처방 정보 (가감방 표시 포함)
let formulaDisplay = data.formula_name || '직접조제';
if (data.is_custom && data.formula_name) {
formulaDisplay += ' <span class="badge bg-warning">가감</span>';
if (data.custom_summary) {
formulaDisplay += `<br><small class="text-muted">${data.custom_summary}</small>`;
}
}
$('#detailFormulaName').html(formulaDisplay); // text() 대신 html() 사용
$('#detailPrescriptionNo').text(data.prescription_no || '-');
$('#detailQuantities').text(`${data.je_count}제 / ${data.cheop_total}첩 / ${data.pouch_total}파우치`);
// 처방 구성 약재
const ingredientsBody = $('#detailIngredients');
ingredientsBody.empty();
if (data.ingredients && data.ingredients.length > 0) {
data.ingredients.forEach(ing => {
ingredientsBody.append(`
<tr>
<td>${ing.herb_name}</td>
<td>${ing.insurance_code || '-'}</td>
<td>${ing.grams_per_cheop}g</td>
<td>${ing.total_grams}g</td>
<td>${ing.notes || '-'}</td>
</tr>
`);
});
}
// 재고 소비 내역
const consumptionsBody = $('#detailConsumptions');
consumptionsBody.empty();
if (data.consumptions && data.consumptions.length > 0) {
data.consumptions.forEach(con => {
consumptionsBody.append(`
<tr>
<td>${con.herb_name}</td>
<td>${con.origin_country || '-'}</td>
<td>${con.supplier_name || '-'}</td>
<td>${con.quantity_used}g</td>
<td>${formatCurrency(con.unit_cost_per_g)}/g</td>
<td>${formatCurrency(con.cost_amount)}</td>
</tr>
`);
});
}
// 총 원가
$('#detailTotalCost').text(formatCurrency(data.cost_total || 0));
// 비고
$('#detailNotes').text(data.notes || '');
// 부모 모달(환자 처방 내역)을 임시로 숨기고 조제 상세 모달 열기
const parentModal = $('#patientCompoundsModal');
const wasParentOpen = parentModal.hasClass('show');
if (wasParentOpen) {
// 부모 모달 숨기기 (DOM에서 제거하지 않음)
parentModal.modal('hide');
// 조제 상세 모달이 닫힐 때 부모 모달 다시 열기
$('#compoundDetailModal').off('hidden.bs.modal').on('hidden.bs.modal', function() {
parentModal.modal('show');
});
}
// 조제 상세 모달 열기
$('#compoundDetailModal').modal('show');
}
}).fail(function() {
alert('조제 상세 정보를 불러오는데 실패했습니다.');
});
}
// 재고 현황 로드
function loadInventory() {
$.get('/api/inventory/summary', function(response) {
if (response.success) {
const tbody = $('#inventoryList');
tbody.empty();
let totalValue = 0;
let herbsInStock = 0;
// 주성분코드 기준 보유 현황 표시
if (response.summary) {
const summary = response.summary;
const coverageHtml = `
<div class="alert alert-info mb-3">
<div class="row align-items-center">
<div class="col-md-8">
<h6 class="mb-2">📊 급여 약재 보유 현황</h6>
<div class="d-flex align-items-center">
<div class="me-4">
<strong>전체 급여 약재:</strong> ${summary.total_ingredient_codes || 454}개 주성분
</div>
<div class="me-4">
<strong>보유 약재:</strong> ${summary.owned_ingredient_codes || 0}개 주성분
</div>
<div>
<strong>보유율:</strong>
<span class="badge bg-primary fs-6">${summary.coverage_rate || 0}%</span>
</div>
</div>
</div>
<div class="col-md-4 text-end">
<div class="progress" style="height: 30px;">
<div class="progress-bar bg-success" role="progressbar"
style="width: ${summary.coverage_rate || 0}%"
aria-valuenow="${summary.coverage_rate || 0}"
aria-valuemin="0" aria-valuemax="100">
${summary.owned_ingredient_codes || 0} / ${summary.total_ingredient_codes || 454}
</div>
</div>
</div>
</div>
<div class="mt-2">
<small class="text-muted">
※ 건강보험 급여 한약재 ${summary.total_ingredient_codes || 454}개 주성분 중 ${summary.owned_ingredient_codes || 0}개 보유
</small>
</div>
</div>
`;
// 재고 테이블 위에 통계 표시
if ($('#inventoryCoverage').length === 0) {
$('#inventoryList').parent().before(`<div id="inventoryCoverage">${coverageHtml}</div>`);
} else {
$('#inventoryCoverage').html(coverageHtml);
}
}
response.data.forEach(item => {
// 원산지가 여러 개인 경우 표시
const originBadge = item.origin_count > 1
? `<span class="badge bg-info ms-2">${item.origin_count}개 원산지</span>`
: '';
// 효능 태그 표시
let efficacyTags = '';
if (item.efficacy_tags && item.efficacy_tags.length > 0) {
efficacyTags = item.efficacy_tags.map(tag =>
`<span class="badge bg-success ms-1">${tag}</span>`
).join('');
}
// 가격 범위 표시 (원산지가 여러 개이고 가격차가 있는 경우)
let priceDisplay = item.avg_price ? formatCurrency(item.avg_price) : '-';
if (item.origin_count > 1 && item.min_price && item.max_price && item.min_price !== item.max_price) {
priceDisplay = `${formatCurrency(item.min_price)} ~ ${formatCurrency(item.max_price)}`;
}
// 통계 업데이트
totalValue += item.total_value || 0;
if (item.total_quantity > 0) herbsInStock++;
const isOTC = item.product_type === 'OTC';
const typeBadge = isOTC
? '<span class="badge bg-warning text-dark">OTC</span>'
: '<span class="badge bg-info">한약재</span>';
const codeDisplay = isOTC
? (item.standard_code || '-')
: (item.insurance_code || '-');
tbody.append(`
<tr class="inventory-row" data-herb-id="${item.herb_item_id}">
<td>${typeBadge}</td>
<td><small class="text-monospace">${codeDisplay}</small></td>
<td>${item.herb_name}${originBadge}${efficacyTags}</td>
<td>${item.total_quantity.toFixed(1)}</td>
<td>${item.lot_count}</td>
<td>${priceDisplay}</td>
<td>${formatCurrency(item.total_value)}</td>
<td>
<button class="btn btn-sm btn-outline-info view-stock-ledger" data-herb-id="${item.herb_item_id}" data-herb-name="${item.herb_name}">
<i class="bi bi-journal-text"></i> 입출고
</button>
<button class="btn btn-sm btn-outline-primary view-inventory-detail" data-herb-id="${item.herb_item_id}" style="cursor: pointer;">
<i class="bi bi-eye"></i> 상세
</button>
</td>
</tr>
`);
});
// 통계 업데이트
$('#totalInventoryValue').text(formatCurrency(totalValue));
$('#totalHerbsInStock').text(`${herbsInStock}`);
// 클릭 이벤트 바인딩
$('.view-inventory-detail').on('click', function(e) {
e.stopPropagation();
const herbId = $(this).data('herb-id');
showInventoryDetail(herbId);
});
// 입출고 내역 버튼 이벤트
$('.view-stock-ledger').on('click', function(e) {
e.stopPropagation();
const herbId = $(this).data('herb-id');
const herbName = $(this).data('herb-name');
viewStockLedger(herbId, herbName);
});
}
});
}
// 재고 상세 모달 표시
function showInventoryDetail(herbId) {
$.get(`/api/inventory/detail/${herbId}`, function(response) {
if (response.success) {
const data = response.data;
// 원산지별 재고 정보 HTML 생성
let originsHtml = '';
data.origins.forEach(origin => {
originsHtml += `
<div class="card mb-3">
<div class="card-header bg-light">
<h6 class="mb-0">
<i class="bi bi-geo-alt"></i> ${origin.origin_country}
<span class="badge bg-primary float-end">${origin.total_quantity.toFixed(1)}g</span>
</h6>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-6">
<small class="text-muted">평균 단가:</small><br>
<strong>${formatCurrency(origin.avg_price)}/g</strong>
</div>
<div class="col-6">
<small class="text-muted">재고 가치:</small><br>
<strong>${formatCurrency(origin.total_value)}</strong>
</div>
</div>
<table class="table table-sm">
<thead>
<tr>
<th>로트ID</th>
<th>품명</th>
<th>수량</th>
<th>단가</th>
<th>입고일</th>
<th>유통기한</th>
<th>도매상</th>
</tr>
</thead>
<tbody>`;
origin.lots.forEach(lot => {
// variant 속성들을 뱃지로 표시
let variantBadges = '';
if (lot.form) variantBadges += `<span class="badge bg-info ms-1">${lot.form}</span>`;
if (lot.processing) variantBadges += `<span class="badge bg-warning ms-1">${lot.processing}</span>`;
if (lot.grade) variantBadges += `<span class="badge bg-success ms-1">${lot.grade}</span>`;
originsHtml += `
<tr>
<td>#${lot.lot_id}</td>
<td>
${lot.display_name ? `<small class="text-primary">${lot.display_name}</small>` : '-'}
${variantBadges}
</td>
<td>${lot.quantity_onhand.toFixed(1)}g</td>
<td>${formatCurrency(lot.unit_price_per_g)}</td>
<td>${lot.received_date}</td>
<td>${lot.expiry_date || '-'}</td>
<td>${lot.supplier_name || '-'}</td>
</tr>`;
});
originsHtml += `
</tbody>
</table>
</div>
</div>`;
});
// 모달 생성 및 표시
const modalHtml = `
<div class="modal fade" id="inventoryDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
${data.herb_name} 재고 상세
<small class="text-muted">(${data.product_type === 'OTC' ? data.standard_code : data.insurance_code})</small>
${data.product_type === 'OTC' ? '<span class="badge bg-warning text-dark ms-2">OTC</span>' : ''}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
${data.total_origins > 1
? `<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
이 약재는 ${data.total_origins}개 원산지의 재고가 있습니다.
조제 시 원산지를 선택할 수 있습니다.
</div>`
: ''}
${originsHtml}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
</div>
</div>
</div>
</div>`;
// 기존 모달 제거
$('#inventoryDetailModal').remove();
$('body').append(modalHtml);
// 모달 표시
const modal = new bootstrap.Modal(document.getElementById('inventoryDetailModal'));
modal.show();
}
});
}
// 약재 목록 로드
function loadHerbs() {
$.get('/api/herbs', function(response) {
if (response.success) {
const tbody = $('#herbsList');
tbody.empty();
response.data.forEach(herb => {
const hIsOTC = herb.product_type === 'OTC';
const hCode = hIsOTC ? (herb.standard_code || '-') : (herb.insurance_code || '-');
const hTypeBadge = hIsOTC
? '<span class="badge bg-warning text-dark">OTC</span>'
: '<span class="badge bg-info">한약재</span>';
tbody.append(`
<tr>
<td>${hTypeBadge} <small class="text-monospace">${hCode}</small></td>
<td>${herb.herb_name}</td>
<td>${herb.specification || '-'}</td>
<td>${herb.current_stock ? herb.current_stock.toFixed(1) + 'g' : '0g'}</td>
<td>
<button class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
`);
});
}
});
}
// 입고장 목록 로드
function loadPurchaseReceipts() {
const startDate = $('#purchaseStartDate').val();
const endDate = $('#purchaseEndDate').val();
const supplierId = $('#purchaseSupplier').val();
let url = '/api/purchase-receipts?';
if (startDate) url += `start_date=${startDate}&`;
if (endDate) url += `end_date=${endDate}&`;
if (supplierId) url += `supplier_id=${supplierId}`;
$.get(url, function(response) {
if (response.success) {
const tbody = $('#purchaseReceiptsList');
tbody.empty();
if (response.data.length === 0) {
tbody.append('<tr><td colspan="7" class="text-center">입고장이 없습니다.</td></tr>');
return;
}
response.data.forEach(receipt => {
tbody.append(`
<tr>
<td>${receipt.receipt_date}</td>
<td>${receipt.supplier_name}</td>
<td>${receipt.line_count}개</td>
<td class="fw-bold text-primary">${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'}</td>
<td class="text-muted small">${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'}</td>
<td>${receipt.source_file || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-info view-receipt" data-id="${receipt.receipt_id}">
<i class="bi bi-eye"></i> 상세
</button>
<button class="btn btn-sm btn-outline-warning edit-receipt" data-id="${receipt.receipt_id}">
<i class="bi bi-pencil"></i> 수정
</button>
<button class="btn btn-sm btn-outline-danger delete-receipt" data-id="${receipt.receipt_id}">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`);
});
// 이벤트 바인딩
$('.view-receipt').on('click', function() {
const receiptId = $(this).data('id');
viewReceiptDetail(receiptId);
});
$('.edit-receipt').on('click', function() {
const receiptId = $(this).data('id');
editReceipt(receiptId);
});
$('.delete-receipt').on('click', function() {
const receiptId = $(this).data('id');
if (confirm('정말 이 입고장을 삭제하시겠습니까? 사용되지 않은 재고만 삭제 가능합니다.')) {
deleteReceipt(receiptId);
}
});
}
});
}
// 입고장 상세 보기
function viewReceiptDetail(receiptId) {
$.get(`/api/purchase-receipts/${receiptId}`, function(response) {
if (response.success) {
const data = response.data;
let linesHtml = '';
data.lines.forEach(line => {
// display_name이 있으면 표시, 없으면 herb_name
const displayName = line.display_name || line.herb_name;
// variant 속성들을 뱃지로 표시
let variantBadges = '';
if (line.form) variantBadges += `<span class="badge bg-info ms-1">${line.form}</span>`;
if (line.processing) variantBadges += `<span class="badge bg-warning ms-1">${line.processing}</span>`;
if (line.grade) variantBadges += `<span class="badge bg-success ms-1">${line.grade}</span>`;
const lineIsOTC = line.product_type === 'OTC';
const lineTypeBadge = lineIsOTC
? '<span class="badge bg-warning text-dark me-1">OTC</span>'
: '<span class="badge bg-info me-1">한약재</span>';
const lineCode = lineIsOTC
? (line.standard_code || '-')
: (line.insurance_code || '-');
const lineCodeLabel = lineIsOTC ? '표준' : '보험';
linesHtml += `
<tr>
<td>
${lineTypeBadge}
<div>${line.herb_name}</div>
${line.display_name ? `<small class="text-primary">${line.display_name}</small>` : ''}
${variantBadges}
</td>
<td><small class="text-muted">${lineCodeLabel}</small><br><small class="text-monospace">${lineCode}</small></td>
<td>${line.origin_country || '-'}</td>
<td>${line.quantity_g}g</td>
<td>${formatCurrency(line.unit_price_per_g)}</td>
<td>${formatCurrency(line.line_total)}</td>
<td>${line.current_stock}g</td>
</tr>
`;
});
const modalHtml = `
<div class="modal fade" id="receiptDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">입고장 상세</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<strong>입고일:</strong> ${data.receipt_date}<br>
<strong>공급업체:</strong> ${data.supplier_name}<br>
<strong>총 금액:</strong> ${formatCurrency(data.total_amount)}
</div>
<table class="table table-sm">
<thead>
<tr>
<th>품목명</th>
<th>코드</th>
<th>원산지</th>
<th>수량</th>
<th>단가</th>
<th>금액</th>
<th>현재고</th>
</tr>
</thead>
<tbody>
${linesHtml}
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
// 기존 모달 제거
$('#receiptDetailModal').remove();
$('body').append(modalHtml);
$('#receiptDetailModal').modal('show');
}
});
}
// 입고장 삭제
function deleteReceipt(receiptId) {
$.ajax({
url: `/api/purchase-receipts/${receiptId}`,
method: 'DELETE',
success: function(response) {
if (response.success) {
alert(response.message);
loadPurchaseReceipts();
}
},
error: function(xhr) {
alert('오류: ' + xhr.responseJSON.error);
}
});
}
// 입고장 조회 버튼
$('#searchPurchaseBtn').on('click', function() {
loadPurchaseReceipts();
});
// 도매상 목록 로드 (셀렉트 박스용)
function loadSuppliersForSelect() {
$.get('/api/suppliers', function(response) {
if (response.success) {
const select = $('#uploadSupplier');
select.empty().append('<option value="">도매상을 선택하세요</option>');
response.data.forEach(supplier => {
select.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
});
// 필터용 셀렉트 박스도 업데이트
const filterSelect = $('#purchaseSupplier');
filterSelect.empty().append('<option value="">전체</option>');
response.data.forEach(supplier => {
filterSelect.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
});
// 수동 입고용 셀렉트 박스도 업데이트
const manualSelect = $('#manualReceiptSupplier');
manualSelect.empty().append('<option value="">도매상을 선택하세요</option>');
response.data.forEach(supplier => {
manualSelect.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
});
}
});
}
// 도매상 등록
$('#saveSupplierBtn').on('click', function() {
const supplierData = {
name: $('#supplierName').val(),
business_no: $('#supplierBusinessNo').val(),
contact_person: $('#supplierContactPerson').val(),
phone: $('#supplierPhone').val(),
address: $('#supplierAddress').val()
};
if (!supplierData.name) {
alert('도매상명은 필수입니다.');
return;
}
$.ajax({
url: '/api/suppliers',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(supplierData),
success: function(response) {
if (response.success) {
alert('도매상이 등록되었습니다.');
$('#supplierModal').modal('hide');
$('#supplierForm')[0].reset();
loadSuppliersForSelect();
}
},
error: function(xhr) {
alert('오류: ' + xhr.responseJSON.error);
}
});
});
// ==================== 수동 입고 ====================
// 전체 약재 목록 로드 (입고용 - 재고 필터 없음)
function loadAllHerbsForSelect(selectElement, initialValue) {
$.get('/api/herbs/masters', function(response) {
if (response.success) {
selectElement.empty().append('<option value="">약재 선택</option>');
response.data.forEach(herb => {
let displayName = herb.herb_name;
if (herb.herb_name_hanja) {
displayName += ` (${herb.herb_name_hanja})`;
}
selectElement.append(`<option value="${herb.ingredient_code}" data-herb-name="${herb.herb_name}">${displayName}</option>`);
});
if (initialValue) {
selectElement.val(initialValue);
}
}
});
}
// 입고장 수정 모드
function editReceipt(receiptId) {
$.get(`/api/purchase-receipts/${receiptId}`, function(response) {
if (!response.success) {
alert('입고장 데이터를 불러올 수 없습니다: ' + response.error);
return;
}
const receipt = response.data;
const modal = $('#manualReceiptModal');
// 수정 모드 플래그 설정
modal.data('edit-mode', true);
modal.data('receipt-id', receiptId);
// 모달 제목 변경
modal.find('.modal-title').html('<i class="bi bi-pencil"></i> 입고장 수정');
modal.find('.modal-header').removeClass('bg-success').addClass('bg-warning');
// 헤더 정보 채우기
$('#manualReceiptDate').val(receipt.receipt_date);
$('#manualReceiptSupplier').val(receipt.supplier_id);
$('#manualReceiptSupplier').prop('disabled', true);
$('#manualReceiptNotes').val(receipt.notes || '');
// 품목 추가 버튼 숨김
$('#addManualReceiptLineBtn').hide();
// 기존 라인 비우기
$('#manualReceiptLines').empty();
manualReceiptLineCount = 0;
// 저장 버튼 텍스트 변경
$('#saveManualReceiptBtn').html('<i class="bi bi-check-circle"></i> 수정 저장');
// 기존 라인 데이터로 행 채우기
receipt.lines.forEach(line => {
manualReceiptLineCount++;
const row = `
<tr data-row="${manualReceiptLineCount}" data-line-id="${line.line_id}">
<td>
<select class="form-control form-control-sm manual-herb-select" disabled>
<option value="">약재 선택</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm manual-qty-input text-end"
min="0.1" step="0.1" value="${line.quantity_g || ''}">
</td>
<td>
<input type="number" class="form-control form-control-sm manual-price-input text-end"
min="0" step="0.1" value="${line.unit_price_per_g || ''}">
</td>
<td class="text-end manual-line-total">${(line.line_total || 0).toLocaleString('ko-KR')}</td>
<td>
<select class="form-control form-control-sm manual-origin-input">
<option value="">원산지 선택</option>
${['한국','중국','베트남','인도','태국','페루','일본','기타'].map(c =>
`<option value="${c}" ${(line.origin_country || '') === c ? 'selected' : ''}>${c}</option>`
).join('')}
</select>
</td>
<td>
<input type="text" class="form-control form-control-sm manual-lot-input"
placeholder="로트번호" value="${line.lot_number || ''}">
</td>
<td>
<input type="date" class="form-control form-control-sm manual-expiry-input"
value="${line.expiry_date || ''}">
</td>
<td></td>
</tr>`;
$('#manualReceiptLines').append(row);
const newRow = $(`#manualReceiptLines tr[data-row="${manualReceiptLineCount}"]`);
// 약재 select에 옵션 로드 후 기존 값 선택
loadAllHerbsForSelect(newRow.find('.manual-herb-select'), line.ingredient_code);
// 금액 자동 계산 이벤트
newRow.find('.manual-qty-input, .manual-price-input').on('input', function() {
updateManualReceiptLineTotals();
});
});
updateManualReceiptLineTotals();
// 모달 열기 (show.bs.modal 이벤트 비활성화를 위해 직접 설정)
modal.modal('show');
});
}
let manualReceiptLineCount = 0;
function addManualReceiptLine() {
manualReceiptLineCount++;
const row = `
<tr data-row="${manualReceiptLineCount}">
<td>
<select class="form-control form-control-sm manual-herb-select">
<option value="">약재 선택</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm manual-qty-input text-end"
min="0.1" step="0.1" placeholder="0.0">
</td>
<td>
<input type="number" class="form-control form-control-sm manual-price-input text-end"
min="0" step="0.1" placeholder="0.0">
</td>
<td class="text-end manual-line-total">0</td>
<td>
<select class="form-control form-control-sm manual-origin-input">
<option value="">원산지 선택</option>
<option value="한국">한국</option>
<option value="중국">중국</option>
<option value="베트남">베트남</option>
<option value="인도">인도</option>
<option value="태국">태국</option>
<option value="페루">페루</option>
<option value="일본">일본</option>
<option value="기타">기타</option>
</select>
</td>
<td>
<input type="text" class="form-control form-control-sm manual-lot-input" placeholder="로트번호">
</td>
<td>
<input type="date" class="form-control form-control-sm manual-expiry-input">
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-manual-line">
<i class="bi bi-x"></i>
</button>
</td>
</tr>`;
$('#manualReceiptLines').append(row);
const newRow = $(`#manualReceiptLines tr[data-row="${manualReceiptLineCount}"]`);
loadAllHerbsForSelect(newRow.find('.manual-herb-select'));
// 금액 자동 계산
newRow.find('.manual-qty-input, .manual-price-input').on('input', function() {
updateManualReceiptLineTotals();
});
// 삭제 버튼
newRow.find('.remove-manual-line').on('click', function() {
$(this).closest('tr').remove();
updateManualReceiptLineTotals();
});
}
function updateManualReceiptLineTotals() {
let totalQty = 0;
let totalAmount = 0;
$('#manualReceiptLines tr').each(function() {
const qty = parseFloat($(this).find('.manual-qty-input').val()) || 0;
const price = parseFloat($(this).find('.manual-price-input').val()) || 0;
const lineTotal = qty * price;
$(this).find('.manual-line-total').text(lineTotal.toLocaleString('ko-KR'));
totalQty += qty;
totalAmount += lineTotal;
});
$('#manualReceiptTotalQty').text(totalQty.toLocaleString('ko-KR'));
$('#manualReceiptTotalAmount').text(totalAmount.toLocaleString('ko-KR'));
}
// 모달 열릴 때 초기화 (수정 모드가 아닐 때만)
$('#manualReceiptModal').on('show.bs.modal', function() {
const modal = $(this);
if (modal.data('edit-mode')) {
// 수정 모드에서는 초기화하지 않음 (editReceipt에서 이미 설정함)
return;
}
// 새 입고 모드 초기화
modal.find('.modal-title').html('<i class="bi bi-plus-circle"></i> 수동 입고');
modal.find('.modal-header').removeClass('bg-warning').addClass('bg-success');
$('#saveManualReceiptBtn').html('<i class="bi bi-check-circle"></i> 입고 저장');
$('#addManualReceiptLineBtn').show();
$('#manualReceiptSupplier').prop('disabled', false);
const today = new Date().toISOString().split('T')[0];
$('#manualReceiptDate').val(today);
$('#manualReceiptSupplier').val('');
$('#manualReceiptNotes').val('');
$('#manualReceiptLines').empty();
manualReceiptLineCount = 0;
updateManualReceiptLineTotals();
addManualReceiptLine();
});
// 모달 닫힐 때 수정 모드 플래그 초기화
$('#manualReceiptModal').on('hidden.bs.modal', function() {
const modal = $(this);
modal.removeData('edit-mode');
modal.removeData('receipt-id');
$('#manualReceiptSupplier').prop('disabled', false);
});
// 품목 추가 버튼
$('#addManualReceiptLineBtn').on('click', function() {
addManualReceiptLine();
});
// 새 도매상 등록 버튼 (수동 입고 모달에서)
$('#manualReceiptAddSupplierBtn').on('click', function() {
$('#supplierModal').modal('show');
});
// 도매상 모달이 수동입고 모달 위에 뜨도록 z-index 조정
$('#supplierModal').on('shown.bs.modal', function() {
if ($('#manualReceiptModal').hasClass('show')) {
$(this).css('z-index', 1060);
$('.modal-backdrop').last().css('z-index', 1055);
}
});
$('#supplierModal').on('hidden.bs.modal', function() {
$(this).css('z-index', '');
});
// 입고 저장 (새 입고 / 수정 공통)
$('#saveManualReceiptBtn').on('click', function() {
const modal = $('#manualReceiptModal');
const isEditMode = modal.data('edit-mode');
const receiptId = modal.data('receipt-id');
const supplierId = $('#manualReceiptSupplier').val();
const receiptDate = $('#manualReceiptDate').val();
const notes = $('#manualReceiptNotes').val();
if (!supplierId) {
alert('도매상을 선택해주세요.');
return;
}
if (!receiptDate) {
alert('입고일을 입력해주세요.');
return;
}
const lines = [];
let valid = true;
$('#manualReceiptLines tr').each(function() {
const ingredientCode = $(this).find('.manual-herb-select').val();
const qty = parseFloat($(this).find('.manual-qty-input').val()) || 0;
const price = parseFloat($(this).find('.manual-price-input').val()) || 0;
if (!ingredientCode) {
valid = false;
alert('약재를 선택해주세요.');
return false;
}
if (qty <= 0) {
valid = false;
alert('수량을 입력해주세요.');
return false;
}
if (price <= 0) {
valid = false;
alert('단가를 입력해주세요.');
return false;
}
const lineData = {
ingredient_code: ingredientCode,
quantity_g: qty,
unit_price_per_g: price,
origin_country: $(this).find('.manual-origin-input').val(),
lot_number: $(this).find('.manual-lot-input').val(),
expiry_date: $(this).find('.manual-expiry-input').val()
};
// 수정 모드에서는 line_id 포함
if (isEditMode) {
lineData.line_id = $(this).data('line-id');
}
lines.push(lineData);
});
if (!valid) return;
if (lines.length === 0) {
alert('입고 품목을 1개 이상 추가해주세요.');
return;
}
const btn = $(this);
btn.prop('disabled', true).text('저장 중...');
if (isEditMode) {
// 수정 모드: PUT bulk
$.ajax({
url: `/api/purchase-receipts/${receiptId}/bulk`,
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
notes: notes,
lines: lines
}),
success: function(response) {
if (response.success) {
alert('입고장이 수정되었습니다.');
$('#manualReceiptModal').modal('hide');
loadPurchaseReceipts();
} else {
alert('오류: ' + response.error);
}
},
error: function(xhr) {
const msg = xhr.responseJSON ? xhr.responseJSON.error : '서버 오류가 발생했습니다.';
alert('오류: ' + msg);
},
complete: function() {
btn.prop('disabled', false).html('<i class="bi bi-check-circle"></i> 수정 저장');
}
});
} else {
// 새 입고 모드: POST
$.ajax({
url: '/api/purchase-receipts/manual',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
supplier_id: supplierId,
receipt_date: receiptDate,
notes: notes,
lines: lines
}),
success: function(response) {
if (response.success) {
alert(`수동 입고 완료!\n입고번호: ${response.receipt_no}\n품목 수: ${response.summary.item_count}\n총 금액: ${response.summary.total_amount}`);
$('#manualReceiptModal').modal('hide');
loadPurchaseReceipts();
} else {
alert('오류: ' + response.error);
}
},
error: function(xhr) {
const msg = xhr.responseJSON ? xhr.responseJSON.error : '서버 오류가 발생했습니다.';
alert('오류: ' + msg);
},
complete: function() {
btn.prop('disabled', false).html('<i class="bi bi-check-circle"></i> 입고 저장');
}
});
}
});
// 입고장 업로드
$('#purchaseUploadForm').on('submit', function(e) {
e.preventDefault();
const supplierId = $('#uploadSupplier').val();
if (!supplierId) {
alert('도매상을 선택해주세요.');
return;
}
const formData = new FormData();
const fileInput = $('#purchaseFile')[0];
if (fileInput.files.length === 0) {
alert('파일을 선택해주세요.');
return;
}
formData.append('file', fileInput.files[0]);
formData.append('supplier_id', supplierId);
$('#uploadResult').html('<div class="alert alert-info">업로드 중...</div>');
$.ajax({
url: '/api/upload/purchase',
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
let summaryHtml = '';
if (response.summary) {
summaryHtml = `<br>
<small>
형식: ${response.summary.format}<br>
처리: ${response.summary.processed_rows}개 라인<br>
품목: ${response.summary.total_items}종<br>
수량: ${response.summary.total_quantity}<br>
금액: ${response.summary.total_amount}
</small>`;
}
$('#uploadResult').html(
`<div class="alert alert-success">
<i class="bi bi-check-circle"></i> ${response.message}
${summaryHtml}
</div>`
);
$('#purchaseUploadForm')[0].reset();
// 입고장 목록 새로고침
loadPurchaseReceipts();
}
},
error: function(xhr) {
$('#uploadResult').html(
`<div class="alert alert-danger">
<i class="bi bi-x-circle"></i> 오류: ${xhr.responseJSON.error}
</div>`
);
}
});
});
// 검색 기능
$('#patientSearch').on('keyup', function() {
const value = $(this).val().toLowerCase();
$('#patientsList tr').filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1);
});
});
$('#inventorySearch').on('keyup', function() {
const value = $(this).val().toLowerCase();
$('#inventoryList tr').filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1);
});
});
// 헬퍼 함수들
function loadPatientsForSelect() {
$.get('/api/patients', function(response) {
if (response.success) {
const select = $('#compoundPatient');
select.empty().append('<option value="">환자를 선택하세요</option>');
response.data.forEach(patient => {
select.append(`<option value="${patient.patient_id}">${patient.name} (${patient.phone})</option>`);
});
}
});
}
function loadFormulasForSelect() {
$.get('/api/formulas', function(response) {
if (response.success) {
const select = $('#compoundFormula');
select.empty().append('<option value="">처방을 선택하세요</option>');
// 직접조제 옵션 추가
select.append('<option value="custom">직접조제</option>');
// 등록된 처방 추가
if (response.data.length > 0) {
select.append('<optgroup label="등록된 처방">');
response.data.forEach(formula => {
select.append(`<option value="${formula.formula_id}">${formula.formula_name}</option>`);
});
select.append('</optgroup>');
}
}
});
}
function loadHerbsForSelect(selectElement) {
$.get('/api/herbs/masters', function(response) {
if (response.success) {
selectElement.empty().append('<option value="">약재 선택</option>');
// 재고가 있는 약재만 필터링하여 표시
const herbsWithStock = response.data.filter(herb => herb.has_stock === 1);
herbsWithStock.forEach(herb => {
// ingredient_code를 value로 사용하고, 한글명(한자명) 형식으로 표시
let displayName = herb.herb_name;
if (herb.herb_name_hanja) {
displayName += ` (${herb.herb_name_hanja})`;
}
selectElement.append(`<option value="${herb.ingredient_code}" data-herb-name="${herb.herb_name}">${displayName}</option>`);
});
}
}).fail(function(error) {
console.error('Failed to load herbs:', error);
});
}
// 현재 선택된 값을 유지하면서 약재 목록 로드
function loadHerbsForSelectWithCurrent(selectElement, currentValue, currentText) {
$.get('/api/herbs/masters', function(response) {
if (response.success) {
selectElement.empty().append('<option value="">약재 선택</option>');
// 재고가 있는 약재만 필터링하여 표시
const herbsWithStock = response.data.filter(herb => herb.has_stock === 1);
let currentFound = false;
herbsWithStock.forEach(herb => {
// ingredient_code를 value로 사용하고, 한글명(한자명) 형식으로 표시
let displayName = herb.herb_name;
if (herb.herb_name_hanja) {
displayName += ` (${herb.herb_name_hanja})`;
}
const isSelected = herb.ingredient_code === currentValue;
if (isSelected) {
currentFound = true;
}
selectElement.append(`<option value="${herb.ingredient_code}" data-herb-name="${herb.herb_name}" ${isSelected ? 'selected' : ''}>${displayName}</option>`);
});
// 만약 현재 선택된 약재가 목록에 없다면 (재고가 없거나 비활성 상태일 경우) 추가
if (!currentFound && currentValue && currentText) {
selectElement.append(`<option value="${currentValue}" data-herb-name="${currentText}" selected>${currentText} (재고없음)</option>`);
}
}
}).fail(function(error) {
console.error('Failed to load herbs:', error);
// 로드 실패시에도 현재 선택된 값은 유지
if (currentValue && currentText) {
selectElement.append(`<option value="${currentValue}" data-herb-name="${currentText}" selected>${currentText}</option>`);
}
});
}
// ingredient_code 기반으로 제품 옵션 로드
function loadProductOptions(row, ingredientCode, herbName) {
$.get(`/api/herbs/by-ingredient/${ingredientCode}`, function(response) {
if (response.success) {
const productSelect = row.find('.product-select');
productSelect.empty();
if (response.data.length === 0) {
productSelect.append('<option value="">재고 없음</option>');
productSelect.prop('disabled', true);
} else {
productSelect.append('<option value="">제품 선택</option>');
response.data.forEach(product => {
const stockInfo = product.stock_quantity > 0 ? `(재고: ${product.stock_quantity.toFixed(1)}g)` : '(재고 없음)';
const companyInfo = product.company_name ? `[${product.company_name}]` : '';
productSelect.append(`<option value="${product.herb_item_id}" ${product.stock_quantity === 0 ? 'disabled' : ''}>${product.herb_name} ${companyInfo} ${stockInfo}</option>`);
});
productSelect.prop('disabled', false);
}
}
}).fail(function() {
console.error(`Failed to load products for ingredient code: ${ingredientCode}`);
});
}
// 원산지별 재고 옵션 로드
function loadOriginOptions(herbId, requiredQty) {
$.get(`/api/herbs/${herbId}/available-lots`, function(response) {
if (response.success) {
const selectElement = $(`tr[data-herb-id="${herbId}"] .origin-select`);
selectElement.empty();
const origins = response.data.origins;
if (origins.length === 0) {
selectElement.append('<option value="">재고 없음</option>');
selectElement.prop('disabled', true);
$(`tr[data-herb-id="${herbId}"] .stock-status`)
.html('<span class="text-danger">재고 없음</span>');
} else {
selectElement.append('<option value="auto">자동 선택 (저렴한 것부터)</option>');
// 로트가 2개 이상인 경우 수동 배분 옵션 추가
const totalLots = origins.reduce((sum, o) => sum + o.lot_count, 0);
if (totalLots > 1) {
selectElement.append('<option value="manual">수동 배분 (로트별 지정)</option>');
}
origins.forEach(origin => {
const stockStatus = origin.total_quantity >= requiredQty ? '' : ' (재고 부족)';
const priceInfo = `${formatCurrency(origin.min_price)}/g`;
// 해당 원산지의 display_name 목록 생성
let displayNames = [];
if (origin.lots && origin.lots.length > 0) {
origin.lots.forEach(lot => {
if (lot.display_name) {
displayNames.push(lot.display_name);
}
});
}
// 고유한 display_name만 표시 (중복 제거)
const uniqueDisplayNames = [...new Set(displayNames)];
const displayNameText = uniqueDisplayNames.length > 0
? ` [${uniqueDisplayNames.join(', ')}]`
: '';
const option = `<option value="${origin.origin_country}"
data-price="${origin.min_price}"
data-available="${origin.total_quantity}"
${origin.total_quantity < requiredQty ? 'disabled' : ''}>
${origin.origin_country}${displayNameText} - ${priceInfo} (재고: ${origin.total_quantity.toFixed(1)}g)${stockStatus}
</option>`;
selectElement.append(option);
});
selectElement.prop('disabled', false);
// 재고 충분한 첫 번째 원산지 자동 선택 (원가 미리보기용)
const firstAvailable = origins.find(o => o.total_quantity >= requiredQty);
if (firstAvailable) {
selectElement.val(firstAvailable.origin_country);
} else if (origins.length > 0) {
selectElement.val(origins[0].origin_country);
}
// 원산지 선택 변경 이벤트 (수동 배분 모달 트리거)
selectElement.off('change').on('change', function() {
const selectedValue = $(this).val();
const row = $(this).closest('tr');
if (selectedValue === 'manual') {
// 현재 행의 실제 필요량 재계산
const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0;
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
const actualRequiredQty = gramsPerCheop * cheopTotal;
// 수동 배분 모달 열기 (재계산된 필요량 사용)
openLotAllocationModal(herbId, actualRequiredQty, row, response.data);
} else {
// 기존 자동/원산지 선택 - lot_assignments 제거
row.removeAttr('data-lot-assignments');
}
updateCostPreview();
});
// 재고 상태 업데이트
const totalAvailable = response.data.total_quantity;
const statusElement = $(`tr[data-herb-id="${herbId}"] .stock-status`);
const altCount = origins.length;
const altBadge = altCount > 1 ? ` <span class="badge bg-outline-info text-info border border-info" style="font-size:0.65rem; cursor:pointer" title="선택 가능한 원산지 ${altCount}종">${altCount}종</span>` : '';
if (totalAvailable >= requiredQty) {
statusElement.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
} else {
statusElement.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
}
}
// 원가 미리보기 갱신
updateCostPreview();
}
});
}
// 재고 원장 보기
let currentLedgerData = []; // 원본 데이터 저장
function viewStockLedger(herbId, herbName) {
const url = herbId ? `/api/stock-ledger?herb_id=${herbId}` : '/api/stock-ledger';
$.get(url, function(response) {
if (response.success) {
// 원본 데이터 저장
currentLedgerData = response.ledger;
// 헤더 업데이트
if (herbName) {
$('#stockLedgerModal .modal-title').html(`<i class="bi bi-journal-text"></i> ${herbName} 입출고 원장`);
} else {
$('#stockLedgerModal .modal-title').html(`<i class="bi bi-journal-text"></i> 전체 입출고 원장`);
}
// 필터 적용하여 표시
applyLedgerFilters();
// 약재 필터 옵션 업데이트
const herbFilter = $('#ledgerHerbFilter');
if (herbFilter.find('option').length <= 1) {
response.summary.forEach(herb => {
herbFilter.append(`<option value="${herb.herb_item_id}">${herb.herb_name}</option>`);
});
}
$('#stockLedgerModal').modal('show');
}
}).fail(function() {
alert('입출고 내역을 불러오는데 실패했습니다.');
});
}
// 필터 적용 함수
function applyLedgerFilters() {
const typeFilter = $('#ledgerTypeFilter').val();
const tbody = $('#stockLedgerList');
tbody.empty();
// 필터링된 데이터
let filteredData = currentLedgerData;
// 타입 필터 적용
if (typeFilter) {
filteredData = currentLedgerData.filter(entry => entry.event_type === typeFilter);
}
// 데이터 표시
filteredData.forEach(entry => {
let typeLabel = '';
let typeBadge = '';
switch(entry.event_type) {
case 'PURCHASE':
case 'RECEIPT':
typeLabel = '입고';
typeBadge = 'badge bg-success';
break;
case 'CONSUME':
typeLabel = '출고';
typeBadge = 'badge bg-danger';
break;
case 'ADJUST':
typeLabel = '보정';
typeBadge = 'badge bg-warning';
break;
case 'RETURN':
typeLabel = '반환';
typeBadge = 'badge bg-info';
break;
case 'DISCARD':
typeLabel = '폐기';
typeBadge = 'badge bg-dark';
break;
default:
typeLabel = entry.event_type;
typeBadge = 'badge bg-secondary';
}
const quantity = Math.abs(entry.quantity_delta);
const sign = entry.quantity_delta > 0 ? '+' : '-';
const quantityDisplay = entry.quantity_delta > 0
? `<span class="text-success">+${quantity.toFixed(1)}g</span>`
: `<span class="text-danger">-${quantity.toFixed(1)}g</span>`;
const referenceInfo = entry.patient_name
? `${entry.patient_name}`
: entry.supplier_name || '-';
tbody.append(`
<tr>
<td>${entry.event_time}</td>
<td><span class="${typeBadge}">${typeLabel}</span></td>
<td>${entry.herb_name}</td>
<td>${quantityDisplay}</td>
<td>${entry.unit_cost_per_g ? formatCurrency(entry.unit_cost_per_g) + '/g' : '-'}</td>
<td>${entry.origin_country || '-'}</td>
<td>${referenceInfo}</td>
<td>${entry.reference_no || '-'}</td>
</tr>
`);
});
// 데이터가 없는 경우
if (filteredData.length === 0) {
tbody.append(`
<tr>
<td colspan="8" class="text-center text-muted">데이터가 없습니다.</td>
</tr>
`);
}
}
// 입출고 원장 모달 버튼 이벤트
$('#showStockLedgerBtn').on('click', function() {
viewStockLedger(null, null);
});
// 필터 변경 이벤트
$('#ledgerHerbFilter').on('change', function() {
const herbId = $(this).val();
// 약재 필터 변경 시 데이터 재로드
if (herbId) {
const herbName = $('#ledgerHerbFilter option:selected').text();
viewStockLedger(herbId, herbName);
} else {
viewStockLedger(null, null);
}
});
// 타입 필터 변경 이벤트 (현재 데이터에서 필터링만)
$('#ledgerTypeFilter').on('change', function() {
applyLedgerFilters();
});
// ==================== 재고 보정 ====================
// 재고 보정 모달 열기
$('#showStockAdjustmentBtn').on('click', function() {
// 현재 날짜 설정
$('#adjustmentDate').val(new Date().toISOString().split('T')[0]);
$('#adjustmentItemsList').empty();
$('#stockAdjustmentForm')[0].reset();
$('#stockAdjustmentModal').modal('show');
});
// 재고 보정 내역 모달 열기
$('#showAdjustmentHistoryBtn').on('click', function() {
loadAdjustmentHistory();
});
// 재고 보정 내역 로드
function loadAdjustmentHistory() {
$.get('/api/stock-adjustments', function(response) {
if (response.success) {
const tbody = $('#adjustmentHistoryList');
tbody.empty();
if (response.data.length === 0) {
tbody.append(`
<tr>
<td colspan="7" class="text-center text-muted">보정 내역이 없습니다.</td>
</tr>
`);
} else {
response.data.forEach(adj => {
// 보정 유형 한글 변환
let typeLabel = '';
switch(adj.adjustment_type) {
case 'LOSS': typeLabel = '감모/손실'; break;
case 'FOUND': typeLabel = '발견'; break;
case 'RECOUNT': typeLabel = '재고조사'; break;
case 'DAMAGE': typeLabel = '파손'; break;
case 'EXPIRE': typeLabel = '유통기한 경과'; break;
default: typeLabel = adj.adjustment_type;
}
tbody.append(`
<tr>
<td>${adj.adjustment_date}</td>
<td><code>${adj.adjustment_no}</code></td>
<td><span class="badge bg-warning">${typeLabel}</span></td>
<td>${adj.detail_count || 0}개</td>
<td>${adj.created_by || '-'}</td>
<td>${adj.notes || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-info view-adjustment-detail"
data-id="${adj.adjustment_id}">
<i class="bi bi-eye"></i> 상세
</button>
</td>
</tr>
`);
});
// 상세보기 버튼 이벤트
$('.view-adjustment-detail').on('click', function() {
const adjustmentId = $(this).data('id');
viewAdjustmentDetail(adjustmentId);
});
}
$('#adjustmentHistoryModal').modal('show');
}
}).fail(function() {
alert('보정 내역을 불러오는데 실패했습니다.');
});
}
// 재고 보정 상세 조회
function viewAdjustmentDetail(adjustmentId) {
$.get(`/api/stock-adjustments/${adjustmentId}`, function(response) {
if (response.success) {
const data = response.data;
// 보정 정보 표시
$('#detailAdjustmentNo').text(data.adjustment_no);
$('#detailAdjustmentDate').text(data.adjustment_date);
// 보정 유형 한글 변환
let typeLabel = '';
switch(data.adjustment_type) {
case 'LOSS': typeLabel = '감모/손실'; break;
case 'FOUND': typeLabel = '발견'; break;
case 'RECOUNT': typeLabel = '재고조사'; break;
case 'DAMAGE': typeLabel = '파손'; break;
case 'EXPIRE': typeLabel = '유통기한 경과'; break;
default: typeLabel = data.adjustment_type;
}
$('#detailAdjustmentType').html(`<span class="badge bg-warning">${typeLabel}</span>`);
$('#detailAdjustmentCreatedBy').text(data.created_by || '-');
$('#detailAdjustmentNotes').text(data.notes || '-');
// 보정 상세 항목 표시
const itemsBody = $('#detailAdjustmentItems');
itemsBody.empty();
if (data.details && data.details.length > 0) {
data.details.forEach(item => {
const delta = item.quantity_delta;
let deltaHtml = '';
if (delta > 0) {
deltaHtml = `<span class="text-success">+${delta.toFixed(1)}g</span>`;
} else if (delta < 0) {
deltaHtml = `<span class="text-danger">${delta.toFixed(1)}g</span>`;
} else {
deltaHtml = '<span class="text-muted">0g</span>';
}
itemsBody.append(`
<tr>
<td>${item.herb_name}</td>
<td>${item.product_type === 'OTC' ? (item.standard_code || '-') : (item.insurance_code || '-')}</td>
<td>${item.origin_country || '-'}</td>
<td>#${item.lot_id}</td>
<td>${item.quantity_before.toFixed(1)}g</td>
<td>${item.quantity_after.toFixed(1)}g</td>
<td>${deltaHtml}</td>
<td>${item.reason || '-'}</td>
</tr>
`);
});
}
// 보정 상세 모달 표시
$('#adjustmentDetailModal').modal('show');
}
}).fail(function() {
alert('보정 상세 정보를 불러오는데 실패했습니다.');
});
}
// 보정 대상 약재 추가
let adjustmentItemCount = 0;
$('#addAdjustmentItemBtn').on('click', function() {
addAdjustmentItemRow();
});
function addAdjustmentItemRow() {
adjustmentItemCount++;
const rowId = `adj-item-${adjustmentItemCount}`;
const newRow = $(`
<tr data-row-id="${rowId}">
<td>
<select class="form-select form-select-sm adj-herb-select" required>
<option value="">약재 선택</option>
</select>
</td>
<td>
<select class="form-select form-select-sm adj-lot-select" disabled required>
<option value="">약재 먼저 선택</option>
</select>
</td>
<td class="before-qty text-end">-</td>
<td>
<input type="number" class="form-control form-control-sm after-qty-input"
min="0" step="0.1" placeholder="0.0" required>
</td>
<td class="delta-qty text-end">-</td>
<td>
<input type="text" class="form-control form-control-sm reason-input"
placeholder="사유">
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-adj-item">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
$('#adjustmentItemsList').append(newRow);
// 약재 목록 로드
loadHerbsForSelect(newRow.find('.adj-herb-select'));
// 약재 선택 이벤트
newRow.find('.adj-herb-select').on('change', function() {
const herbId = $(this).val();
const row = $(this).closest('tr');
if (herbId) {
loadLotsForAdjustment(herbId, row);
} else {
row.find('.adj-lot-select').empty().append('<option value="">약재 먼저 선택</option>').prop('disabled', true);
row.find('.before-qty').text('-');
row.find('.after-qty-input').val('');
row.find('.delta-qty').text('-');
}
});
// 로트 선택 이벤트
newRow.find('.adj-lot-select').on('change', function() {
const selectedOption = $(this).find('option:selected');
const row = $(this).closest('tr');
if (selectedOption.val()) {
const beforeQty = parseFloat(selectedOption.data('qty')) || 0;
row.find('.before-qty').text(beforeQty.toFixed(1) + 'g');
row.data('before-qty', beforeQty);
// 기존 변경후 값이 있으면 델타 재계산
const afterQty = parseFloat(row.find('.after-qty-input').val());
if (!isNaN(afterQty)) {
updateDelta(row, beforeQty, afterQty);
}
} else {
row.find('.before-qty').text('-');
row.find('.after-qty-input').val('');
row.find('.delta-qty').text('-');
}
});
// 변경후 수량 입력 이벤트
newRow.find('.after-qty-input').on('input', function() {
const row = $(this).closest('tr');
const beforeQty = row.data('before-qty') || 0;
const afterQty = parseFloat($(this).val()) || 0;
updateDelta(row, beforeQty, afterQty);
});
// 삭제 버튼
newRow.find('.remove-adj-item').on('click', function() {
$(this).closest('tr').remove();
});
}
// 약재별 로트 목록 로드
function loadLotsForAdjustment(herbId, row) {
$.get(`/api/inventory/detail/${herbId}`, function(response) {
if (response.success) {
const lotSelect = row.find('.adj-lot-select');
lotSelect.empty();
lotSelect.append('<option value="">로트/원산지 선택</option>');
const data = response.data;
// 원산지별로 로트 표시
data.origins.forEach(origin => {
const optgroup = $(`<optgroup label="${origin.origin_country}">`);
origin.lots.forEach(lot => {
optgroup.append(`
<option value="${lot.lot_id}"
data-qty="${lot.quantity_onhand}"
data-origin="${origin.origin_country}">
로트#${lot.lot_id} - ${lot.quantity_onhand.toFixed(1)}g (${lot.received_date})
</option>
`);
});
lotSelect.append(optgroup);
});
lotSelect.prop('disabled', false);
}
}).fail(function() {
alert('재고 정보를 불러오는데 실패했습니다.');
});
}
// 델타 계산 및 표시
function updateDelta(row, beforeQty, afterQty) {
const delta = afterQty - beforeQty;
const deltaElement = row.find('.delta-qty');
if (delta > 0) {
deltaElement.html(`<span class="text-success">+${delta.toFixed(1)}g</span>`);
} else if (delta < 0) {
deltaElement.html(`<span class="text-danger">${delta.toFixed(1)}g</span>`);
} else {
deltaElement.html('<span class="text-muted">0g</span>');
}
row.data('delta', delta);
}
// 재고 보정 저장 버튼
$('#saveAdjustmentBtn').on('click', function() {
saveStockAdjustment();
});
// 재고 보정 저장
$('#stockAdjustmentForm').on('submit', function(e) {
e.preventDefault();
saveStockAdjustment();
});
function saveStockAdjustment() {
const items = [];
let hasError = false;
$('#adjustmentItemsList tr').each(function() {
const herbId = $(this).find('.adj-herb-select').val();
const lotId = $(this).find('.adj-lot-select').val();
const beforeQty = $(this).data('before-qty');
const afterQty = parseFloat($(this).find('.after-qty-input').val());
const delta = $(this).data('delta');
const reason = $(this).find('.reason-input').val();
if (!herbId || !lotId) {
hasError = true;
return false;
}
items.push({
herb_item_id: parseInt(herbId),
lot_id: parseInt(lotId),
quantity_before: beforeQty,
quantity_after: afterQty,
quantity_delta: delta,
reason: reason
});
});
if (hasError) {
alert('모든 항목의 약재와 로트를 선택해주세요.');
return;
}
if (items.length === 0) {
alert('보정할 항목을 추가해주세요.');
return;
}
const adjustmentData = {
adjustment_date: $('#adjustmentDate').val(),
adjustment_type: $('#adjustmentType').val(),
created_by: $('#adjustmentCreatedBy').val() || 'SYSTEM',
notes: $('#adjustmentNotes').val(),
details: items // API expects 'details', not 'items'
};
$.ajax({
url: '/api/stock-adjustments',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(adjustmentData),
success: function(response) {
if (response.success) {
alert(`재고 보정이 완료되었습니다.\n보정번호: ${response.adjustment_no}\n항목 수: ${items.length}`);
$('#stockAdjustmentModal').modal('hide');
// 재고 목록 새로고침
loadInventory();
}
},
error: function(xhr) {
alert('오류: ' + (xhr.responseJSON?.error || '재고 보정 실패'));
}
});
}
function formatCurrency(amount) {
if (amount === null || amount === undefined) return '0원';
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(amount);
}
// === 재고 자산 계산 설정 기능 ===
// 재고 현황 로드 (모드 포함)
function loadInventorySummary() {
const mode = localStorage.getItem('inventoryMode') || 'all';
$.get(`/api/inventory/summary?mode=${mode}`, function(response) {
if (response.success) {
$('#totalHerbs').text(response.data.length);
$('#inventoryValue').text(formatCurrency(response.summary.total_value));
// 모드 표시 업데이트
if (response.summary.calculation_mode) {
$('#inventoryMode').text(response.summary.calculation_mode.mode_label);
// 설정 모달이 열려있으면 정보 표시
if ($('#inventorySettingsModal').hasClass('show')) {
updateModeInfo(response.summary.calculation_mode);
}
}
}
});
}
// 재고 계산 설정 저장
window.saveInventorySettings = function() {
const selectedMode = $('input[name="inventoryMode"]:checked').val();
// localStorage에 저장
localStorage.setItem('inventoryMode', selectedMode);
inventoryCalculationMode = selectedMode;
// 재고 현황 다시 로드
loadInventorySummary();
// 모달 닫기
$('#inventorySettingsModal').modal('hide');
// 성공 메시지
showToast('success', '재고 계산 설정이 변경되었습니다.');
}
// 모드 정보 업데이트
function updateModeInfo(modeInfo) {
let infoHtml = '';
if (modeInfo.mode === 'all' && modeInfo.no_receipt_lots !== undefined) {
if (modeInfo.no_receipt_lots > 0) {
infoHtml = `
<p class="mb-1">• 입고장 없는 LOT: <strong>${modeInfo.no_receipt_lots}개</strong></p>
<p class="mb-0">• 해당 재고 가치: <strong>${formatCurrency(modeInfo.no_receipt_value)}</strong></p>
`;
} else {
infoHtml = '<p class="mb-0">• 모든 LOT이 입고장과 연결되어 있습니다.</p>';
}
} else if (modeInfo.mode === 'receipt_only') {
infoHtml = '<p class="mb-0">• 입고장과 연결된 LOT만 계산합니다.</p>';
} else if (modeInfo.mode === 'verified') {
infoHtml = '<p class="mb-0">• 검증 확인된 LOT만 계산합니다.</p>';
}
if (infoHtml) {
$('#modeInfoContent').html(infoHtml);
$('#modeInfo').show();
} else {
$('#modeInfo').hide();
}
}
// 설정 모달이 열릴 때 현재 모드 설정
$('#inventorySettingsModal').on('show.bs.modal', function() {
const currentMode = localStorage.getItem('inventoryMode') || 'all';
$(`input[name="inventoryMode"][value="${currentMode}"]`).prop('checked', true);
// 현재 모드 정보 로드
$.get(`/api/inventory/summary?mode=${currentMode}`, function(response) {
if (response.success && response.summary.calculation_mode) {
updateModeInfo(response.summary.calculation_mode);
}
});
});
// 모드 선택 시 즉시 정보 업데이트
$('input[name="inventoryMode"]').on('change', function() {
const selectedMode = $(this).val();
// 선택한 모드의 정보를 미리보기로 로드
$.get(`/api/inventory/summary?mode=${selectedMode}`, function(response) {
if (response.success && response.summary.calculation_mode) {
updateModeInfo(response.summary.calculation_mode);
}
});
});
// Toast 메시지 표시 함수 (없으면 추가)
function showToast(type, message) {
const toastHtml = `
<div class="toast align-items-center text-white bg-${type === 'success' ? 'success' : 'danger'} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
// Toast 컨테이너가 없으면 생성
if (!$('#toastContainer').length) {
$('body').append('<div id="toastContainer" class="position-fixed bottom-0 end-0 p-3" style="z-index: 11"></div>');
}
const $toast = $(toastHtml);
$('#toastContainer').append($toast);
const toast = new bootstrap.Toast($toast[0]);
toast.show();
// 5초 후 자동 제거
setTimeout(() => {
$toast.remove();
}, 5000);
}
// ==================== 주성분코드 기반 약재 관리 ====================
let allHerbMasters = []; // 전체 약재 데이터 저장
let currentFilter = 'all'; // 현재 필터 상태
// 약재 마스터 목록 로드
function loadHerbMasters() {
$.get('/api/herbs/masters', function(response) {
if (response.success) {
allHerbMasters = response.data;
// 통계 정보 표시
const summary = response.summary;
$('#herbMasterSummary').html(`
<div class="row align-items-center">
<div class="col-md-9">
<h6 class="mb-2">📊 급여 약재 현황</h6>
<div class="d-flex align-items-center">
<div class="me-4">
<strong>전체:</strong> ${summary.total_herbs}개 주성분
</div>
<div class="me-4">
<strong>재고 있음:</strong> <span class="text-success">${summary.herbs_with_stock}개</span>
</div>
<div class="me-4">
<strong>재고 없음:</strong> <span class="text-secondary">${summary.herbs_without_stock}개</span>
</div>
<div>
<strong>보유율:</strong> <span class="badge bg-primary fs-6">${summary.coverage_rate}%</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="progress" style="height: 25px;">
<div class="progress-bar bg-success" role="progressbar"
style="width: ${summary.coverage_rate}%"
aria-valuenow="${summary.coverage_rate}"
aria-valuemin="0" aria-valuemax="100">
${summary.herbs_with_stock} / ${summary.total_herbs}
</div>
</div>
</div>
</div>
`);
// 목록 표시
displayHerbMasters(allHerbMasters);
}
});
}
// 약재 목록 표시
function displayHerbMasters(herbs) {
const tbody = $('#herbMastersList');
tbody.empty();
// 필터링
let filteredHerbs = herbs;
if (currentFilter === 'stock') {
filteredHerbs = herbs.filter(h => h.has_stock);
} else if (currentFilter === 'no-stock') {
filteredHerbs = herbs.filter(h => !h.has_stock);
}
// 검색 필터
const searchText = $('#herbSearch').val().toLowerCase();
if (searchText) {
filteredHerbs = filteredHerbs.filter(h =>
h.herb_name.toLowerCase().includes(searchText) ||
h.ingredient_code.toLowerCase().includes(searchText)
);
}
// 효능 필터
const efficacyFilter = $('#efficacyFilter').val();
if (efficacyFilter) {
filteredHerbs = filteredHerbs.filter(h =>
h.efficacy_tags && h.efficacy_tags.includes(efficacyFilter)
);
}
// 표시
filteredHerbs.forEach(herb => {
// 효능 태그 표시
let efficacyTags = '';
if (herb.efficacy_tags && herb.efficacy_tags.length > 0) {
efficacyTags = herb.efficacy_tags.map(tag =>
`<span class="badge bg-success ms-1">${tag}</span>`
).join('');
}
// 상태 표시
const statusBadge = herb.has_stock
? '<span class="badge bg-success">재고 있음</span>'
: '<span class="badge bg-secondary">재고 없음</span>';
// 재고량 표시
const stockDisplay = herb.stock_quantity > 0
? `${herb.stock_quantity.toFixed(1)}g`
: '-';
// 평균단가 표시
const priceDisplay = herb.avg_price > 0
? formatCurrency(herb.avg_price)
: '-';
tbody.append(`
<tr class="${herb.has_stock ? '' : 'table-secondary'}">
<td><code>${herb.ingredient_code}</code></td>
<td><strong>${herb.herb_name}</strong></td>
<td>${efficacyTags}</td>
<td>${stockDisplay}</td>
<td>${priceDisplay}</td>
<td>${herb.product_count || 0}개</td>
<td>${statusBadge}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewHerbDetail('${herb.ingredient_code}')">
<i class="bi bi-eye"></i> 상세
</button>
</td>
</tr>
`);
});
if (filteredHerbs.length === 0) {
tbody.append('<tr><td colspan="8" class="text-center">표시할 약재가 없습니다.</td></tr>');
}
}
// 약재 상세 보기
function viewHerbDetail(ingredientCode) {
// TODO: 약재 상세 모달 구현
console.log('View detail for:', ingredientCode);
}
// 필터 버튼 이벤트
$('#herbs .btn-group button[data-filter]').on('click', function() {
$('#herbs .btn-group button').removeClass('active');
$(this).addClass('active');
currentFilter = $(this).data('filter');
displayHerbMasters(allHerbMasters);
});
// 검색 이벤트
$('#herbSearch').on('keyup', function() {
displayHerbMasters(allHerbMasters);
});
// 효능 필터 이벤트
$('#efficacyFilter').on('change', function() {
displayHerbMasters(allHerbMasters);
});
// 약재 관리 페이지가 활성화되면 데이터 로드
$('.nav-link[data-page="herbs"]').on('click', function() {
setTimeout(() => loadHerbMasters(), 100);
});
// ==================== 로트 배분 모달 관련 함수들 ====================
// 로트 배분 모달 열기
window.openLotAllocationModal = function(herbId, requiredQty, row, data) {
// 디버깅: 전달받은 필요량 확인
console.log('로트 배분 모달 열기:', {
herbId: herbId,
requiredQty: requiredQty,
herbName: data.herb_name,
gramsPerCheop: row.find('.grams-per-cheop').val(),
cheopTotal: $('#cheopTotal').val()
});
currentLotAllocation = {
herbId: herbId,
requiredQty: requiredQty,
row: row,
data: data
};
// 모달 초기화
$('#lotAllocationHerbName').text(data.herb_name);
$('#lotAllocationRequired').text(requiredQty.toFixed(1));
$('#lotAllocationError').addClass('d-none');
// 로트 목록 생성
const tbody = $('#lotAllocationList');
tbody.empty();
// 모든 로트를 하나의 목록으로 표시
let allLots = [];
data.origins.forEach(origin => {
origin.lots.forEach(lot => {
allLots.push({
...lot,
origin: origin.origin_country
});
});
});
// 단가 순으로 정렬
allLots.sort((a, b) => a.unit_price_per_g - b.unit_price_per_g);
allLots.forEach(lot => {
tbody.append(`
<tr data-lot-id="${lot.lot_id}">
<td>#${lot.lot_id}</td>
<td>${lot.origin || '미지정'}</td>
<td>${lot.quantity_onhand.toFixed(1)}g</td>
<td>${formatCurrency(lot.unit_price_per_g)}/g</td>
<td>
<input type="number" class="form-control form-control-sm lot-allocation-input"
min="0" max="${lot.quantity_onhand}" step="0.1" value="0"
data-max="${lot.quantity_onhand}" data-price="${lot.unit_price_per_g}">
</td>
<td class="lot-subtotal">0원</td>
</tr>
`);
});
// 입력 이벤트
$('.lot-allocation-input').on('input', function() {
updateLotAllocationSummary();
});
$('#lotAllocationModal').modal('show');
};
// 로트 배분 합계 업데이트
function updateLotAllocationSummary() {
let totalQty = 0;
let totalCost = 0;
$('.lot-allocation-input').each(function() {
const qty = parseFloat($(this).val()) || 0;
const price = parseFloat($(this).data('price')) || 0;
const subtotal = qty * price;
totalQty += qty;
totalCost += subtotal;
// 소계 표시
$(this).closest('tr').find('.lot-subtotal').text(formatCurrency(subtotal) + '원');
});
$('#lotAllocationTotal').text(totalQty.toFixed(1));
$('#lotAllocationSumQty').text(totalQty.toFixed(1) + 'g');
$('#lotAllocationSumCost').text(formatCurrency(totalCost) + '원');
// 검증
const required = currentLotAllocation.requiredQty;
const diff = Math.abs(totalQty - required);
if (diff > 0.01) {
$('#lotAllocationError')
.removeClass('d-none')
.text(`필요량(${required.toFixed(1)}g)과 배분 합계(${totalQty.toFixed(1)}g)가 일치하지 않습니다.`);
$('#lotAllocationConfirmBtn').prop('disabled', true);
} else {
$('#lotAllocationError').addClass('d-none');
$('#lotAllocationConfirmBtn').prop('disabled', false);
}
}
// 자동 배분
$('#lotAllocationAutoBtn').on('click', function() {
let remaining = currentLotAllocation.requiredQty;
$('.lot-allocation-input').each(function() {
const maxAvailable = parseFloat($(this).data('max'));
const allocate = Math.min(remaining, maxAvailable);
$(this).val(allocate.toFixed(1));
remaining -= allocate;
if (remaining <= 0) return false; // break
});
updateLotAllocationSummary();
});
// 로트 배분 확인
$('#lotAllocationConfirmBtn').on('click', function() {
const allocations = [];
$('.lot-allocation-input').each(function() {
const qty = parseFloat($(this).val()) || 0;
if (qty > 0) {
const lotId = $(this).closest('tr').data('lot-id');
allocations.push({
lot_id: lotId,
quantity: qty
});
}
});
// 현재 행에 로트 배분 정보 저장
currentLotAllocation.row.attr('data-lot-assignments', JSON.stringify(allocations));
// 상태 표시 업데이트
const statusElement = currentLotAllocation.row.find('.stock-status');
statusElement.html(`<span class="text-info">수동 배분 (${allocations.length}개 로트)</span>`);
$('#lotAllocationModal').modal('hide');
});
// 조제 실행 시 lot_assignments 포함
window.getIngredientDataForCompound = function() {
const ingredients = [];
$('#compoundIngredients tr').each(function() {
const herbId = $(this).attr('data-herb-id');
if (herbId) {
const ingredient = {
herb_item_id: parseInt(herbId),
grams_per_cheop: parseFloat($(this).find('.grams-per-cheop').val()) || 0,
total_grams: parseFloat($(this).find('.total-grams').text()) || 0,
origin: $(this).find('.origin-select').val() || 'auto'
};
// 수동 로트 배분이 있는 경우
const lotAssignments = $(this).attr('data-lot-assignments');
if (lotAssignments) {
ingredient.lot_assignments = JSON.parse(lotAssignments);
ingredient.origin = 'manual'; // origin을 manual로 설정
}
ingredients.push(ingredient);
}
});
return ingredients;
};
// ==================== 약재 정보 시스템 ====================
// 약재 정보 페이지 로드
window.loadHerbInfo = function loadHerbInfo() {
loadAllHerbsInfo();
loadEfficacyTags();
// 뷰 전환 버튼
$('#herb-info button[data-view]').on('click', function() {
$('#herb-info button[data-view]').removeClass('active');
$(this).addClass('active');
const view = $(this).data('view');
if (view === 'search') {
$('#herb-search-section').show();
$('#herb-efficacy-section').hide();
loadAllHerbsInfo();
} else if (view === 'efficacy') {
$('#herb-search-section').hide();
$('#herb-efficacy-section').show();
loadEfficacyTagButtons();
} else if (view === 'category') {
$('#herb-search-section').show();
$('#herb-efficacy-section').hide();
loadHerbsByCategory();
}
});
// 검색 버튼
$('#herbSearchBtn').off('click').on('click', function() {
const searchTerm = $('#herbSearchInput').val();
searchHerbs(searchTerm);
});
// 엔터 키로 검색
$('#herbSearchInput').off('keypress').on('keypress', function(e) {
if (e.which === 13) {
searchHerbs($(this).val());
}
});
// 필터 변경
$('#herbInfoEfficacyFilter, #herbInfoPropertyFilter').off('change').on('change', function() {
filterHerbs();
});
}
// 모든 약재 정보 로드
window.loadAllHerbsInfo = function loadAllHerbsInfo() {
$.get('/api/herbs/masters', function(response) {
if (response.success) {
displayHerbCards(response.data);
}
});
}
// 약재 카드 표시
window.displayHerbCards = function displayHerbCards(herbs) {
const grid = $('#herbInfoGrid');
grid.empty();
if (herbs.length === 0) {
grid.html('<div class="col-12 text-center text-muted py-5">검색 결과가 없습니다.</div>');
return;
}
herbs.forEach(herb => {
// 재고 상태에 따른 배지 색상
const stockBadge = herb.has_stock ?
'<span class="badge bg-success">재고있음</span>' :
'<span class="badge bg-secondary">재고없음</span>';
// 효능 태그 HTML
let tagsHtml = '';
if (herb.efficacy_tags && herb.efficacy_tags.length > 0) {
tagsHtml = herb.efficacy_tags.slice(0, 3).map(tag =>
`<span class="badge bg-info me-1">${tag}</span>`
).join('');
if (herb.efficacy_tags.length > 3) {
tagsHtml += `<span class="badge bg-secondary">+${herb.efficacy_tags.length - 3}</span>`;
}
}
const card = `
<div class="col-md-4 col-lg-3">
<div class="card h-100 herb-info-card" data-herb-id="${herb.herb_id}"
data-ingredient-code="${herb.ingredient_code}"
style="cursor: pointer;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="card-title mb-0">
${herb.herb_name}
${herb.herb_name_hanja ? `<small class="text-muted">(${herb.herb_name_hanja})</small>` : ''}
</h6>
${stockBadge}
</div>
<p class="card-text small text-muted mb-2">
${herb.ingredient_code}
</p>
<div class="mb-2">
${tagsHtml || '<span class="text-muted small">태그 없음</span>'}
</div>
${herb.main_effects ?
`<p class="card-text small">${herb.main_effects.substring(0, 50)}...</p>` :
'<p class="card-text small text-muted">효능 정보 없음</p>'
}
</div>
</div>
</div>
`;
grid.append(card);
});
// 카드 클릭 이벤트
$('.herb-info-card').off('click').on('click', function() {
const ingredientCode = $(this).data('ingredient-code');
showHerbDetail(ingredientCode);
});
}
// 약재 상세 정보 표시
function showHerbDetail(ingredientCode) {
// herb_master_extended에서 herb_id 찾기
$.get('/api/herbs/masters', function(response) {
if (response.success) {
const herb = response.data.find(h => h.ingredient_code === ingredientCode);
if (herb && herb.herb_id) {
// 확장 정보 조회
$.get(`/api/herbs/${herb.herb_id}/extended`, function(detailResponse) {
displayHerbDetailModal(detailResponse);
}).fail(function() {
// 확장 정보가 없으면 기본 정보만 표시
displayHerbDetailModal(herb);
});
}
}
});
}
// 상세 정보 모달 표시
function displayHerbDetailModal(herb) {
$('#herbDetailName').text(herb.name_korean || herb.herb_name || '-');
$('#herbDetailHanja').text(herb.name_hanja || herb.herb_name_hanja || '');
// 기본 정보
$('#detailIngredientCode').text(herb.ingredient_code || '-');
$('#detailLatinName').text(herb.name_latin || herb.herb_name_latin || '-');
$('#detailMedicinalPart').text(herb.medicinal_part || '-');
$('#detailOriginPlant').text(herb.origin_plant || '-');
// 성미귀경
$('#detailProperty').text(herb.property || '-');
$('#detailTaste').text(herb.taste || '-');
$('#detailMeridian').text(herb.meridian_tropism || '-');
// 효능효과
$('#detailMainEffects').text(herb.main_effects || '-');
$('#detailIndications').text(herb.indications || '-');
// 효능 태그
if (herb.efficacy_tags && herb.efficacy_tags.length > 0) {
const tagsHtml = herb.efficacy_tags.map(tag => {
const strength = tag.strength || 3;
const sizeClass = strength >= 4 ? 'fs-5' : 'fs-6';
return `<span class="badge bg-primary ${sizeClass} me-2">${tag.name || tag}</span>`;
}).join('');
$('#detailEfficacyTags').html(tagsHtml);
} else {
$('#detailEfficacyTags').html('<span class="text-muted">태그 없음</span>');
}
// 용법용량
$('#detailDosageRange').text(herb.dosage_range || '-');
$('#detailDosageMax').text(herb.dosage_max || '-');
$('#detailPreparation').text(herb.preparation_method || '-');
// 안전성
$('#detailContraindications').text(herb.contraindications || '-');
$('#detailPrecautions').text(herb.precautions || '-');
// 성분정보
$('#detailActiveCompounds').text(herb.active_compounds || '-');
// 임상응용
$('#detailPharmacological').text(herb.pharmacological_effects || '-');
$('#detailClinical').text(herb.clinical_applications || '-');
// 정보 수정 버튼
$('#editHerbInfoBtn').off('click').on('click', function() {
editHerbInfo(herb.herb_id || herb.ingredient_code);
});
$('#herbDetailModal').modal('show');
}
// 약재 검색
function searchHerbs(searchTerm) {
if (!searchTerm) {
loadAllHerbsInfo();
return;
}
$.get('/api/herbs/masters', function(response) {
if (response.success) {
const filtered = response.data.filter(herb => {
const term = searchTerm.toLowerCase();
return (herb.herb_name && herb.herb_name.toLowerCase().includes(term)) ||
(herb.herb_name_hanja && herb.herb_name_hanja.includes(term)) ||
(herb.ingredient_code && herb.ingredient_code.toLowerCase().includes(term)) ||
(herb.main_effects && herb.main_effects.toLowerCase().includes(term)) ||
(herb.efficacy_tags && herb.efficacy_tags.some(tag => tag.toLowerCase().includes(term)));
});
displayHerbCards(filtered);
}
});
}
// 필터 적용
function filterHerbs() {
const efficacyFilter = $('#herbInfoEfficacyFilter').val();
const propertyFilter = $('#herbInfoPropertyFilter').val();
$.get('/api/herbs/masters', function(response) {
if (response.success) {
let filtered = response.data;
if (efficacyFilter) {
filtered = filtered.filter(herb =>
herb.efficacy_tags && herb.efficacy_tags.includes(efficacyFilter)
);
}
if (propertyFilter) {
filtered = filtered.filter(herb =>
herb.property === propertyFilter
);
}
displayHerbCards(filtered);
}
});
}
// 효능 태그 로드
function loadEfficacyTags() {
$.get('/api/efficacy-tags', function(tags) {
const select = $('#herbInfoEfficacyFilter');
select.empty().append('<option value="">모든 효능</option>');
tags.forEach(tag => {
select.append(`<option value="${tag.name}">${tag.name} - ${tag.description}</option>`);
});
});
}
// 효능 태그 버튼 표시
function loadEfficacyTagButtons() {
$.get('/api/efficacy-tags', function(tags) {
const container = $('#efficacyTagsContainer');
container.empty();
// 카테고리별로 그룹화
const grouped = {};
tags.forEach(tag => {
if (!grouped[tag.category]) {
grouped[tag.category] = [];
}
grouped[tag.category].push(tag);
});
// 카테고리별로 표시
Object.keys(grouped).forEach(category => {
const categoryHtml = `
<div class="col-12">
<h6 class="text-muted mb-2">${category}</h6>
<div class="btn-group flex-wrap mb-3" role="group">
${grouped[category].map(tag => `
<button type="button" class="btn btn-outline-primary efficacy-tag-btn m-1"
data-tag="${tag.name}">
${tag.name}
</button>
`).join('')}
</div>
</div>
`;
container.append(categoryHtml);
});
// 태그 버튼 클릭 이벤트
$('.efficacy-tag-btn').on('click', function() {
$(this).toggleClass('active');
const selectedTags = $('.efficacy-tag-btn.active').map(function() {
return $(this).data('tag');
}).get();
if (selectedTags.length > 0) {
searchByEfficacyTags(selectedTags);
} else {
loadAllHerbsInfo();
}
});
});
}
// 효능 태그로 검색
function searchByEfficacyTags(tags) {
const queryString = tags.map(tag => `tags=${encodeURIComponent(tag)}`).join('&');
$.get(`/api/herbs/search-by-efficacy?${queryString}`, function(herbs) {
displayHerbCards(herbs);
});
}
// 약재 정보 수정 (추후 구현)
function editHerbInfo(herbId) {
// herbId는 향후 수정 기능 구현시 사용 예정
console.log('Edit herb info for ID:', herbId);
alert('약재 정보 수정 기능은 준비 중입니다.');
// TODO: 정보 수정 폼 구현
}
// ==================== 판매 관리 기능 ====================
// 판매 모달 열기
function openSalesModal(compoundId, formulaName, patientName, patientId, costTotal, priceTotal) {
$('#salesCompoundId').val(compoundId);
$('#salesFormulaName').val(formulaName);
$('#salesPatientName').val(patientName);
$('#salesCostTotal').val(costTotal);
// 환자 마일리지 조회 (patient_id로 직접 조회)
loadPatientMileage(patientId);
// 기본 가격 계산 (원가 + 조제료)
const dispensingFee = parseFloat($('#salesDispensingFee').val()) || 20000;
const basePrice = costTotal + dispensingFee;
$('#salesBasePrice').val(basePrice);
// 판매가가 없으면 기본가격으로 설정
if (!priceTotal || priceTotal === 0) {
priceTotal = basePrice;
}
// 초기화
$('#salesDiscountValue').val(0);
$('#salesPriceAfterDiscount').val(priceTotal);
$('#salesMileageUse').val(0);
$('#salesPaymentCash').val(0);
$('#salesPaymentCard').val(0);
$('#salesPaymentTransfer').val(0);
updatePaymentSummary();
// 현재 시간 설정
const now = new Date();
const localDateTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
$('#salesPaymentDate').val(localDateTime);
// 내일 날짜 설정 (배송예정일)
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
$('#salesDeliveryDate').val(tomorrow.toISOString().slice(0, 10));
$('#salesModal').modal('show');
// 가격 자동 계산 이벤트
calculateSalesPrice();
}
// 환자 마일리지 조회
function loadPatientMileage(patientId) {
if (patientId) {
// patient_id로 직접 조회
$.get(`/api/patients/${patientId}`, function(response) {
if (response.success && response.data) {
const patient = response.data;
const mileage = patient.mileage_balance || 0;
$('#patientMileageBalance').text(mileage.toLocaleString());
$('#salesMileageUse').attr('max', mileage);
} else {
$('#patientMileageBalance').text('0');
$('#salesMileageUse').attr('max', 0);
}
}).fail(function() {
console.error('Failed to load patient mileage');
$('#patientMileageBalance').text('0');
$('#salesMileageUse').attr('max', 0);
});
} else {
// 직접조제인 경우
$('#patientMileageBalance').text('0');
$('#salesMileageUse').attr('max', 0);
}
}
// 가격 자동 계산
function calculateSalesPrice() {
const costTotal = parseFloat($('#salesCostTotal').val()) || 0;
const dispensingFee = parseFloat($('#salesDispensingFee').val()) || 0;
const basePrice = costTotal + dispensingFee;
$('#salesBasePrice').val(basePrice);
const discountType = $('input[name="discountType"]:checked').val();
const discountValue = parseFloat($('#salesDiscountValue').val()) || 0;
let discountAmount = 0;
if (discountType === 'rate') {
discountAmount = basePrice * (discountValue / 100);
} else {
discountAmount = discountValue;
}
const priceAfterDiscount = Math.max(0, basePrice - discountAmount);
$('#salesPriceAfterDiscount').val(Math.round(priceAfterDiscount));
updatePaymentSummary();
}
// 결제 요약 업데이트
function updatePaymentSummary() {
const priceAfterDiscount = parseFloat($('#salesPriceAfterDiscount').val()) || 0;
const mileageUse = parseFloat($('#salesMileageUse').val()) || 0;
const cashPayment = parseFloat($('#salesPaymentCash').val()) || 0;
const cardPayment = parseFloat($('#salesPaymentCard').val()) || 0;
const transferPayment = parseFloat($('#salesPaymentTransfer').val()) || 0;
const totalPayment = mileageUse + cashPayment + cardPayment + transferPayment;
const needToPay = Math.max(0, priceAfterDiscount - totalPayment);
$('#salesNeedToPay').text(needToPay.toLocaleString());
$('#salesTotalPayment').text(totalPayment.toLocaleString());
// 결제 방법 자동 설정
if (totalPayment > 0) {
let methodCount = 0;
if (mileageUse > 0) methodCount++;
if (cashPayment > 0) methodCount++;
if (cardPayment > 0) methodCount++;
if (transferPayment > 0) methodCount++;
if (methodCount > 1) {
$('#salesPaymentMethod').val('COMPLEX');
} else if (cashPayment > 0) {
$('#salesPaymentMethod').val('CASH');
} else if (cardPayment > 0) {
$('#salesPaymentMethod').val('CARD');
} else if (transferPayment > 0) {
$('#salesPaymentMethod').val('TRANSFER');
}
}
}
// 할인 방식 변경 이벤트
$('input[name="discountType"]').on('change', function() {
const discountType = $(this).val();
if (discountType === 'rate') {
$('#discountUnit').text('%');
$('#salesDiscountValue').attr('max', 100);
} else {
$('#discountUnit').text('원');
$('#salesDiscountValue').removeAttr('max');
}
calculateSalesPrice();
});
// 가격 계산 이벤트 핸들러
$('#salesDispensingFee, #salesDiscountValue').on('input', calculateSalesPrice);
$('#salesMileageUse, #salesPaymentCash, #salesPaymentCard, #salesPaymentTransfer').on('input', updatePaymentSummary);
// 마일리지 전액 사용 버튼
$('#applyMaxMileage').on('click', function() {
const maxMileage = parseFloat($('#salesMileageUse').attr('max')) || 0;
const priceAfterDiscount = parseFloat($('#salesPriceAfterDiscount').val()) || 0;
const useAmount = Math.min(maxMileage, priceAfterDiscount);
$('#salesMileageUse').val(useAmount);
updatePaymentSummary();
});
// 판매 저장
$('#saveSalesBtn').on('click', function() {
const compoundId = $('#salesCompoundId').val();
const priceAfterDiscount = parseFloat($('#salesPriceAfterDiscount').val()) || 0;
const discountType = $('input[name="discountType"]:checked').val();
const discountValue = parseFloat($('#salesDiscountValue').val()) || 0;
const discountReason = $('#salesDiscountReason').val();
// 복합 결제 정보
const mileageUse = parseFloat($('#salesMileageUse').val()) || 0;
const cashPayment = parseFloat($('#salesPaymentCash').val()) || 0;
const cardPayment = parseFloat($('#salesPaymentCard').val()) || 0;
const transferPayment = parseFloat($('#salesPaymentTransfer').val()) || 0;
const totalPayment = mileageUse + cashPayment + cardPayment + transferPayment;
// 결제 금액 검증
if (Math.abs(totalPayment - priceAfterDiscount) > 1) {
alert(`결제 금액이 일치하지 않습니다.\n필요금액: ${priceAfterDiscount.toLocaleString()}\n결제금액: ${totalPayment.toLocaleString()}`);
return;
}
const paymentMethod = $('#salesPaymentMethod').val();
const paymentDate = $('#salesPaymentDate').val();
const deliveryMethod = $('#salesDeliveryMethod').val();
const deliveryDate = $('#salesDeliveryDate').val();
if (!paymentMethod) {
alert('결제방법을 선택해주세요.');
return;
}
// 할인액 계산
const basePrice = parseFloat($('#salesBasePrice').val()) || 0;
let discountAmount = 0;
if (discountType === 'rate') {
discountAmount = basePrice * (discountValue / 100);
} else {
discountAmount = discountValue;
}
// 1. 가격 및 결제 정보 업데이트
$.ajax({
url: `/api/compounds/${compoundId}/price`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
sell_price_total: priceAfterDiscount,
discount_rate: discountType === 'rate' ? discountValue : 0,
discount_amount: Math.round(discountAmount),
discount_reason: discountReason,
mileage_used: mileageUse,
payment_cash: cashPayment,
payment_card: cardPayment,
payment_transfer: transferPayment
}),
success: function() {
// 2. 상태를 PAID로 변경
$.ajax({
url: `/api/compounds/${compoundId}/status`,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
status: 'PAID',
payment_method: paymentMethod,
payment_date: paymentDate,
actual_payment_amount: priceAfterDiscount,
delivery_method: deliveryMethod,
delivery_date: deliveryDate,
mileage_used: mileageUse,
changed_by: 'user'
}),
success: function() {
alert('판매 처리가 완료되었습니다.');
$('#salesModal').modal('hide');
loadCompounds(); // 목록 새로고침
},
error: function(xhr) {
alert('상태 업데이트 실패: ' + (xhr.responseJSON?.error || '알 수 없는 오류'));
}
});
},
error: function(xhr) {
alert('가격 업데이트 실패: ' + (xhr.responseJSON?.error || '알 수 없는 오류'));
}
});
});
// 배송 처리
function processDelivery(compoundId) {
if (!confirm('배송 처리를 하시겠습니까?')) {
return;
}
$.ajax({
url: `/api/compounds/${compoundId}/status`,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
status: 'DELIVERED',
delivery_date: new Date().toISOString(),
changed_by: 'user'
}),
success: function() {
alert('배송 처리가 완료되었습니다.');
loadCompounds(); // 목록 새로고침
},
error: function(xhr) {
alert('배송 처리 실패: ' + (xhr.responseJSON?.error || '알 수 없는 오류'));
}
});
}
});