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