feat(반품관리): 위치 지정 기능 추가
- 위치 뱃지 클릭 시 위치 수정 모달 표시 - '미지정' 뱃지 스타일 (점선 테두리, 클릭 유도) - 기존 위치 선택 드롭다운 + 직접 입력 가능 - 위치 삭제 기능 - products 페이지와 동일한 API 재활용 (/api/locations, /api/drugs/.../location) - 다크 테마에 맞는 모달 스타일 - Edit 툴로 부분 수정하여 인코딩 유지
This commit is contained in:
parent
e82f4be4af
commit
a7bcf46aaa
@ -327,8 +327,39 @@
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.location-empty { color: var(--text-muted); font-size: 12px; }
|
||||
.location-badge:hover { background: rgba(251, 191, 36, 0.4); transform: scale(1.05); }
|
||||
.location-badge.unset {
|
||||
background: rgba(100, 116, 139, 0.2);
|
||||
color: var(--text-muted);
|
||||
border: 1px dashed var(--border);
|
||||
}
|
||||
.location-badge.unset:hover { background: rgba(168, 85, 247, 0.2); border-color: var(--accent-purple); color: var(--accent-purple); }
|
||||
|
||||
/* 위치 모달 */
|
||||
.location-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 2000; align-items: center; justify-content: center; backdrop-filter: blur(4px); }
|
||||
.location-modal.show { display: flex; }
|
||||
.location-modal-content { background: var(--bg-card); border-radius: 16px; padding: 24px; max-width: 400px; width: 90%; border: 1px solid var(--border); box-shadow: 0 20px 60px rgba(0,0,0,0.5); animation: locModalIn 0.2s ease; }
|
||||
@keyframes locModalIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.location-modal-content h3 { margin: 0 0 16px 0; color: var(--accent-amber); font-size: 18px; display: flex; align-items: center; gap: 8px; }
|
||||
.location-product-info { background: var(--bg-secondary); border-radius: 8px; padding: 12px; margin-bottom: 16px; border: 1px solid var(--border); }
|
||||
.location-product-info .name { font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
|
||||
.location-product-info .code { font-size: 12px; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; }
|
||||
.location-select-wrapper, .location-input-wrapper { margin-bottom: 12px; }
|
||||
.location-select-wrapper label, .location-input-wrapper label { display: block; font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-bottom: 6px; }
|
||||
.location-select, .location-input { width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; font-family: inherit; background: var(--bg-primary); color: var(--text-primary); transition: border-color 0.2s; }
|
||||
.location-select:focus, .location-input:focus { outline: none; border-color: var(--accent-amber); }
|
||||
.location-hint { font-size: 11px; color: var(--text-muted); margin-top: 6px; }
|
||||
.location-modal-btns { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||
.location-modal-btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; font-size: 14px; transition: all 0.15s; }
|
||||
.location-modal-btn.secondary { background: var(--bg-secondary); color: var(--text-secondary); }
|
||||
.location-modal-btn.secondary:hover { background: var(--bg-card-hover); }
|
||||
.location-modal-btn.danger { background: rgba(244, 63, 94, 0.2); color: var(--accent-rose); }
|
||||
.location-modal-btn.danger:hover { background: rgba(244, 63, 94, 0.3); }
|
||||
.location-modal-btn.primary { background: linear-gradient(135deg, var(--accent-amber), #d97706); color: #fff; }
|
||||
.location-modal-btn.primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4); }
|
||||
|
||||
/* 긴급도 배지 */
|
||||
.urgency-badge {
|
||||
@ -920,6 +951,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 위치 모달 -->
|
||||
<div class="location-modal" id="locationModal">
|
||||
<div class="location-modal-content">
|
||||
<h3>📍 위치 설정</h3>
|
||||
<div class="location-product-info">
|
||||
<div class="name" id="locModalProductName">제품명</div>
|
||||
<div class="code" id="locModalDrugCode">상품코드</div>
|
||||
</div>
|
||||
<div class="location-select-wrapper">
|
||||
<label>기존 위치에서 선택</label>
|
||||
<select class="location-select" id="locationSelect" onchange="onLocationSelectChange()">
|
||||
<option value="">-- 선택하세요 --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="location-input-wrapper">
|
||||
<label>또는 직접 입력</label>
|
||||
<input type="text" class="location-input" id="locationInput" placeholder="예: 진열대1-3, 냉장고, 창고A" maxlength="20">
|
||||
<div class="location-hint">최대 20자 / 새 위치를 입력하면 목록에 추가됩니다</div>
|
||||
</div>
|
||||
<div class="location-modal-btns">
|
||||
<button class="location-modal-btn danger" onclick="clearLocation()" id="locClearBtn" style="display:none;">삭제</button>
|
||||
<div style="flex:1;"></div>
|
||||
<button class="location-modal-btn secondary" onclick="closeLocationModal()">취소</button>
|
||||
<button class="location-modal-btn primary" onclick="saveLocation()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allData = [];
|
||||
let currentPage = 1;
|
||||
@ -1059,7 +1118,11 @@
|
||||
<span class="drug-code">${item.drug_code}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="location-cell">${item.location ? `<span class="location-badge">${escapeHtml(item.location)}</span>` : '<span class="location-empty">-</span>'}</td>
|
||||
<td class="location-cell">
|
||||
${item.location
|
||||
? `<span class="location-badge" onclick="event.stopPropagation();openLocationModal('${item.drug_code}', '${escapeHtml(item.drug_name).replace(/'/g, "\\'")}', '${escapeHtml(item.location).replace(/'/g, "\\'")}')">${escapeHtml(item.location)}</span>`
|
||||
: `<span class="location-badge unset" onclick="event.stopPropagation();openLocationModal('${item.drug_code}', '${escapeHtml(item.drug_name).replace(/'/g, "\\'")}', '')">미지정</span>`}
|
||||
</td>
|
||||
<td class="qty-cell">${item.current_stock || 0}</td>
|
||||
<td class="amount-cell ${item.unit_price ? '' : 'zero'}">${formatPrice(item.unit_price)}</td>
|
||||
<td class="amount-cell ${hasAmount ? '' : 'zero'}">${hasAmount ? '₩' + Math.round(item.recoverable_amount).toLocaleString() : '-'}</td>
|
||||
@ -1284,6 +1347,91 @@
|
||||
document.getElementById('purchaseModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closePurchaseModal();
|
||||
});
|
||||
|
||||
// ══════════════════ 위치 모달 ══════════════════
|
||||
let locModalDrugCode = null;
|
||||
let locModalCurrentLocation = null;
|
||||
let allLocations = [];
|
||||
|
||||
async function openLocationModal(drugCode, productName, currentLocation) {
|
||||
locModalDrugCode = drugCode;
|
||||
locModalCurrentLocation = currentLocation;
|
||||
|
||||
document.getElementById('locModalProductName').textContent = productName;
|
||||
document.getElementById('locModalDrugCode').textContent = drugCode;
|
||||
document.getElementById('locationInput').value = currentLocation || '';
|
||||
|
||||
// 삭제 버튼 표시 (현재 위치가 있을 때만)
|
||||
document.getElementById('locClearBtn').style.display = currentLocation ? 'block' : 'none';
|
||||
|
||||
// 위치 목록 로드
|
||||
try {
|
||||
const res = await fetch('/api/locations');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
allLocations = data.locations || [];
|
||||
const select = document.getElementById('locationSelect');
|
||||
select.innerHTML = '<option value="">-- 선택하세요 --</option>' +
|
||||
allLocations.map(loc =>
|
||||
`<option value="${escapeHtml(loc)}" ${loc === currentLocation ? 'selected' : ''}>${escapeHtml(loc)}</option>`
|
||||
).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('위치 목록 로드 실패:', e);
|
||||
}
|
||||
|
||||
document.getElementById('locationModal').classList.add('show');
|
||||
document.getElementById('locationInput').focus();
|
||||
}
|
||||
|
||||
function closeLocationModal() {
|
||||
document.getElementById('locationModal').classList.remove('show');
|
||||
locModalDrugCode = null;
|
||||
locModalCurrentLocation = null;
|
||||
}
|
||||
|
||||
function onLocationSelectChange() {
|
||||
const selected = document.getElementById('locationSelect').value;
|
||||
if (selected) {
|
||||
document.getElementById('locationInput').value = selected;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLocation() {
|
||||
const newLocation = document.getElementById('locationInput').value.trim();
|
||||
|
||||
if (!locModalDrugCode) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/drugs/${locModalDrugCode}/location`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ location_name: newLocation })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(newLocation ? `✅ 위치가 "${newLocation}"(으)로 설정되었습니다` : '✅ 위치가 삭제되었습니다', 'success');
|
||||
closeLocationModal();
|
||||
loadData(); // 테이블 새로고침
|
||||
} else {
|
||||
showToast(data.error || '저장 실패', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('오류: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function clearLocation() {
|
||||
if (!confirm('위치 정보를 삭제하시겠습니까?')) return;
|
||||
document.getElementById('locationInput').value = '';
|
||||
await saveLocation();
|
||||
}
|
||||
|
||||
// 위치 모달 외부 클릭 시 닫기
|
||||
document.getElementById('locationModal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) closeLocationModal();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user