DB 구조 개선: - 454개 주성분코드 기반 herb_masters 테이블 생성 - 53,769개 제품 데이터를 herb_products 테이블에 임포트 - 128개 업체 정보를 product_companies 테이블에 추가 - 기존 herb_items에 ingredient_code 매핑 (100% 완료) UI/API 개선: - 급여 약재 보유 현황 표시 (28/454 = 6.2%) - 재고 현황에 프로그레스 바 추가 - 주성분코드 기준 통계 API 추가 문서화: - 데이터베이스 리팩토링 제안서 작성 - 리팩토링 결과 보고서 작성 - 백업 정보 문서화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1138 lines
46 KiB
JavaScript
1138 lines
46 KiB
JavaScript
// 한약 재고관리 시스템 - Frontend JavaScript
|
|
|
|
$(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;
|
|
}
|
|
}
|
|
|
|
// 대시보드 데이터 로드
|
|
function loadDashboard() {
|
|
// 환자 수
|
|
$.get('/api/patients', function(response) {
|
|
if (response.success) {
|
|
$('#totalPatients').text(response.data.length);
|
|
}
|
|
});
|
|
|
|
// 재고 현황
|
|
$.get('/api/inventory/summary', function(response) {
|
|
if (response.success) {
|
|
$('#totalHerbs').text(response.data.length);
|
|
$('#inventoryValue').text(formatCurrency(response.summary.total_value));
|
|
}
|
|
});
|
|
|
|
// TODO: 오늘 조제 수, 최근 조제 내역
|
|
}
|
|
|
|
// 환자 목록 로드
|
|
function loadPatients() {
|
|
$.get('/api/patients', function(response) {
|
|
if (response.success) {
|
|
const tbody = $('#patientsList');
|
|
tbody.empty();
|
|
|
|
response.data.forEach(patient => {
|
|
tbody.append(`
|
|
<tr>
|
|
<td>${patient.name}</td>
|
|
<td>${patient.phone}</td>
|
|
<td>${patient.gender === 'M' ? '남' : patient.gender === 'F' ? '여' : '-'}</td>
|
|
<td>${patient.birth_date || '-'}</td>
|
|
<td>${patient.notes || '-'}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 환자 등록
|
|
$('#savePatientBtn').on('click', function() {
|
|
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()
|
|
};
|
|
|
|
$.ajax({
|
|
url: '/api/patients',
|
|
method: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify(patientData),
|
|
success: function(response) {
|
|
if (response.success) {
|
|
alert('환자가 등록되었습니다.');
|
|
$('#patientModal').modal('hide');
|
|
$('#patientForm')[0].reset();
|
|
loadPatients();
|
|
}
|
|
},
|
|
error: function(xhr) {
|
|
alert('오류: ' + xhr.responseJSON.error);
|
|
}
|
|
});
|
|
});
|
|
|
|
// 처방 목록 로드
|
|
function loadFormulas() {
|
|
$.get('/api/formulas', function(response) {
|
|
if (response.success) {
|
|
const tbody = $('#formulasList');
|
|
tbody.empty();
|
|
|
|
response.data.forEach(formula => {
|
|
tbody.append(`
|
|
<tr>
|
|
<td>${formula.formula_code || '-'}</td>
|
|
<td>${formula.formula_name}</td>
|
|
<td>${formula.base_cheop}첩</td>
|
|
<td>${formula.base_pouches}파우치</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-info view-ingredients"
|
|
data-id="${formula.formula_id}">
|
|
<i class="bi bi-eye"></i> 보기
|
|
</button>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`);
|
|
});
|
|
|
|
// 구성 약재 보기
|
|
$('.view-ingredients').on('click', function() {
|
|
const formulaId = $(this).data('id');
|
|
$.get(`/api/formulas/${formulaId}/ingredients`, function(response) {
|
|
if (response.success) {
|
|
let ingredientsList = response.data.map(ing =>
|
|
`${ing.herb_name}: ${ing.grams_per_cheop}g`
|
|
).join(', ');
|
|
alert('구성 약재:\n' + ingredientsList);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 처방 구성 약재 추가 (모달)
|
|
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 herbId = $(this).find('.herb-select').val();
|
|
const grams = $(this).find('.grams-input').val();
|
|
|
|
if (herbId && grams) {
|
|
ingredients.push({
|
|
herb_item_id: parseInt(herbId),
|
|
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(),
|
|
ingredients: ingredients
|
|
};
|
|
|
|
$.ajax({
|
|
url: '/api/formulas',
|
|
method: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify(formulaData),
|
|
success: function(response) {
|
|
if (response.success) {
|
|
alert('처방이 등록되었습니다.');
|
|
$('#formulaModal').modal('hide');
|
|
$('#formulaForm')[0].reset();
|
|
$('#formulaIngredients').empty();
|
|
loadFormulas();
|
|
}
|
|
},
|
|
error: function(xhr) {
|
|
alert('오류: ' + xhr.responseJSON.error);
|
|
}
|
|
});
|
|
});
|
|
|
|
// 조제 관리
|
|
$('#newCompoundBtn').on('click', function() {
|
|
$('#compoundForm').show();
|
|
$('#compoundEntryForm')[0].reset();
|
|
$('#compoundIngredients').empty();
|
|
});
|
|
|
|
$('#cancelCompoundBtn').on('click', function() {
|
|
$('#compoundForm').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();
|
|
if (!formulaId) {
|
|
$('#compoundIngredients').empty();
|
|
return;
|
|
}
|
|
|
|
// 직접조제인 경우
|
|
if (formulaId === 'custom') {
|
|
$('#compoundIngredients').empty();
|
|
// 빈 행 하나 추가
|
|
addEmptyIngredientRow();
|
|
return;
|
|
}
|
|
|
|
// 등록된 처방인 경우
|
|
$.get(`/api/formulas/${formulaId}/ingredients`, function(response) {
|
|
if (response.success) {
|
|
$('#compoundIngredients').empty();
|
|
|
|
response.data.forEach(ing => {
|
|
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
|
const totalGrams = ing.grams_per_cheop * cheopTotal;
|
|
|
|
$('#compoundIngredients').append(`
|
|
<tr data-herb-id="${ing.herb_item_id}">
|
|
<td>${ing.herb_name}</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">
|
|
<select class="form-control form-control-sm origin-select" disabled>
|
|
<option value="">로딩중...</option>
|
|
</select>
|
|
</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>
|
|
`);
|
|
|
|
// 각 약재별로 원산지별 재고 확인
|
|
loadOriginOptions(ing.herb_item_id, totalGrams);
|
|
});
|
|
|
|
// 재고 확인
|
|
checkStockForCompound();
|
|
|
|
// 용량 변경 이벤트
|
|
$('.grams-per-cheop').on('input', updateIngredientTotals);
|
|
|
|
// 삭제 버튼 이벤트
|
|
$('.remove-compound-ingredient').on('click', function() {
|
|
$(this).closest('tr').remove();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// 약재별 총 용량 업데이트
|
|
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));
|
|
});
|
|
|
|
checkStockForCompound();
|
|
}
|
|
|
|
// 재고 확인
|
|
function checkStockForCompound() {
|
|
$('#compoundIngredients tr').each(function() {
|
|
const herbId = $(this).data('herb-id');
|
|
const totalGrams = parseFloat($(this).find('.total-grams').text()) || 0;
|
|
const $stockStatus = $(this).find('.stock-status');
|
|
|
|
// TODO: API 호출로 실제 재고 확인
|
|
$stockStatus.text('재고 확인 필요');
|
|
});
|
|
}
|
|
|
|
// 조제 약재 추가
|
|
// 빈 약재 행 추가 함수
|
|
function addEmptyIngredientRow() {
|
|
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="origin-select-cell">
|
|
<select class="form-control form-control-sm origin-select" disabled>
|
|
<option value="">약재 선택 후 표시</option>
|
|
</select>
|
|
</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('.herb-select-compound').on('change', function() {
|
|
const herbId = $(this).val();
|
|
if (herbId) {
|
|
const row = $(this).closest('tr');
|
|
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);
|
|
}
|
|
});
|
|
|
|
// 이벤트 바인딩
|
|
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();
|
|
});
|
|
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();
|
|
|
|
const ingredients = [];
|
|
$('#compoundIngredients tr').each(function() {
|
|
const herbId = $(this).data('herb-id');
|
|
const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val());
|
|
const totalGrams = parseFloat($(this).find('.total-grams').text());
|
|
const originCountry = $(this).find('.origin-select').val();
|
|
|
|
if (herbId && gramsPerCheop) {
|
|
ingredients.push({
|
|
herb_item_id: parseInt(herbId),
|
|
grams_per_cheop: gramsPerCheop,
|
|
total_grams: totalGrams,
|
|
origin_country: originCountry || null // 원산지 선택 정보 추가
|
|
});
|
|
}
|
|
});
|
|
|
|
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()),
|
|
ingredients: ingredients
|
|
};
|
|
|
|
$.ajax({
|
|
url: '/api/compounds',
|
|
method: 'POST',
|
|
contentType: 'application/json',
|
|
data: JSON.stringify(compoundData),
|
|
success: function(response) {
|
|
if (response.success) {
|
|
alert(`조제가 완료되었습니다.\n원가: ${formatCurrency(response.total_cost)}`);
|
|
$('#compoundForm').hide();
|
|
loadCompounds();
|
|
}
|
|
},
|
|
error: function(xhr) {
|
|
alert('오류: ' + xhr.responseJSON.error);
|
|
}
|
|
});
|
|
});
|
|
|
|
// 조제 내역 로드
|
|
function loadCompounds() {
|
|
// TODO: 조제 내역 API 구현 필요
|
|
$('#compoundsList').html('<tr><td colspan="7" class="text-center">조제 내역이 없습니다.</td></tr>');
|
|
}
|
|
|
|
// 재고 현황 로드
|
|
function loadInventory() {
|
|
$.get('/api/inventory/summary', function(response) {
|
|
if (response.success) {
|
|
const tbody = $('#inventoryList');
|
|
tbody.empty();
|
|
|
|
// 주성분코드 기준 보유 현황 표시
|
|
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)}`;
|
|
}
|
|
|
|
tbody.append(`
|
|
<tr class="inventory-row" data-herb-id="${item.herb_item_id}" style="cursor: pointer;">
|
|
<td>${item.insurance_code || '-'}</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>
|
|
</tr>
|
|
`);
|
|
});
|
|
|
|
// 클릭 이벤트 바인딩
|
|
$('.inventory-row').on('click', function() {
|
|
const herbId = $(this).data('herb-id');
|
|
showInventoryDetail(herbId);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 재고 상세 모달 표시
|
|
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>
|
|
</tr>
|
|
</thead>
|
|
<tbody>`;
|
|
|
|
origin.lots.forEach(lot => {
|
|
originsHtml += `
|
|
<tr>
|
|
<td>#${lot.lot_id}</td>
|
|
<td>${lot.quantity_onhand.toFixed(1)}g</td>
|
|
<td>${formatCurrency(lot.unit_price_per_g)}</td>
|
|
<td>${lot.received_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.insurance_code})</small>
|
|
</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 => {
|
|
tbody.append(`
|
|
<tr>
|
|
<td>${herb.insurance_code || '-'}</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-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);
|
|
});
|
|
|
|
$('.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 => {
|
|
linesHtml += `
|
|
<tr>
|
|
<td>${line.herb_name}</td>
|
|
<td>${line.insurance_code || '-'}</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>`);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 도매상 등록
|
|
$('#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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// 입고장 업로드
|
|
$('#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', function(response) {
|
|
if (response.success) {
|
|
selectElement.empty().append('<option value="">약재 선택</option>');
|
|
|
|
response.data.forEach(herb => {
|
|
selectElement.append(`<option value="${herb.herb_item_id}">${herb.herb_name}</option>`);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 원산지별 재고 옵션 로드
|
|
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>');
|
|
|
|
origins.forEach(origin => {
|
|
const stockStatus = origin.total_quantity >= requiredQty ? '' : ' (재고 부족)';
|
|
const priceInfo = `${formatCurrency(origin.min_price)}/g`;
|
|
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} - ${priceInfo} (재고: ${origin.total_quantity.toFixed(1)}g)${stockStatus}
|
|
</option>`;
|
|
selectElement.append(option);
|
|
});
|
|
|
|
selectElement.prop('disabled', false);
|
|
|
|
// 재고 상태 업데이트
|
|
const totalAvailable = response.data.total_quantity;
|
|
const statusElement = $(`tr[data-herb-id="${herbId}"] .stock-status`);
|
|
|
|
if (totalAvailable >= requiredQty) {
|
|
statusElement.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>`);
|
|
} else {
|
|
statusElement.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function formatCurrency(amount) {
|
|
if (amount === null || amount === undefined) return '0원';
|
|
return new Intl.NumberFormat('ko-KR', {
|
|
style: 'currency',
|
|
currency: 'KRW'
|
|
}).format(amount);
|
|
}
|
|
}); |