feat: 100처방 UI, 처방 가감 표시, 어울림 스타일링
- 처방 관리 페이지에 100처방 원방 마스터 섹션 추가 (검색 포함) - 100처방 상세 모달 (구성약재, 참고자료 편집, 내 처방으로 등록) - 내 처방 목록에 100처방 뱃지 및 가감 정보 표시 - 변경: 파란 뱃지, 추가: 초록 뱃지, 제거: 빨간 뱃지 - 원방 그대로: 회색 뱃지 - "어울림" 접두어 초록색 볼드 스타일링 - stock_ledger에 RETURN(반환)/DISCARD(폐기) 한글 라벨 추가 - 수동입고 원산지 드롭다운 변경, 재고 상세 유통기한 표시 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51e0c99c77
commit
1679f75d33
540
static/app.js
540
static/app.js
@ -577,16 +577,69 @@ $(document).ready(function() {
|
||||
|
||||
// 처방 목록 로드
|
||||
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처방 매칭: 정확 매칭 우선, 없으면 내 처방명이 100처방명으로 시작하는지 확인
|
||||
let officialNum = officialNames.get(formula.formula_name);
|
||||
if (officialNum == null) {
|
||||
for (const [name, num] of officialNames) {
|
||||
if (formula.formula_name.startsWith(name)) {
|
||||
officialNum = num;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const officialBadge = officialNum != null
|
||||
? ` <span class="badge bg-info">100처방 #${officialNum}</span>`
|
||||
: '';
|
||||
|
||||
// 처방명 스타일링: "어울림" 접두어 색상 처리
|
||||
let displayName = formula.formula_name;
|
||||
if (displayName.startsWith('어울림 ')) {
|
||||
displayName = `<span class="text-success fw-bold">어울림</span> ${displayName.substring(4)}`;
|
||||
}
|
||||
|
||||
// 가감 정보 표시 (100처방 기반 처방)
|
||||
let customInfo = '';
|
||||
if (formula.official_formula_id && formula.is_custom) {
|
||||
let details = [];
|
||||
if (formula.custom_modified && formula.custom_modified.length > 0) {
|
||||
details.push(...formula.custom_modified.map(m =>
|
||||
`<span class="badge bg-primary bg-opacity-75 me-1">${m}</span>`
|
||||
));
|
||||
}
|
||||
if (formula.custom_added && formula.custom_added.length > 0) {
|
||||
details.push(...formula.custom_added.map(a =>
|
||||
`<span class="badge bg-success bg-opacity-75 me-1">+${a}</span>`
|
||||
));
|
||||
}
|
||||
if (formula.custom_removed && formula.custom_removed.length > 0) {
|
||||
details.push(...formula.custom_removed.map(r =>
|
||||
`<span class="badge bg-danger bg-opacity-75 me-1">-${r}</span>`
|
||||
));
|
||||
}
|
||||
if (details.length > 0) {
|
||||
customInfo = `<br><span class="d-inline-flex flex-wrap gap-1 mt-1">${details.join('')}</span>`;
|
||||
}
|
||||
} else if (formula.official_formula_id && !formula.is_custom) {
|
||||
customInfo = ` <span class="badge bg-secondary">원방 그대로</span>`;
|
||||
}
|
||||
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td>${formula.formula_code || '-'}</td>
|
||||
<td>${formula.formula_name}</td>
|
||||
<td>${displayName}${officialBadge}${customInfo}</td>
|
||||
<td>${formula.base_cheop}첩</td>
|
||||
<td>${formula.base_pouches}파우치</td>
|
||||
<td>
|
||||
@ -633,9 +686,214 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 내 처방 렌더링 완료 후 100처방 로드
|
||||
loadOfficialFormulas();
|
||||
});
|
||||
}); // /api/official-formulas 콜백 닫기
|
||||
}
|
||||
|
||||
// 100처방 원방 마스터 로드
|
||||
function loadOfficialFormulas(search) {
|
||||
const params = search ? `?search=${encodeURIComponent(search)}` : '';
|
||||
|
||||
// 내 처방 이름 목록을 API에서 가져와서 비교
|
||||
$.get('/api/formulas', function(formulasRes) {
|
||||
const myFormulaNames = new Set();
|
||||
if (formulasRes.success) {
|
||||
formulasRes.data.forEach(f => myFormulaNames.add(f.formula_name));
|
||||
}
|
||||
|
||||
$.get(`/api/official-formulas${params}`, function(response) {
|
||||
if (response.success) {
|
||||
const tbody = $('#officialFormulasList');
|
||||
tbody.empty();
|
||||
|
||||
$('#officialFormulaCount').text(response.data.length);
|
||||
|
||||
response.data.forEach(formula => {
|
||||
// 등록 여부: 정확 매칭 또는 내 처방명이 100처방명으로 시작
|
||||
const isRegistered = myFormulaNames.has(formula.formula_name)
|
||||
|| [...myFormulaNames].some(name => name.startsWith(formula.formula_name));
|
||||
const statusBadge = isRegistered
|
||||
? '<span class="badge bg-success">등록됨</span>'
|
||||
: '<span class="badge bg-outline-secondary text-muted">미등록</span>';
|
||||
|
||||
const hasNotes = formula.reference_notes ? '<i class="bi bi-journal-text text-info ms-1" title="참고자료 있음"></i>' : '';
|
||||
|
||||
tbody.append(`
|
||||
<tr class="official-formula-row" style="cursor:pointer"
|
||||
data-id="${formula.official_formula_id}"
|
||||
data-number="${formula.formula_number}"
|
||||
data-name="${formula.formula_name}"
|
||||
data-hanja="${formula.formula_name_hanja || ''}"
|
||||
data-source="${formula.source_text || ''}"
|
||||
data-description="${(formula.description || '').replace(/"/g, '"')}"
|
||||
data-notes="${(formula.reference_notes || '').replace(/"/g, '"')}">
|
||||
<td class="text-center">${formula.formula_number}</td>
|
||||
<td><strong>${formula.formula_name}</strong>${hasNotes}</td>
|
||||
<td class="text-muted">${formula.formula_name_hanja || '-'}</td>
|
||||
<td>${formula.source_text || '-'}</td>
|
||||
<td class="text-center">${statusBadge}</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
|
||||
if (response.data.length === 0) {
|
||||
tbody.html('<tr><td colspan="5" class="text-center text-muted">검색 결과가 없습니다.</td></tr>');
|
||||
}
|
||||
}
|
||||
});
|
||||
}); // /api/formulas 콜백 닫기
|
||||
}
|
||||
|
||||
// 100처방 검색 이벤트
|
||||
let officialSearchTimer = null;
|
||||
$(document).on('input', '#officialFormulaSearch', function() {
|
||||
clearTimeout(officialSearchTimer);
|
||||
const search = $(this).val().trim();
|
||||
officialSearchTimer = setTimeout(() => {
|
||||
loadOfficialFormulas(search);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 100처방 행 클릭 → 상세/참고자료 모달
|
||||
$(document).on('click', '.official-formula-row', function() {
|
||||
const row = $(this);
|
||||
const id = row.data('id');
|
||||
$('#officialFormulaModal').data('formula-id', id);
|
||||
$('#ofModalNumber').text(row.data('number'));
|
||||
$('#ofModalName').text(row.data('name'));
|
||||
$('#ofModalHanja').text(row.data('hanja') || '');
|
||||
$('#ofModalSource').text(row.data('source') || '-');
|
||||
$('#ofEditHanja').val(row.data('hanja') || '');
|
||||
$('#ofEditDescription').val(row.data('description') || '');
|
||||
$('#ofEditReferenceNotes').val(row.data('notes') || '');
|
||||
|
||||
// 원방 구성 약재 로드
|
||||
$.get(`/api/official-formulas/${id}/ingredients`, function(res) {
|
||||
const section = $('#ofIngredientsSection');
|
||||
const tbody = $('#ofIngredientsList');
|
||||
tbody.empty();
|
||||
|
||||
if (res.success && res.data.length > 0) {
|
||||
let totalGrams = 0;
|
||||
res.data.forEach((ing, idx) => {
|
||||
totalGrams += ing.grams_per_cheop;
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td class="text-muted">${idx + 1}</td>
|
||||
<td><strong>${ing.herb_name}</strong> <small class="text-muted">${ing.herb_name_hanja || ''}</small></td>
|
||||
<td class="text-end">${ing.grams_per_cheop}g</td>
|
||||
<td><small class="text-muted">${ing.notes || '-'}</small></td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
$('#ofIngredientCount').text(res.data.length);
|
||||
$('#ofTotalGrams').text(totalGrams.toFixed(1));
|
||||
section.show();
|
||||
} else {
|
||||
section.hide();
|
||||
}
|
||||
});
|
||||
|
||||
$('#officialFormulaModal').modal('show');
|
||||
});
|
||||
|
||||
// 100처방 참고자료 저장
|
||||
$(document).on('click', '#saveOfficialFormulaBtn', function() {
|
||||
const id = $('#officialFormulaModal').data('formula-id');
|
||||
$.ajax({
|
||||
url: `/api/official-formulas/${id}`,
|
||||
method: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
formula_name_hanja: $('#ofEditHanja').val().trim(),
|
||||
description: $('#ofEditDescription').val().trim(),
|
||||
reference_notes: $('#ofEditReferenceNotes').val().trim()
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('저장되었습니다.');
|
||||
$('#officialFormulaModal').modal('hide');
|
||||
loadOfficialFormulas($('#officialFormulaSearch').val().trim());
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
alert(xhr.responseJSON?.error || '저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 100처방 → 내 처방으로 등록
|
||||
$(document).on('click', '#createFromOfficialBtn', function() {
|
||||
const id = $('#officialFormulaModal').data('formula-id');
|
||||
const name = $('#ofModalName').text();
|
||||
const description = $('#ofEditDescription').val().trim();
|
||||
|
||||
// 100처방 모달 닫기
|
||||
$('#officialFormulaModal').modal('hide');
|
||||
|
||||
// 처방 등록 모달 초기화 (신규 모드)
|
||||
$('#formulaModal').data('edit-mode', false);
|
||||
$('#formulaModal').data('formula-id', null);
|
||||
$('#formulaModal .modal-title').text('처방 등록 (원방 기반)');
|
||||
$('#formulaForm')[0].reset();
|
||||
$('#formulaIngredients').empty();
|
||||
|
||||
// 기본값 세팅
|
||||
$('#formulaName').val(`어울림 ${name}`);
|
||||
$('#formulaType').val('CUSTOM');
|
||||
$('#baseCheop').val(20);
|
||||
$('#basePouches').val(30);
|
||||
$('#formulaDescription').val(description);
|
||||
$('#formulaModal').data('official-formula-id', id);
|
||||
|
||||
// 원방 구성 약재 로드
|
||||
$.get(`/api/official-formulas/${id}/ingredients`, function(res) {
|
||||
if (res.success && res.data.length > 0) {
|
||||
formulaIngredientCount = 0;
|
||||
|
||||
res.data.forEach(ing => {
|
||||
formulaIngredientCount++;
|
||||
$('#formulaIngredients').append(`
|
||||
<tr data-row="${formulaIngredientCount}">
|
||||
<td>
|
||||
<select class="form-control form-control-sm herb-select">
|
||||
<option value="${ing.ingredient_code}" selected>${ing.herb_name}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm grams-input"
|
||||
min="0.1" step="0.1" value="${ing.grams_per_cheop}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm notes-input" value="${ing.notes || ''}">
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-ingredient">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
|
||||
// 약재 select에 전체 목록 로드 (현재 값 유지)
|
||||
const selectEl = $(`#formulaIngredients tr[data-row="${formulaIngredientCount}"] .herb-select`);
|
||||
loadHerbsForSelectWithCurrent(selectEl, ing.ingredient_code, ing.herb_name);
|
||||
});
|
||||
|
||||
// 삭제 버튼 이벤트
|
||||
$('.remove-ingredient').on('click', function() {
|
||||
$(this).closest('tr').remove();
|
||||
});
|
||||
}
|
||||
|
||||
// 처방 등록 모달 열기
|
||||
$('#formulaModal').modal('show');
|
||||
});
|
||||
});
|
||||
|
||||
// 처방 상세 정보 표시 함수
|
||||
function showFormulaDetail(formulaId, formulaName) {
|
||||
// 모달에 formulaId 저장
|
||||
@ -897,7 +1155,8 @@ $(document).ready(function() {
|
||||
base_pouches: parseInt($('#basePouches').val()),
|
||||
description: $('#formulaDescription').val(),
|
||||
efficacy: $('#formulaEfficacy').val(),
|
||||
ingredients: ingredients
|
||||
ingredients: ingredients,
|
||||
official_formula_id: $('#formulaModal').data('official-formula-id') || null
|
||||
};
|
||||
|
||||
// 수정 모드인지 확인
|
||||
@ -922,6 +1181,7 @@ $(document).ready(function() {
|
||||
// 수정 모드 초기화
|
||||
$('#formulaModal').data('edit-mode', false);
|
||||
$('#formulaModal').data('formula-id', null);
|
||||
$('#formulaModal').data('official-formula-id', null);
|
||||
$('#formulaModal .modal-title').text('처방 등록');
|
||||
loadFormulas();
|
||||
}
|
||||
@ -1459,6 +1719,9 @@ $(document).ready(function() {
|
||||
data-price="${compound.sell_price_total || 0}">
|
||||
<i class="bi bi-cash-coin"></i> 판매
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger cancel-compound" data-id="${compound.compound_id}">
|
||||
<i class="bi bi-x-circle"></i> 취소
|
||||
</button>
|
||||
` : ''}
|
||||
${compound.status === 'PAID' ? `
|
||||
<button class="btn btn-sm btn-outline-primary process-delivery" data-id="${compound.compound_id}">
|
||||
@ -1498,6 +1761,31 @@ $(document).ready(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 || '취소 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tbody.html('<tr><td colspan="13" class="text-center text-muted">조제 내역이 없습니다.</td></tr>');
|
||||
$('#todayCompoundCount').text(0);
|
||||
@ -1754,6 +2042,7 @@ $(document).ready(function() {
|
||||
<th>수량</th>
|
||||
<th>단가</th>
|
||||
<th>입고일</th>
|
||||
<th>유통기한</th>
|
||||
<th>도매상</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -1776,6 +2065,7 @@ $(document).ready(function() {
|
||||
<td>${lot.quantity_onhand.toFixed(1)}g</td>
|
||||
<td>${formatCurrency(lot.unit_price_per_g)}</td>
|
||||
<td>${lot.received_date}</td>
|
||||
<td>${lot.expiry_date || '-'}</td>
|
||||
<td>${lot.supplier_name || '-'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
@ -1887,6 +2177,9 @@ $(document).ready(function() {
|
||||
<button class="btn btn-sm btn-outline-info view-receipt" data-id="${receipt.receipt_id}">
|
||||
<i class="bi bi-eye"></i> 상세
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning edit-receipt" data-id="${receipt.receipt_id}">
|
||||
<i class="bi bi-pencil"></i> 수정
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger delete-receipt" data-id="${receipt.receipt_id}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
@ -1901,6 +2194,11 @@ $(document).ready(function() {
|
||||
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('정말 이 입고장을 삭제하시겠습니까? 사용되지 않은 재고만 삭제 가능합니다.')) {
|
||||
@ -2076,7 +2374,7 @@ $(document).ready(function() {
|
||||
// ==================== 수동 입고 ====================
|
||||
|
||||
// 전체 약재 목록 로드 (입고용 - 재고 필터 없음)
|
||||
function loadAllHerbsForSelect(selectElement) {
|
||||
function loadAllHerbsForSelect(selectElement, initialValue) {
|
||||
$.get('/api/herbs/masters', function(response) {
|
||||
if (response.success) {
|
||||
selectElement.empty().append('<option value="">약재 선택</option>');
|
||||
@ -2087,10 +2385,105 @@ $(document).ready(function() {
|
||||
}
|
||||
selectElement.append(`<option value="${herb.ingredient_code}" data-herb-name="${herb.herb_name}">${displayName}</option>`);
|
||||
});
|
||||
if (initialValue) {
|
||||
selectElement.val(initialValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 입고장 수정 모드
|
||||
function editReceipt(receiptId) {
|
||||
$.get(`/api/purchase-receipts/${receiptId}`, function(response) {
|
||||
if (!response.success) {
|
||||
alert('입고장 데이터를 불러올 수 없습니다: ' + response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const receipt = response.data;
|
||||
const modal = $('#manualReceiptModal');
|
||||
|
||||
// 수정 모드 플래그 설정
|
||||
modal.data('edit-mode', true);
|
||||
modal.data('receipt-id', receiptId);
|
||||
|
||||
// 모달 제목 변경
|
||||
modal.find('.modal-title').html('<i class="bi bi-pencil"></i> 입고장 수정');
|
||||
modal.find('.modal-header').removeClass('bg-success').addClass('bg-warning');
|
||||
|
||||
// 헤더 정보 채우기
|
||||
$('#manualReceiptDate').val(receipt.receipt_date);
|
||||
$('#manualReceiptSupplier').val(receipt.supplier_id);
|
||||
$('#manualReceiptSupplier').prop('disabled', true);
|
||||
$('#manualReceiptNotes').val(receipt.notes || '');
|
||||
|
||||
// 품목 추가 버튼 숨김
|
||||
$('#addManualReceiptLineBtn').hide();
|
||||
|
||||
// 기존 라인 비우기
|
||||
$('#manualReceiptLines').empty();
|
||||
manualReceiptLineCount = 0;
|
||||
|
||||
// 저장 버튼 텍스트 변경
|
||||
$('#saveManualReceiptBtn').html('<i class="bi bi-check-circle"></i> 수정 저장');
|
||||
|
||||
// 기존 라인 데이터로 행 채우기
|
||||
receipt.lines.forEach(line => {
|
||||
manualReceiptLineCount++;
|
||||
const row = `
|
||||
<tr data-row="${manualReceiptLineCount}" data-line-id="${line.line_id}">
|
||||
<td>
|
||||
<select class="form-control form-control-sm manual-herb-select" disabled>
|
||||
<option value="">약재 선택</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm manual-qty-input text-end"
|
||||
min="0.1" step="0.1" value="${line.quantity_g || ''}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm manual-price-input text-end"
|
||||
min="0" step="0.1" value="${line.unit_price_per_g || ''}">
|
||||
</td>
|
||||
<td class="text-end manual-line-total">${(line.line_total || 0).toLocaleString('ko-KR')}</td>
|
||||
<td>
|
||||
<select class="form-control form-control-sm manual-origin-input">
|
||||
<option value="">원산지 선택</option>
|
||||
${['한국','중국','베트남','인도','태국','페루','일본','기타'].map(c =>
|
||||
`<option value="${c}" ${(line.origin_country || '') === c ? 'selected' : ''}>${c}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm manual-lot-input"
|
||||
placeholder="로트번호" value="${line.lot_number || ''}">
|
||||
</td>
|
||||
<td>
|
||||
<input type="date" class="form-control form-control-sm manual-expiry-input"
|
||||
value="${line.expiry_date || ''}">
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>`;
|
||||
$('#manualReceiptLines').append(row);
|
||||
|
||||
const newRow = $(`#manualReceiptLines tr[data-row="${manualReceiptLineCount}"]`);
|
||||
|
||||
// 약재 select에 옵션 로드 후 기존 값 선택
|
||||
loadAllHerbsForSelect(newRow.find('.manual-herb-select'), line.ingredient_code);
|
||||
|
||||
// 금액 자동 계산 이벤트
|
||||
newRow.find('.manual-qty-input, .manual-price-input').on('input', function() {
|
||||
updateManualReceiptLineTotals();
|
||||
});
|
||||
});
|
||||
|
||||
updateManualReceiptLineTotals();
|
||||
|
||||
// 모달 열기 (show.bs.modal 이벤트 비활성화를 위해 직접 설정)
|
||||
modal.modal('show');
|
||||
});
|
||||
}
|
||||
|
||||
let manualReceiptLineCount = 0;
|
||||
|
||||
function addManualReceiptLine() {
|
||||
@ -2112,7 +2505,17 @@ $(document).ready(function() {
|
||||
</td>
|
||||
<td class="text-end manual-line-total">0</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm manual-origin-input" placeholder="예: 중국">
|
||||
<select class="form-control form-control-sm manual-origin-input">
|
||||
<option value="">원산지 선택</option>
|
||||
<option value="한국">한국</option>
|
||||
<option value="중국">중국</option>
|
||||
<option value="베트남">베트남</option>
|
||||
<option value="인도">인도</option>
|
||||
<option value="태국">태국</option>
|
||||
<option value="페루">페루</option>
|
||||
<option value="일본">일본</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm manual-lot-input" placeholder="로트번호">
|
||||
@ -2158,8 +2561,20 @@ $(document).ready(function() {
|
||||
$('#manualReceiptTotalAmount').text(totalAmount.toLocaleString('ko-KR'));
|
||||
}
|
||||
|
||||
// 모달 열릴 때 초기화
|
||||
// 모달 열릴 때 초기화 (수정 모드가 아닐 때만)
|
||||
$('#manualReceiptModal').on('show.bs.modal', function() {
|
||||
const modal = $(this);
|
||||
if (modal.data('edit-mode')) {
|
||||
// 수정 모드에서는 초기화하지 않음 (editReceipt에서 이미 설정함)
|
||||
return;
|
||||
}
|
||||
// 새 입고 모드 초기화
|
||||
modal.find('.modal-title').html('<i class="bi bi-plus-circle"></i> 수동 입고');
|
||||
modal.find('.modal-header').removeClass('bg-warning').addClass('bg-success');
|
||||
$('#saveManualReceiptBtn').html('<i class="bi bi-check-circle"></i> 입고 저장');
|
||||
$('#addManualReceiptLineBtn').show();
|
||||
$('#manualReceiptSupplier').prop('disabled', false);
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
$('#manualReceiptDate').val(today);
|
||||
$('#manualReceiptSupplier').val('');
|
||||
@ -2170,6 +2585,14 @@ $(document).ready(function() {
|
||||
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();
|
||||
@ -2192,8 +2615,12 @@ $(document).ready(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();
|
||||
@ -2230,14 +2657,21 @@ $(document).ready(function() {
|
||||
return false;
|
||||
}
|
||||
|
||||
lines.push({
|
||||
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;
|
||||
@ -2249,33 +2683,63 @@ $(document).ready(function() {
|
||||
const btn = $(this);
|
||||
btn.prop('disabled', true).text('저장 중...');
|
||||
|
||||
$.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);
|
||||
if (isEditMode) {
|
||||
// 수정 모드: PUT bulk
|
||||
$.ajax({
|
||||
url: `/api/purchase-receipts/${receiptId}/bulk`,
|
||||
method: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
notes: notes,
|
||||
lines: lines
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('입고장이 수정되었습니다.');
|
||||
$('#manualReceiptModal').modal('hide');
|
||||
loadPurchaseReceipts();
|
||||
} else {
|
||||
alert('오류: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
const msg = xhr.responseJSON ? xhr.responseJSON.error : '서버 오류가 발생했습니다.';
|
||||
alert('오류: ' + msg);
|
||||
},
|
||||
complete: function() {
|
||||
btn.prop('disabled', false).html('<i class="bi bi-check-circle"></i> 수정 저장');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
const msg = xhr.responseJSON ? xhr.responseJSON.error : '서버 오류가 발생했습니다.';
|
||||
alert('오류: ' + msg);
|
||||
},
|
||||
complete: function() {
|
||||
btn.prop('disabled', false).html('<i class="bi bi-check-circle"></i> 입고 저장');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 새 입고 모드: POST
|
||||
$.ajax({
|
||||
url: '/api/purchase-receipts/manual',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
supplier_id: supplierId,
|
||||
receipt_date: receiptDate,
|
||||
notes: notes,
|
||||
lines: lines
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert(`수동 입고 완료!\n입고번호: ${response.receipt_no}\n품목 수: ${response.summary.item_count}\n총 금액: ${response.summary.total_amount}`);
|
||||
$('#manualReceiptModal').modal('hide');
|
||||
loadPurchaseReceipts();
|
||||
} else {
|
||||
alert('오류: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
const msg = xhr.responseJSON ? xhr.responseJSON.error : '서버 오류가 발생했습니다.';
|
||||
alert('오류: ' + msg);
|
||||
},
|
||||
complete: function() {
|
||||
btn.prop('disabled', false).html('<i class="bi bi-check-circle"></i> 입고 저장');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 입고장 업로드
|
||||
@ -2635,6 +3099,14 @@ $(document).ready(function() {
|
||||
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';
|
||||
|
||||
@ -355,13 +355,17 @@
|
||||
|
||||
<!-- Formulas Page -->
|
||||
<div id="formulas" class="main-content">
|
||||
<!-- 내 처방 목록 -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3>처방 관리</h3>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#formulaModal">
|
||||
<i class="bi bi-plus-circle"></i> 새 처방 등록
|
||||
</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="bi bi-journal-medical"></i> 내 처방 목록</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
@ -380,6 +384,35 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 100처방 원방 마스터 -->
|
||||
<div class="card">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><i class="bi bi-book"></i> 100처방 원방 마스터 <span class="badge bg-secondary" id="officialFormulaCount">0</span></h6>
|
||||
<div style="width: 300px;">
|
||||
<input type="text" class="form-control form-control-sm" id="officialFormulaSearch"
|
||||
placeholder="처방명/출전 검색...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 500px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="table-light" style="position: sticky; top: 0; z-index: 1;">
|
||||
<tr>
|
||||
<th width="60">연번</th>
|
||||
<th>처방명</th>
|
||||
<th>한자명</th>
|
||||
<th>출전</th>
|
||||
<th width="80">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="officialFormulasList">
|
||||
<!-- Dynamic content -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compound Page -->
|
||||
@ -1667,6 +1700,78 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 100처방 상세/참고자료 모달 -->
|
||||
<div class="modal fade" id="officialFormulaModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-info text-white">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-book"></i>
|
||||
<span id="ofModalNumber"></span>. <span id="ofModalName"></span>
|
||||
<small id="ofModalHanja" class="ms-2"></small>
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">출전</label>
|
||||
<p id="ofModalSource" class="text-muted">-</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">한자명</label>
|
||||
<input type="text" class="form-control form-control-sm" id="ofEditHanja" placeholder="예: 加味溫膽湯">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">설명/효능</label>
|
||||
<textarea class="form-control" id="ofEditDescription" rows="2" placeholder="처방의 주요 효능..."></textarea>
|
||||
</div>
|
||||
<!-- 원방 구성 약재 -->
|
||||
<div class="mb-3" id="ofIngredientsSection" style="display:none;">
|
||||
<label class="form-label fw-bold">
|
||||
<i class="bi bi-list-ul"></i> 원방 구성
|
||||
<span class="badge bg-secondary ms-1" id="ofIngredientCount">0</span>종
|
||||
<span class="badge bg-primary ms-1" id="ofTotalGrams">0</span>g/첩
|
||||
</label>
|
||||
<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light" style="position: sticky; top: 0;">
|
||||
<tr>
|
||||
<th width="40">#</th>
|
||||
<th>약재명</th>
|
||||
<th width="80" class="text-end">첩당</th>
|
||||
<th>역할</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ofIngredientsList">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">
|
||||
<i class="bi bi-lightbulb"></i> 상담 참고자료
|
||||
<small class="text-muted fw-normal">(OTC 대비 차별점, 구성 해설, 업셀링 포인트 등)</small>
|
||||
</label>
|
||||
<textarea class="form-control" id="ofEditReferenceNotes" rows="10"
|
||||
placeholder="예: 경악전서의 가미패독산은 일반 패독산과 달리... • OTC 대비 차별점 • 구성 약재 해설 • 적응증 상세 • 상담 시 활용 포인트"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success" id="createFromOfficialBtn">
|
||||
<i class="bi bi-plus-circle"></i> 내 처방으로 등록
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
<button type="button" class="btn btn-info text-white" id="saveOfficialFormulaBtn">
|
||||
<i class="bi bi-check-lg"></i> 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formula Detail Modal (처방 상세 모달) -->
|
||||
<div class="modal fade" id="formulaDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user