feat: 조제 원가 미리보기 및 재고 상태 표시 개선
원가 미리보기: - 조제 실행 전 약재별 예상 원가(용량×단가) 및 합계 표시 - 용량/원산지/로트 변경 시 실시간 갱신 - 추가 약재의 이름 표시 오류 수정 (select 내 전체 옵션 텍스트 → 선택값만) 원산지 자동 선택: - 처방 로드 시 재고 충분한 최저가 원산지를 자동 선택 - "자동 선택" 상태가 아닌 실제 원산지가 선택되어 원가 즉시 계산 재고 상태 표시: - checkStockForCompound() TODO 제거, 실제 API 호출로 재고 확인 - 기존 원산지 선택을 덮어쓰지 않고 재고 상태만 갱신 - 선택 가능한 원산지가 2개 이상이면 "N종" 뱃지 표시 조제 폼 초기화: - 새 조제 시 제수 기본값(1)으로 총 첩수(20)/파우치(30) 자동 설정 - 처방 선택 시 총 첩수가 비어있으면 자동 계산 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
974ce5f655
commit
725f14c59a
137
static/app.js
137
static/app.js
@ -1207,10 +1207,16 @@ $(document).ready(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();
|
||||
});
|
||||
|
||||
// 제수 변경 시 첩수 자동 계산
|
||||
@ -1230,12 +1236,20 @@ $(document).ready(function() {
|
||||
$('#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;
|
||||
}
|
||||
|
||||
@ -1364,6 +1378,7 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
// 약재별 총 용량 업데이트
|
||||
let _stockCheckTimer = null;
|
||||
function updateIngredientTotals() {
|
||||
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
||||
|
||||
@ -1373,9 +1388,88 @@ $(document).ready(function() {
|
||||
$(this).find('.total-grams').text(totalGrams.toFixed(1));
|
||||
});
|
||||
|
||||
checkStockForCompound();
|
||||
// 커스텀 처방 감지 호출
|
||||
checkCustomPrescription();
|
||||
// 원가 미리보기 갱신 (즉시)
|
||||
updateCostPreview();
|
||||
// 재고 상태 갱신 (디바운스 300ms)
|
||||
clearTimeout(_stockCheckTimer);
|
||||
_stockCheckTimer = setTimeout(() => checkStockForCompound(), 300);
|
||||
}
|
||||
|
||||
// 원가 미리보기 계산
|
||||
function updateCostPreview() {
|
||||
const rows = $('#compoundIngredients tr');
|
||||
if (rows.length === 0) {
|
||||
$('#costPreview').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const items = [];
|
||||
let totalCost = 0;
|
||||
let allHavePrice = true;
|
||||
|
||||
rows.each(function() {
|
||||
// 약재명: 처방에서 로드된 행은 텍스트, 추가된 행은 select의 선택값
|
||||
const firstTd = $(this).find('td:first');
|
||||
const herbSelect = firstTd.find('.herb-select-compound');
|
||||
const herbName = herbSelect.length > 0
|
||||
? (herbSelect.find('option:selected').text().trim() || '미선택')
|
||||
: firstTd.text().trim().split('(')[0].trim();
|
||||
const totalGrams = parseFloat($(this).find('.total-grams').text()) || 0;
|
||||
const originSelect = $(this).find('.origin-select');
|
||||
const selectedOption = originSelect.find('option:selected');
|
||||
const unitPrice = parseFloat(selectedOption.attr('data-price')) || 0;
|
||||
|
||||
// 수동 배분인 경우 data-lot-assignments에서 계산
|
||||
const lotAssignmentsStr = $(this).attr('data-lot-assignments');
|
||||
let itemCost = 0;
|
||||
|
||||
if (lotAssignmentsStr) {
|
||||
try {
|
||||
const assignments = JSON.parse(lotAssignmentsStr);
|
||||
assignments.forEach(a => {
|
||||
itemCost += (a.quantity || 0) * (a.unit_price || 0);
|
||||
});
|
||||
} catch(e) {
|
||||
itemCost = totalGrams * unitPrice;
|
||||
}
|
||||
} else if (unitPrice > 0) {
|
||||
itemCost = totalGrams * unitPrice;
|
||||
} else {
|
||||
allHavePrice = false;
|
||||
}
|
||||
|
||||
totalCost += itemCost;
|
||||
items.push({ name: herbName, grams: totalGrams, unitPrice, cost: itemCost });
|
||||
});
|
||||
|
||||
// UI 렌더링
|
||||
const tbody = $('#costPreviewItems');
|
||||
tbody.empty();
|
||||
items.forEach(item => {
|
||||
const costText = item.cost > 0
|
||||
? formatCurrency(Math.round(item.cost))
|
||||
: '<span class="text-muted">-</span>';
|
||||
const priceText = item.unitPrice > 0
|
||||
? `${item.grams.toFixed(1)}g × ₩${item.unitPrice.toFixed(1)}`
|
||||
: `${item.grams.toFixed(1)}g`;
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td>${item.name} <small class="text-muted">${priceText}</small></td>
|
||||
<td class="text-end">${costText}</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
|
||||
$('#costPreviewTotal').text(formatCurrency(Math.round(totalCost)));
|
||||
|
||||
const status = allHavePrice && items.length > 0
|
||||
? '<span class="badge bg-success">확정</span>'
|
||||
: '<span class="badge bg-warning text-dark">일부 미확정</span>';
|
||||
$('#costPreviewStatus').html(status);
|
||||
|
||||
$('#costPreview').show();
|
||||
}
|
||||
|
||||
// 커스텀 처방 감지 함수
|
||||
@ -1452,13 +1546,30 @@ $(document).ready(function() {
|
||||
|
||||
// 재고 확인
|
||||
function checkStockForCompound() {
|
||||
// 각 약재의 재고 상태를 API로 갱신 (기존 선택 보존)
|
||||
$('#compoundIngredients tr').each(function() {
|
||||
const herbId = $(this).data('herb-id');
|
||||
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');
|
||||
|
||||
// TODO: API 호출로 실제 재고 확인
|
||||
$stockStatus.text('재고 확인 필요');
|
||||
if (totalGrams > 0) {
|
||||
$.get(`/api/herbs/${herbId}/available-lots`, function(response) {
|
||||
if (response.success) {
|
||||
const totalAvailable = response.data.total_quantity;
|
||||
const origins = response.data.origins;
|
||||
const altCount = origins.length;
|
||||
const altBadge = altCount > 1 ? ` <span class="badge bg-outline-info text-info border border-info" style="font-size:0.65rem">${altCount}종</span>` : '';
|
||||
|
||||
if (totalAvailable >= totalGrams) {
|
||||
$stockStatus.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
|
||||
} else {
|
||||
$stockStatus.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -3055,6 +3166,14 @@ $(document).ready(function() {
|
||||
|
||||
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();
|
||||
@ -3072,18 +3191,24 @@ $(document).ready(function() {
|
||||
// 기존 자동/원산지 선택 - lot_assignments 제거
|
||||
row.removeAttr('data-lot-assignments');
|
||||
}
|
||||
updateCostPreview();
|
||||
});
|
||||
|
||||
// 재고 상태 업데이트
|
||||
const totalAvailable = response.data.total_quantity;
|
||||
const statusElement = $(`tr[data-herb-id="${herbId}"] .stock-status`);
|
||||
const altCount = origins.length;
|
||||
const altBadge = altCount > 1 ? ` <span class="badge bg-outline-info text-info border border-info" style="font-size:0.65rem; cursor:pointer" title="선택 가능한 원산지 ${altCount}종">${altCount}종</span>` : '';
|
||||
|
||||
if (totalAvailable >= requiredQty) {
|
||||
statusElement.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>`);
|
||||
statusElement.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
|
||||
} else {
|
||||
statusElement.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>`);
|
||||
statusElement.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>${altBadge}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 원가 미리보기 갱신
|
||||
updateCostPreview();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -486,6 +486,24 @@
|
||||
<i class="bi bi-plus"></i> 약재 추가
|
||||
</button>
|
||||
</div>
|
||||
<!-- 원가 미리보기 -->
|
||||
<div id="costPreview" class="mt-3 p-3 bg-light rounded border" style="display:none;">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><i class="bi bi-calculator"></i> 예상 원가</h6>
|
||||
<span class="badge bg-secondary" id="costPreviewStatus">계산중...</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<table class="table table-sm table-borderless mb-0" style="font-size: 0.85rem;">
|
||||
<tbody id="costPreviewItems"></tbody>
|
||||
<tfoot>
|
||||
<tr class="border-top fw-bold">
|
||||
<td>합계</td>
|
||||
<td class="text-end" id="costPreviewTotal">₩0</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-check-circle"></i> 조제 실행
|
||||
|
||||
Loading…
Reference in New Issue
Block a user