// 한약 재고관리 시스템 - 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 = '조제완료'; break; case 'PAID': statusBadge = '결제완료'; break; case 'DELIVERED': statusBadge = '배송완료'; break; case 'COMPLETED': statusBadge = '판매완료'; break; case 'CANCELLED': statusBadge = '취소'; break; default: statusBadge = '대기'; } tbody.append(` ${compound.compound_date || '-'} ${compound.patient_name || '직접조제'} ${compound.formula_name || '직접조제'} ${compound.je_count}제 ${compound.pouch_total}개 ${statusBadge} `); }); } else { tbody.html('조제 내역이 없습니다.'); } } }); } // 환자 목록 로드 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(` ${patient.name} ${formatPhoneNumber(patient.phone)} ${patient.gender === 'M' ? '남' : patient.gender === 'F' ? '여' : '-'} ${patient.birth_date || '-'} ${compoundCount}회 ${mileageBalance.toLocaleString()}P ${mileageBalance > 0 ? '' : ''} ${patient.notes || '-'} `); }); // 처방내역 버튼 이벤트 $('.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(` 처방 내역이 없습니다. `); } else { compounds.forEach((compound, index) => { // 상태 뱃지 let statusBadge = ''; switch(compound.status) { case 'PREPARED': statusBadge = '조제완료'; break; case 'DISPENSED': statusBadge = '출고완료'; break; case 'CANCELLED': statusBadge = '취소'; break; default: statusBadge = '대기'; } const detailRowId = `compound-detail-${compound.compound_id}`; // 처방명 표시 (가감방 여부 포함) let formulaDisplay = compound.formula_name || '직접조제'; if (compound.is_custom && compound.formula_name) { formulaDisplay = `${compound.formula_name} 가감`; } tbody.append(` ${compound.compound_date || '-'} ${formulaDisplay} ${compound.custom_summary ? `
${compound.custom_summary}` : ''} ${compound.je_count || 0} ${compound.cheop_total || 0} ${compound.pouch_total || 0} ${formatCurrency(compound.cost_total || 0)} ${formatCurrency(compound.sell_price_total || 0)} ${statusBadge} ${compound.prescription_no || '-'}
구성 약재
로딩 중...
재고 소비 내역
로딩 중...
`); }); // 행 클릭 이벤트 - 상세 정보 토글 $('.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 = ''; if (data.ingredients && data.ingredients.length > 0) { data.ingredients.forEach(ing => { ingredientsHtml += ` `; }); } else { ingredientsHtml += ''; } ingredientsHtml += '
약재명보험코드첩당용량총용량
${ing.herb_name} ${ing.insurance_code || '-'} ${ing.grams_per_cheop}g ${ing.total_grams}g
약재 정보가 없습니다
'; $(`#ingredients-${compoundId}`).html(ingredientsHtml); // 재고 소비 내역 테이블 let consumptionsHtml = ''; if (data.consumptions && data.consumptions.length > 0) { let totalCost = 0; data.consumptions.forEach(con => { totalCost += con.cost_amount || 0; consumptionsHtml += ` `; }); consumptionsHtml += ` `; } else { consumptionsHtml += ''; } consumptionsHtml += '
약재명원산지도매상사용량단가원가
${con.herb_name} ${con.origin_country || '-'} ${con.supplier_name || '-'} ${con.quantity_used}g ${formatCurrency(con.unit_cost_per_g)}/g ${formatCurrency(con.cost_amount)}
총 원가: ${formatCurrency(totalCost)}
재고 소비 내역이 없습니다
'; $(`#consumptions-${compoundId}`).html(consumptionsHtml); } }).fail(function() { $(`#ingredients-${compoundId}`).html('
데이터를 불러오는데 실패했습니다.
'); $(`#consumptions-${compoundId}`).html('
데이터를 불러오는데 실패했습니다.
'); }); } // 처방 목록 로드 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 ? ` 100처방 #${officialNum}` : ''; // 처방명 스타일링: "어울림" 접두어 색상 처리 let displayName = formula.formula_name; if (displayName.startsWith('어울림 ')) { displayName = `어울림 ${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 => `${m}` )); } if (formula.custom_added && formula.custom_added.length > 0) { details.push(...formula.custom_added.map(a => `+${a}` )); } if (formula.custom_removed && formula.custom_removed.length > 0) { details.push(...formula.custom_removed.map(r => `-${r}` )); } if (details.length > 0) { customInfo = `
${details.join('')}`; } } else if (formula.official_formula_id && !formula.is_custom) { customInfo = ` 원방 그대로`; } tbody.append(` ${formula.formula_code || '-'} ${displayName}${officialBadge}${customInfo} ${formula.base_cheop}첩 ${formula.base_pouches}파우치 `); }); // 처방 상세 보기 버튼 이벤트 $('.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 = `${formula.registered_names.length > 1 ? formula.registered_names.length + '개 등록' : '등록됨'}`; } else { statusBadge = '미등록'; } const hasNotes = formula.reference_notes ? '' : ''; // 구성 약재 수 표시 const ingCount = formula.ingredient_count || 0; const ingBadge = ingCount > 0 ? `${ingCount}종` : `미입력`; tbody.append(` ${formula.formula_number} ${formula.formula_name}${hasNotes} ${formula.formula_name_hanja || '-'} ${formula.source_text || '-'} ${ingBadge} ${statusBadge} `); }); if (response.data.length === 0) { tbody.html('검색 결과가 없습니다.'); } } }); } // 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(` ${idx + 1} ${ing.herb_name} ${ing.herb_name_hanja || ''} ${ing.grams_per_cheop}g ${ing.notes || '-'} `); }); $('#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(` `); // 약재 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(`

${formula.efficacy}

`); } else { $('#formulaEffects').html('

처방의 주요 효능 정보가 등록되지 않았습니다.

'); } // 처방 구성 약재 로드 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 = `재고 ${stockQty.toFixed(1)}g`; } else { stockStatus = `재고없음`; } // 사용 가능한 제품 수 표시 if (ingredient.product_count > 0) { stockStatus += `
${ingredient.product_count}개 제품`; } tbody.append(` ${index + 1} ${ingredient.herb_name}
${ingredient.ingredient_code} ${gram1.toFixed(1)}g ${gram1Je.toFixed(1)}g ${ingredient.notes || '-'} ${stockStatus} `); }); // 합계 업데이트 $('#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(` `); // 약재 목록 로드 (현재 선택된 값 유지) 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(` `); // 약재 목록 로드 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 = ''; if (ing.available_products && ing.available_products.length > 0) { ing.available_products.forEach(product => { const specInfo = product.specification ? ` [${product.specification}]` : ''; productOptions += ``; }); } $('#compoundIngredients').append(` ${ing.herb_name} ${ing.total_available_stock > 0 ? `(총 ${ing.total_available_stock.toFixed(0)}g 사용 가능)` : '(재고 없음)'} ${totalGrams.toFixed(1)}
대기중 `); // 첫 번째 제품 자동 선택 및 원산지 로드 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('').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)) : '-'; const priceText = item.unitPrice > 0 ? `${item.grams.toFixed(1)}g × ₩${item.unitPrice.toFixed(1)}` : `${item.grams.toFixed(1)}g`; tbody.append(` ${item.name} ${priceText} ${costText} `); }); $('#costPreviewTotal').text(formatCurrency(Math.round(totalCost))); const status = allHavePrice && items.length > 0 ? '확정' : '일부 미확정'; $('#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 = `
가감방 ${customDetails.join(' | ')}
`; // 처방 선택 영역 아래에 추가 $('#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 ? ` ${altCount}종` : ''; if (totalAvailable >= totalGrams) { $stockStatus.html(`충분 (${totalAvailable.toFixed(1)}g)${altBadge}`); } else { $stockStatus.html(`부족 (${totalAvailable.toFixed(1)}g)${altBadge}`); } } }); } }); } // 조제 약재 추가 // 빈 약재 행 추가 함수 function addEmptyIngredientRow() { const newRow = $(` 0.0
- `); $('#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('').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('').prop('disabled', true); row.find('.origin-select').empty().append('').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('').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 = $(` 0.0 - `); $('#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 = '조제완료'; break; case 'PENDING_PAYMENT': statusBadge = '결제대기'; break; case 'PAID': statusBadge = '결제완료'; break; case 'PENDING_DELIVERY': statusBadge = '배송대기'; break; case 'DELIVERED': statusBadge = '배송완료'; break; case 'COMPLETED': statusBadge = '판매완료'; break; case 'OTC_CONVERTED': statusBadge = 'OTC전환'; break; case 'CANCELLED': statusBadge = '취소'; break; case 'REFUNDED': statusBadge = '환불'; break; default: statusBadge = '대기'; } // 용도 뱃지 (클릭으로 변경 가능) 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 = `${usageLabels[curUsage]}`; const isSale = curUsage === 'SALE'; const row = $(` ${response.data.length - index} ${compound.compound_date || ''}
${compound.created_at ? compound.created_at.split(' ')[1] : ''} ${compound.patient_name || '직접조제'} ${formatPhoneNumber(compound.patient_phone)} ${compound.formula_name || '직접조제'} ${compound.je_count || 0} ${compound.cheop_total || 0} ${compound.pouch_total || 0} ${formatCurrency(compound.cost_total || 0)} ${isSale ? formatCurrency(compound.sell_price_total || 0) : '-'} ${usageBadge} ${statusBadge} ${compound.prescription_no || '-'} ${compound.status === 'PREPARED' && isSale ? ` ` : ''} ${compound.status === 'PREPARED' ? ` ` : ''} ${compound.status === 'PAID' ? ` ` : ''} `); 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('조제 내역이 없습니다.'); $('#todayCompoundCount').text(0); $('#monthCompoundCount').text(0); } }).fail(function() { $('#compoundsList').html('데이터를 불러오는데 실패했습니다.'); }); } // 조제 상세보기 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 += ' 가감'; if (data.custom_summary) { formulaDisplay += `
${data.custom_summary}`; } } $('#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(` ${ing.herb_name} ${ing.insurance_code || '-'} ${ing.grams_per_cheop}g ${ing.total_grams}g ${ing.notes || '-'} `); }); } // 재고 소비 내역 const consumptionsBody = $('#detailConsumptions'); consumptionsBody.empty(); if (data.consumptions && data.consumptions.length > 0) { data.consumptions.forEach(con => { consumptionsBody.append(` ${con.herb_name} ${con.origin_country || '-'} ${con.supplier_name || '-'} ${con.quantity_used}g ${formatCurrency(con.unit_cost_per_g)}/g ${formatCurrency(con.cost_amount)} `); }); } // 총 원가 $('#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 = `
📊 급여 약재 보유 현황
전체 급여 약재: ${summary.total_ingredient_codes || 454}개 주성분
보유 약재: ${summary.owned_ingredient_codes || 0}개 주성분
보유율: ${summary.coverage_rate || 0}%
${summary.owned_ingredient_codes || 0} / ${summary.total_ingredient_codes || 454}
※ 건강보험 급여 한약재 ${summary.total_ingredient_codes || 454}개 주성분 중 ${summary.owned_ingredient_codes || 0}개 보유
`; // 재고 테이블 위에 통계 표시 if ($('#inventoryCoverage').length === 0) { $('#inventoryList').parent().before(`
${coverageHtml}
`); } else { $('#inventoryCoverage').html(coverageHtml); } } response.data.forEach(item => { // 원산지가 여러 개인 경우 표시 const originBadge = item.origin_count > 1 ? `${item.origin_count}개 원산지` : ''; // 효능 태그 표시 let efficacyTags = ''; if (item.efficacy_tags && item.efficacy_tags.length > 0) { efficacyTags = item.efficacy_tags.map(tag => `${tag}` ).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 ? 'OTC' : '한약재'; const codeDisplay = isOTC ? (item.standard_code || '-') : (item.insurance_code || '-'); tbody.append(` ${typeBadge} ${codeDisplay} ${item.herb_name}${originBadge}${efficacyTags} ${item.total_quantity.toFixed(1)} ${item.lot_count} ${priceDisplay} ${formatCurrency(item.total_value)} `); }); // 통계 업데이트 $('#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 += `
${origin.origin_country} ${origin.total_quantity.toFixed(1)}g
평균 단가:
${formatCurrency(origin.avg_price)}/g
재고 가치:
${formatCurrency(origin.total_value)}
`; origin.lots.forEach(lot => { // variant 속성들을 뱃지로 표시 let variantBadges = ''; if (lot.form) variantBadges += `${lot.form}`; if (lot.processing) variantBadges += `${lot.processing}`; if (lot.grade) variantBadges += `${lot.grade}`; originsHtml += ` `; }); originsHtml += `
로트ID 품명 수량 단가 입고일 유통기한 도매상
#${lot.lot_id} ${lot.display_name ? `${lot.display_name}` : '-'} ${variantBadges} ${lot.quantity_onhand.toFixed(1)}g ${formatCurrency(lot.unit_price_per_g)} ${lot.received_date} ${lot.expiry_date || '-'} ${lot.supplier_name || '-'}
`; }); // 모달 생성 및 표시 const modalHtml = ` `; // 기존 모달 제거 $('#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 ? 'OTC' : '한약재'; tbody.append(` ${hTypeBadge} ${hCode} ${herb.herb_name} ${herb.specification || '-'} ${herb.current_stock ? herb.current_stock.toFixed(1) + 'g' : '0g'} `); }); } }); } // 입고장 목록 로드 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('입고장이 없습니다.'); return; } response.data.forEach(receipt => { tbody.append(` ${receipt.receipt_date} ${receipt.supplier_name} ${receipt.line_count}개 ${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'} ${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'} ${receipt.source_file || '-'} `); }); // 이벤트 바인딩 $('.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 += `${line.form}`; if (line.processing) variantBadges += `${line.processing}`; if (line.grade) variantBadges += `${line.grade}`; const lineIsOTC = line.product_type === 'OTC'; const lineTypeBadge = lineIsOTC ? 'OTC' : '한약재'; const lineCode = lineIsOTC ? (line.standard_code || '-') : (line.insurance_code || '-'); const lineCodeLabel = lineIsOTC ? '표준' : '보험'; linesHtml += ` ${lineTypeBadge}
${line.herb_name}
${line.display_name ? `${line.display_name}` : ''} ${variantBadges} ${lineCodeLabel}
${lineCode} ${line.origin_country || '-'} ${line.quantity_g}g ${formatCurrency(line.unit_price_per_g)} ${formatCurrency(line.line_total)} ${line.current_stock}g `; }); const modalHtml = ` `; // 기존 모달 제거 $('#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(''); response.data.forEach(supplier => { select.append(``); }); // 필터용 셀렉트 박스도 업데이트 const filterSelect = $('#purchaseSupplier'); filterSelect.empty().append(''); response.data.forEach(supplier => { filterSelect.append(``); }); // 수동 입고용 셀렉트 박스도 업데이트 const manualSelect = $('#manualReceiptSupplier'); manualSelect.empty().append(''); response.data.forEach(supplier => { manualSelect.append(``); }); } }); } // 도매상 등록 $('#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(''); response.data.forEach(herb => { let displayName = herb.herb_name; if (herb.herb_name_hanja) { displayName += ` (${herb.herb_name_hanja})`; } selectElement.append(``); }); 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(' 입고장 수정'); 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(' 수정 저장'); // 기존 라인 데이터로 행 채우기 receipt.lines.forEach(line => { manualReceiptLineCount++; const row = ` ${(line.line_total || 0).toLocaleString('ko-KR')} `; $('#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 = ` 0 `; $('#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(' 수동 입고'); modal.find('.modal-header').removeClass('bg-warning').addClass('bg-success'); $('#saveManualReceiptBtn').html(' 입고 저장'); $('#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(' 수정 저장'); } }); } 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(' 입고 저장'); } }); } }); // 입고장 업로드 $('#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('
업로드 중...
'); $.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 = `
형식: ${response.summary.format}
처리: ${response.summary.processed_rows}개 라인
품목: ${response.summary.total_items}종
수량: ${response.summary.total_quantity}
금액: ${response.summary.total_amount}
`; } $('#uploadResult').html( `
${response.message} ${summaryHtml}
` ); $('#purchaseUploadForm')[0].reset(); // 입고장 목록 새로고침 loadPurchaseReceipts(); } }, error: function(xhr) { $('#uploadResult').html( `
오류: ${xhr.responseJSON.error}
` ); } }); }); // 검색 기능 $('#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(''); response.data.forEach(patient => { select.append(``); }); } }); } function loadFormulasForSelect() { $.get('/api/formulas', function(response) { if (response.success) { const select = $('#compoundFormula'); select.empty().append(''); // 직접조제 옵션 추가 select.append(''); // 등록된 처방 추가 if (response.data.length > 0) { select.append(''); response.data.forEach(formula => { select.append(``); }); select.append(''); } } }); } function loadHerbsForSelect(selectElement) { $.get('/api/herbs/masters', function(response) { if (response.success) { selectElement.empty().append(''); // 재고가 있는 약재만 필터링하여 표시 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(``); }); } }).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(''); // 재고가 있는 약재만 필터링하여 표시 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(``); }); // 만약 현재 선택된 약재가 목록에 없다면 (재고가 없거나 비활성 상태일 경우) 추가 if (!currentFound && currentValue && currentText) { selectElement.append(``); } } }).fail(function(error) { console.error('Failed to load herbs:', error); // 로드 실패시에도 현재 선택된 값은 유지 if (currentValue && currentText) { selectElement.append(``); } }); } // 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(''); productSelect.prop('disabled', true); } else { productSelect.append(''); 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(``); }); 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(''); selectElement.prop('disabled', true); $(`tr[data-herb-id="${herbId}"] .stock-status`) .html('재고 없음'); } else { selectElement.append(''); // 로트가 2개 이상인 경우 수동 배분 옵션 추가 const totalLots = origins.reduce((sum, o) => sum + o.lot_count, 0); if (totalLots > 1) { selectElement.append(''); } 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 = ``; 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 ? ` ${altCount}종` : ''; if (totalAvailable >= requiredQty) { statusElement.html(`충분 (${totalAvailable.toFixed(1)}g)${altBadge}`); } else { statusElement.html(`부족 (${totalAvailable.toFixed(1)}g)${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(` ${herbName} 입출고 원장`); } else { $('#stockLedgerModal .modal-title').html(` 전체 입출고 원장`); } // 필터 적용하여 표시 applyLedgerFilters(); // 약재 필터 옵션 업데이트 const herbFilter = $('#ledgerHerbFilter'); if (herbFilter.find('option').length <= 1) { response.summary.forEach(herb => { herbFilter.append(``); }); } $('#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 ? `+${quantity.toFixed(1)}g` : `-${quantity.toFixed(1)}g`; const referenceInfo = entry.patient_name ? `${entry.patient_name}` : entry.supplier_name || '-'; tbody.append(` ${entry.event_time} ${typeLabel} ${entry.herb_name} ${quantityDisplay} ${entry.unit_cost_per_g ? formatCurrency(entry.unit_cost_per_g) + '/g' : '-'} ${entry.origin_country || '-'} ${referenceInfo} ${entry.reference_no || '-'} `); }); // 데이터가 없는 경우 if (filteredData.length === 0) { tbody.append(` 데이터가 없습니다. `); } } // 입출고 원장 모달 버튼 이벤트 $('#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(` 보정 내역이 없습니다. `); } 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(` ${adj.adjustment_date} ${adj.adjustment_no} ${typeLabel} ${adj.detail_count || 0}개 ${adj.created_by || '-'} ${adj.notes || '-'} `); }); // 상세보기 버튼 이벤트 $('.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(`${typeLabel}`); $('#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 = `+${delta.toFixed(1)}g`; } else if (delta < 0) { deltaHtml = `${delta.toFixed(1)}g`; } else { deltaHtml = '0g'; } itemsBody.append(` ${item.herb_name} ${item.product_type === 'OTC' ? (item.standard_code || '-') : (item.insurance_code || '-')} ${item.origin_country || '-'} #${item.lot_id} ${item.quantity_before.toFixed(1)}g ${item.quantity_after.toFixed(1)}g ${deltaHtml} ${item.reason || '-'} `); }); } // 보정 상세 모달 표시 $('#adjustmentDetailModal').modal('show'); } }).fail(function() { alert('보정 상세 정보를 불러오는데 실패했습니다.'); }); } // 보정 대상 약재 추가 let adjustmentItemCount = 0; $('#addAdjustmentItemBtn').on('click', function() { addAdjustmentItemRow(); }); function addAdjustmentItemRow() { adjustmentItemCount++; const rowId = `adj-item-${adjustmentItemCount}`; const newRow = $(` - - `); $('#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('').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(''); const data = response.data; // 원산지별로 로트 표시 data.origins.forEach(origin => { const optgroup = $(``); origin.lots.forEach(lot => { optgroup.append(` `); }); 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(`+${delta.toFixed(1)}g`); } else if (delta < 0) { deltaElement.html(`${delta.toFixed(1)}g`); } else { deltaElement.html('0g'); } 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 = `

• 입고장 없는 LOT: ${modeInfo.no_receipt_lots}개

• 해당 재고 가치: ${formatCurrency(modeInfo.no_receipt_value)}

`; } else { infoHtml = '

• 모든 LOT이 입고장과 연결되어 있습니다.

'; } } else if (modeInfo.mode === 'receipt_only') { infoHtml = '

• 입고장과 연결된 LOT만 계산합니다.

'; } else if (modeInfo.mode === 'verified') { infoHtml = '

• 검증 확인된 LOT만 계산합니다.

'; } 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 = ` `; // Toast 컨테이너가 없으면 생성 if (!$('#toastContainer').length) { $('body').append('
'); } 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(`
📊 급여 약재 현황
전체: ${summary.total_herbs}개 주성분
재고 있음: ${summary.herbs_with_stock}개
재고 없음: ${summary.herbs_without_stock}개
보유율: ${summary.coverage_rate}%
${summary.herbs_with_stock} / ${summary.total_herbs}
`); // 목록 표시 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 => `${tag}` ).join(''); } // 상태 표시 const statusBadge = herb.has_stock ? '재고 있음' : '재고 없음'; // 재고량 표시 const stockDisplay = herb.stock_quantity > 0 ? `${herb.stock_quantity.toFixed(1)}g` : '-'; // 평균단가 표시 const priceDisplay = herb.avg_price > 0 ? formatCurrency(herb.avg_price) : '-'; tbody.append(` ${herb.ingredient_code} ${herb.herb_name} ${efficacyTags} ${stockDisplay} ${priceDisplay} ${herb.product_count || 0}개 ${statusBadge} `); }); if (filteredHerbs.length === 0) { tbody.append('표시할 약재가 없습니다.'); } } // 약재 상세 보기 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(` #${lot.lot_id} ${lot.origin || '미지정'} ${lot.quantity_onhand.toFixed(1)}g ${formatCurrency(lot.unit_price_per_g)}/g 0원 `); }); // 입력 이벤트 $('.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(`수동 배분 (${allocations.length}개 로트)`); $('#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('
검색 결과가 없습니다.
'); return; } herbs.forEach(herb => { // 재고 상태에 따른 배지 색상 const stockBadge = herb.has_stock ? '재고있음' : '재고없음'; // 효능 태그 HTML let tagsHtml = ''; if (herb.efficacy_tags && herb.efficacy_tags.length > 0) { tagsHtml = herb.efficacy_tags.slice(0, 3).map(tag => `${tag}` ).join(''); if (herb.efficacy_tags.length > 3) { tagsHtml += `+${herb.efficacy_tags.length - 3}`; } } const card = `
${herb.herb_name} ${herb.herb_name_hanja ? `(${herb.herb_name_hanja})` : ''}
${stockBadge}

${herb.ingredient_code}

${tagsHtml || '태그 없음'}
${herb.main_effects ? `

${herb.main_effects.substring(0, 50)}...

` : '

효능 정보 없음

' }
`; 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 `${tag.name || tag}`; }).join(''); $('#detailEfficacyTags').html(tagsHtml); } else { $('#detailEfficacyTags').html('태그 없음'); } // 용법용량 $('#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(''); tags.forEach(tag => { select.append(``); }); }); } // 효능 태그 버튼 표시 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 = `
${category}
${grouped[category].map(tag => ` `).join('')}
`; 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 || '알 수 없는 오류')); } }); } });