문제: - 원산지 선택에서 "수동 배분" 선택 시 필요량이 0으로 표시 - loadOriginOptions 함수 호출 시점의 requiredQty를 그대로 사용 해결: - 모달 열기 시점에 현재 행의 실제 필요량 재계산 - gramsPerCheop × cheopTotal로 실시간 계산 - 디버깅 로그 추가로 값 확인 가능 이제 첩수나 용량이 변경된 후에도 정확한 필요량이 모달에 표시됩니다. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2729 lines
117 KiB
JavaScript
2729 lines
117 KiB
JavaScript
// 한약 재고관리 시스템 - Frontend JavaScript
|
|
|
|
// 원래 처방 구성 저장용 전역 변수
|
|
let originalFormulaIngredients = {};
|
|
|
|
// 로트 배분 관련 전역 변수
|
|
let currentLotAllocation = {
|
|
herbId: null,
|
|
requiredQty: 0,
|
|
row: null,
|
|
data: null
|
|
};
|
|
|
|
$(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));
|
|
}
|
|
});
|
|
|
|
// 오늘 조제 수 및 최근 조제 내역
|
|
$.get('/api/compounds', function(response) {
|
|
if (response.success) {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const todayCompounds = response.data.filter(c => c.compound_date === today);
|
|
$('#todayCompounds').text(todayCompounds.length);
|
|
|
|
// 최근 조제 내역 (최근 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-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>';
|
|
}
|
|
|
|
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;
|
|
|
|
tbody.append(`
|
|
<tr>
|
|
<td><strong>${patient.name}</strong></td>
|
|
<td>${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>${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);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 환자 등록
|
|
$('#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 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() {
|
|
$.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();
|
|
|
|
// 원래 처방 구성 초기화
|
|
originalFormulaIngredients = {};
|
|
$('#customPrescriptionBadge').remove(); // 커스텀 뱃지 제거
|
|
|
|
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 => {
|
|
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(); // 총용량 재계산 및 커스텀 감지
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// 약재별 총 용량 업데이트
|
|
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();
|
|
// 커스텀 처방 감지 호출
|
|
checkCustomPrescription();
|
|
}
|
|
|
|
// 커스텀 처방 감지 함수
|
|
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() {
|
|
$('#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 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()),
|
|
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() {
|
|
$.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-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 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>${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>${formatCurrency(compound.sell_price_total || 0)}</td>
|
|
<td>${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>
|
|
</td>
|
|
</tr>
|
|
`);
|
|
tbody.append(row);
|
|
});
|
|
|
|
// 통계 업데이트
|
|
$('#todayCompoundCount').text(todayCount);
|
|
$('#monthCompoundCount').text(monthCount);
|
|
|
|
// 상세보기 버튼 이벤트
|
|
$('.view-compound-detail').on('click', function() {
|
|
const compoundId = $(this).data('id');
|
|
viewCompoundDetail(compoundId);
|
|
});
|
|
} 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(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++;
|
|
|
|
tbody.append(`
|
|
<tr class="inventory-row" data-herb-id="${item.herb_item_id}">
|
|
<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>
|
|
<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>
|
|
</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.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 => {
|
|
// 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>`;
|
|
|
|
linesHtml += `
|
|
<tr>
|
|
<td>
|
|
<div>${line.herb_name}</div>
|
|
${line.display_name ? `<small class="text-primary">${line.display_name}</small>` : ''}
|
|
${variantBadges}
|
|
</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/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);
|
|
});
|
|
}
|
|
|
|
// 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);
|
|
|
|
// 원산지 선택 변경 이벤트 (수동 배분 모달 트리거)
|
|
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');
|
|
}
|
|
});
|
|
|
|
// 재고 상태 업데이트
|
|
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>`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 재고 원장 보기
|
|
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;
|
|
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.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);
|
|
}
|
|
|
|
// ==================== 주성분코드 기반 약재 관리 ====================
|
|
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;
|
|
};
|
|
}); |