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();
|
$('#compoundForm').show();
|
||||||
$('#compoundEntryForm')[0].reset();
|
$('#compoundEntryForm')[0].reset();
|
||||||
$('#compoundIngredients').empty();
|
$('#compoundIngredients').empty();
|
||||||
|
$('#costPreview').hide();
|
||||||
|
// 제수 기본값(1)으로 첩수/파우치 초기화
|
||||||
|
$('#jeCount').val(1);
|
||||||
|
$('#cheopTotal').val(20);
|
||||||
|
$('#pouchTotal').val(30);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#cancelCompoundBtn').on('click', function() {
|
$('#cancelCompoundBtn').on('click', function() {
|
||||||
$('#compoundForm').hide();
|
$('#compoundForm').hide();
|
||||||
|
$('#costPreview').hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 제수 변경 시 첩수 자동 계산
|
// 제수 변경 시 첩수 자동 계산
|
||||||
@ -1230,12 +1236,20 @@ $(document).ready(function() {
|
|||||||
$('#compoundFormula').on('change', function() {
|
$('#compoundFormula').on('change', function() {
|
||||||
const formulaId = $(this).val();
|
const formulaId = $(this).val();
|
||||||
|
|
||||||
|
// 제수 기반 첩수/파우치 자동 계산 (초기값 반영)
|
||||||
|
const jeCount = parseFloat($('#jeCount').val()) || 0;
|
||||||
|
if (jeCount > 0 && !$('#cheopTotal').val()) {
|
||||||
|
$('#cheopTotal').val(jeCount * 20);
|
||||||
|
$('#pouchTotal').val(jeCount * 30);
|
||||||
|
}
|
||||||
|
|
||||||
// 원래 처방 구성 초기화
|
// 원래 처방 구성 초기화
|
||||||
originalFormulaIngredients = {};
|
originalFormulaIngredients = {};
|
||||||
$('#customPrescriptionBadge').remove(); // 커스텀 뱃지 제거
|
$('#customPrescriptionBadge').remove(); // 커스텀 뱃지 제거
|
||||||
|
|
||||||
if (!formulaId) {
|
if (!formulaId) {
|
||||||
$('#compoundIngredients').empty();
|
$('#compoundIngredients').empty();
|
||||||
|
$('#costPreview').hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1364,6 +1378,7 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 약재별 총 용량 업데이트
|
// 약재별 총 용량 업데이트
|
||||||
|
let _stockCheckTimer = null;
|
||||||
function updateIngredientTotals() {
|
function updateIngredientTotals() {
|
||||||
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
||||||
|
|
||||||
@ -1373,9 +1388,88 @@ $(document).ready(function() {
|
|||||||
$(this).find('.total-grams').text(totalGrams.toFixed(1));
|
$(this).find('.total-grams').text(totalGrams.toFixed(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
checkStockForCompound();
|
|
||||||
// 커스텀 처방 감지 호출
|
// 커스텀 처방 감지 호출
|
||||||
checkCustomPrescription();
|
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() {
|
function checkStockForCompound() {
|
||||||
|
// 각 약재의 재고 상태를 API로 갱신 (기존 선택 보존)
|
||||||
$('#compoundIngredients tr').each(function() {
|
$('#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 totalGrams = parseFloat($(this).find('.total-grams').text()) || 0;
|
||||||
|
const currentSelection = $(this).find('.origin-select').val();
|
||||||
const $stockStatus = $(this).find('.stock-status');
|
const $stockStatus = $(this).find('.stock-status');
|
||||||
|
|
||||||
// TODO: API 호출로 실제 재고 확인
|
if (totalGrams > 0) {
|
||||||
$stockStatus.text('재고 확인 필요');
|
$.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);
|
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() {
|
selectElement.off('change').on('change', function() {
|
||||||
const selectedValue = $(this).val();
|
const selectedValue = $(this).val();
|
||||||
@ -3072,18 +3191,24 @@ $(document).ready(function() {
|
|||||||
// 기존 자동/원산지 선택 - lot_assignments 제거
|
// 기존 자동/원산지 선택 - lot_assignments 제거
|
||||||
row.removeAttr('data-lot-assignments');
|
row.removeAttr('data-lot-assignments');
|
||||||
}
|
}
|
||||||
|
updateCostPreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 재고 상태 업데이트
|
// 재고 상태 업데이트
|
||||||
const totalAvailable = response.data.total_quantity;
|
const totalAvailable = response.data.total_quantity;
|
||||||
const statusElement = $(`tr[data-herb-id="${herbId}"] .stock-status`);
|
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) {
|
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 {
|
} 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> 약재 추가
|
<i class="bi bi-plus"></i> 약재 추가
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div class="mt-3">
|
||||||
<button type="submit" class="btn btn-success">
|
<button type="submit" class="btn btn-success">
|
||||||
<i class="bi bi-check-circle"></i> 조제 실행
|
<i class="bi bi-check-circle"></i> 조제 실행
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user