feat(flow-logs): fix text overlapping, show full asset code, filter by current month and support JSON logs
This commit is contained in:
625
src/components/Modal/PCFlowModal.ts
Normal file
625
src/components/Modal/PCFlowModal.ts
Normal file
@@ -0,0 +1,625 @@
|
||||
import { state, loadMasterDataFromDB } from '../../core/state';
|
||||
import { createIcons, Search, Monitor, RefreshCw } from 'lucide';
|
||||
import { API_BASE_URL } from '../../core/utils';
|
||||
|
||||
export class PCFlowModal {
|
||||
private static instance: PCFlowModal | null = null;
|
||||
|
||||
private modalEl: HTMLElement | null = null;
|
||||
private currentFlowType: 'checkout' | 'return' | 'move' = 'checkout';
|
||||
|
||||
// Selected state
|
||||
private selectedUser: any = null;
|
||||
private selectedTargetUser: any = null;
|
||||
private selectedPC: any = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): PCFlowModal {
|
||||
if (!PCFlowModal.instance) {
|
||||
PCFlowModal.instance = new PCFlowModal();
|
||||
}
|
||||
return PCFlowModal.instance;
|
||||
}
|
||||
|
||||
public init(onSave: () => void) {
|
||||
if (document.getElementById('pc-flow-modal')) return;
|
||||
|
||||
// Inject HTML
|
||||
document.body.insertAdjacentHTML('beforeend', this.renderHTML());
|
||||
|
||||
this.modalEl = document.getElementById('pc-flow-modal');
|
||||
this.setupEventListeners(onSave);
|
||||
|
||||
// Set default date to today
|
||||
const dateInput = document.getElementById('pc-flow-date') as HTMLInputElement;
|
||||
if (dateInput) {
|
||||
dateInput.value = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
createIcons({ icons: { Search, Monitor, RefreshCw } });
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.resetState();
|
||||
if (this.modalEl) {
|
||||
this.modalEl.classList.remove('hidden');
|
||||
}
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
public close() {
|
||||
if (this.modalEl) {
|
||||
this.modalEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
private resetState() {
|
||||
this.selectedUser = null;
|
||||
this.selectedTargetUser = null;
|
||||
this.selectedPC = null;
|
||||
this.currentFlowType = 'checkout';
|
||||
|
||||
const radioCheckout = document.querySelector('input[name="flow-type"][value="checkout"]') as HTMLInputElement;
|
||||
if (radioCheckout) radioCheckout.checked = true;
|
||||
|
||||
// Reset text fields
|
||||
const userSearch = document.getElementById('pc-flow-user-search') as HTMLInputElement;
|
||||
if (userSearch) userSearch.value = '';
|
||||
|
||||
const targetUserSearch = document.getElementById('pc-flow-target-user-search') as HTMLInputElement;
|
||||
if (targetUserSearch) targetUserSearch.value = '';
|
||||
|
||||
const stockSearch = document.getElementById('pc-flow-stock-search') as HTMLInputElement;
|
||||
if (stockSearch) stockSearch.value = '';
|
||||
|
||||
const details = document.getElementById('pc-flow-details') as HTMLTextAreaElement;
|
||||
if (details) details.value = '';
|
||||
}
|
||||
|
||||
private setupEventListeners(onSave: () => void) {
|
||||
const btnClose = document.getElementById('btn-close-pc-flow-modal');
|
||||
const btnCancel = document.getElementById('btn-cancel-pc-flow-modal');
|
||||
const btnSubmit = document.getElementById('btn-submit-pc-flow');
|
||||
|
||||
btnClose?.addEventListener('click', () => this.close());
|
||||
btnCancel?.addEventListener('click', () => this.close());
|
||||
|
||||
// Flow Type Radio Buttons
|
||||
const labels = document.querySelectorAll('.flow-type-label');
|
||||
labels.forEach(label => {
|
||||
const radio = label.querySelector('input[name="flow-type"]') as HTMLInputElement;
|
||||
label.addEventListener('click', () => {
|
||||
labels.forEach(l => l.classList.remove('active'));
|
||||
label.classList.add('active');
|
||||
radio.checked = true;
|
||||
this.currentFlowType = radio.value as any;
|
||||
|
||||
// Reset selected PC when switching flow types
|
||||
this.selectedPC = null;
|
||||
this.updateUI();
|
||||
});
|
||||
});
|
||||
|
||||
// 1. Source User Autocomplete Search
|
||||
const userSearch = document.getElementById('pc-flow-user-search') as HTMLInputElement;
|
||||
const userSuggestions = document.getElementById('pc-flow-user-suggestions')!;
|
||||
|
||||
userSearch?.addEventListener('input', () => {
|
||||
const query = userSearch.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
userSuggestions.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const users = state.masterData.users || [];
|
||||
const filtered = users.filter((u: any) =>
|
||||
(u.user_name && u.user_name.toLowerCase().includes(query)) ||
|
||||
(u.dept_name && u.dept_name.toLowerCase().includes(query)) ||
|
||||
(u.emp_no && u.emp_no.toString().includes(query))
|
||||
);
|
||||
|
||||
const uniqueFiltered: any[] = [];
|
||||
const seen = new Set();
|
||||
filtered.forEach((u: any) => {
|
||||
const key = u.emp_no || u.user_name;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
uniqueFiltered.push(u);
|
||||
}
|
||||
});
|
||||
|
||||
this.renderUserSuggestions(uniqueFiltered, userSuggestions, (user) => {
|
||||
this.selectedUser = user;
|
||||
userSearch.value = `${user.user_name} (${user.dept_name} / 사번:${user.emp_no || '-'})`;
|
||||
userSuggestions.classList.add('hidden');
|
||||
|
||||
// Automatically populate details if return or move
|
||||
if (this.currentFlowType === 'return' || this.currentFlowType === 'move') {
|
||||
this.selectedPC = null; // Reset selection
|
||||
}
|
||||
this.updateUI();
|
||||
});
|
||||
});
|
||||
|
||||
// Close suggestion overlays on clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('#pc-flow-user-search') && !target.closest('#pc-flow-user-suggestions')) {
|
||||
userSuggestions.classList.add('hidden');
|
||||
}
|
||||
if (!target.closest('#pc-flow-target-user-search') && !target.closest('#pc-flow-target-user-suggestions')) {
|
||||
const targetSuggestions = document.getElementById('pc-flow-target-user-suggestions');
|
||||
targetSuggestions?.classList.add('hidden');
|
||||
}
|
||||
if (!target.closest('#pc-flow-stock-search') && !target.closest('#pc-flow-stock-suggestions')) {
|
||||
const stockSuggestions = document.getElementById('pc-flow-stock-suggestions');
|
||||
stockSuggestions?.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Target User Autocomplete Search (For Moves)
|
||||
const targetUserSearch = document.getElementById('pc-flow-target-user-search') as HTMLInputElement;
|
||||
const targetSuggestions = document.getElementById('pc-flow-target-user-suggestions')!;
|
||||
|
||||
targetUserSearch?.addEventListener('input', () => {
|
||||
const query = targetUserSearch.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
targetSuggestions.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const users = state.masterData.users || [];
|
||||
const filtered = users.filter((u: any) =>
|
||||
(u.user_name && u.user_name.toLowerCase().includes(query)) ||
|
||||
(u.dept_name && u.dept_name.toLowerCase().includes(query)) ||
|
||||
(u.emp_no && u.emp_no.toString().includes(query))
|
||||
);
|
||||
|
||||
const uniqueFiltered: any[] = [];
|
||||
const seen = new Set();
|
||||
filtered.forEach((u: any) => {
|
||||
const key = u.emp_no || u.user_name;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
uniqueFiltered.push(u);
|
||||
}
|
||||
});
|
||||
|
||||
this.renderUserSuggestions(uniqueFiltered, targetSuggestions, (user) => {
|
||||
this.selectedTargetUser = user;
|
||||
targetUserSearch.value = `${user.user_name} (${user.dept_name} / 사번:${user.emp_no || '-'})`;
|
||||
targetSuggestions.classList.add('hidden');
|
||||
this.updateUI();
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Stock PC Autocomplete Search (For Checkout)
|
||||
const stockSearch = document.getElementById('pc-flow-stock-search') as HTMLInputElement;
|
||||
const stockSuggestions = document.getElementById('pc-flow-stock-suggestions')!;
|
||||
|
||||
const showStockSuggestions = () => {
|
||||
const query = stockSearch.value.trim().toLowerCase();
|
||||
|
||||
// Filter available PCs (category PC, status '대기' or '재고창고')
|
||||
const pcs = state.masterData.pc || [];
|
||||
const filtered = pcs.filter((p: any) => {
|
||||
const status = (p.hw_status || '').trim();
|
||||
const matchesQuery = !query ||
|
||||
(p.asset_code && p.asset_code.toLowerCase().includes(query)) ||
|
||||
(p.model_name && p.model_name.toLowerCase().includes(query)) ||
|
||||
(p.cpu && p.cpu.toLowerCase().includes(query));
|
||||
|
||||
return (status === '대기' || status === '재고창고' || status === '미할당') && matchesQuery;
|
||||
});
|
||||
|
||||
this.renderPCSuggestions(filtered, stockSuggestions, (pc) => {
|
||||
this.selectedPC = pc;
|
||||
stockSearch.value = `${pc.asset_code} - ${pc.model_name}`;
|
||||
stockSuggestions.classList.add('hidden');
|
||||
this.updateUI();
|
||||
});
|
||||
};
|
||||
|
||||
stockSearch?.addEventListener('input', showStockSuggestions);
|
||||
stockSearch?.addEventListener('focus', showStockSuggestions);
|
||||
stockSearch?.addEventListener('click', showStockSuggestions);
|
||||
|
||||
// 4. Submit Transaction
|
||||
btnSubmit?.addEventListener('click', async () => {
|
||||
if (!this.validateInputs()) return;
|
||||
|
||||
const dateVal = (document.getElementById('pc-flow-date') as HTMLInputElement).value;
|
||||
const detailsVal = (document.getElementById('pc-flow-details') as HTMLTextAreaElement).value.trim();
|
||||
const loginUser = state.currentUserRole === 'admin' ? '관리자' : '실무담당자';
|
||||
|
||||
// Build Details Message as JSON
|
||||
const logData = {
|
||||
type: this.currentFlowType,
|
||||
user: this.selectedUser ? this.selectedUser.user_name : '',
|
||||
dept: this.selectedUser ? this.selectedUser.dept_name : '',
|
||||
targetUser: this.selectedTargetUser ? this.selectedTargetUser.user_name : '',
|
||||
targetDept: this.selectedTargetUser ? this.selectedTargetUser.dept_name : '',
|
||||
assetCode: this.selectedPC ? this.selectedPC.asset_code : '',
|
||||
memo: detailsVal
|
||||
};
|
||||
const finalDetails = JSON.stringify(logData);
|
||||
|
||||
const payload: any = {
|
||||
action: this.currentFlowType,
|
||||
assetId: this.selectedPC.id,
|
||||
date: dateVal,
|
||||
details: finalDetails,
|
||||
manager: loginUser
|
||||
};
|
||||
|
||||
if (this.currentFlowType === 'checkout') {
|
||||
payload.userName = this.selectedUser.user_name;
|
||||
payload.dept = this.selectedUser.dept_name;
|
||||
payload.empNo = this.selectedUser.emp_no;
|
||||
payload.position = this.selectedUser.position || '사원';
|
||||
} else if (this.currentFlowType === 'move') {
|
||||
payload.userName = this.selectedTargetUser.user_name;
|
||||
payload.dept = this.selectedTargetUser.dept_name;
|
||||
payload.empNo = this.selectedTargetUser.emp_no;
|
||||
payload.position = this.selectedTargetUser.position || '사원';
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/pc/flow`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('PC 이동/반납 처리가 완료되었습니다.');
|
||||
this.close();
|
||||
onSave(); // Refresh views
|
||||
} else {
|
||||
const errData = await response.json();
|
||||
alert(`오류 발생: ${errData.error || '처리 실패'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('API Error:', err);
|
||||
alert('서버 전송 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private validateInputs(): boolean {
|
||||
if (this.currentFlowType === 'checkout') {
|
||||
if (!this.selectedUser) { alert('대상 사원을 선택해주세요.'); return false; }
|
||||
if (!this.selectedPC) { alert('불출할 재고 PC를 선택해주세요.'); return false; }
|
||||
} else if (this.currentFlowType === 'return') {
|
||||
if (!this.selectedUser) { alert('반납 대상 사원을 선택해주세요.'); return false; }
|
||||
if (!this.selectedPC) { alert('반납할 PC 자산을 선택해주세요.'); return false; }
|
||||
} else if (this.currentFlowType === 'move') {
|
||||
if (!this.selectedUser) { alert('인계 사원을 선택해주세요.'); return false; }
|
||||
if (!this.selectedPC) { alert('이동할 PC 자산을 선택해주세요.'); return false; }
|
||||
if (!this.selectedTargetUser) { alert('인수 사원을 선택해주세요.'); return false; }
|
||||
if (this.selectedUser.emp_no === this.selectedTargetUser.emp_no) {
|
||||
alert('인계자와 인수자는 동일할 수 없습니다.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private renderUserSuggestions(users: any[], container: HTMLElement, onSelect: (user: any) => void) {
|
||||
container.innerHTML = '';
|
||||
if (users.length === 0) {
|
||||
container.innerHTML = '<div style="padding: 10px; color: var(--text-muted); font-size: 13px;">일치하는 사원이 없습니다.</div>';
|
||||
container.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
users.forEach(u => {
|
||||
const item = document.createElement('div');
|
||||
item.style.padding = '8px 12px';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.fontSize = '13px';
|
||||
item.style.borderBottom = '1px solid #F3F4F6';
|
||||
item.className = 'suggestion-item';
|
||||
item.innerHTML = `
|
||||
<div style="font-weight: 700; color: var(--text-main);">${u.user_name}</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted); display: flex; gap: 8px;">
|
||||
<span>부서: ${u.dept_name}</span>
|
||||
<span>|</span>
|
||||
<span>사번: ${u.emp_no || '-'}</span>
|
||||
</div>
|
||||
`;
|
||||
item.addEventListener('click', () => onSelect(u));
|
||||
container.appendChild(item);
|
||||
});
|
||||
container.classList.remove('hidden');
|
||||
}
|
||||
|
||||
private renderPCSuggestions(pcs: any[], container: HTMLElement, onSelect: (pc: any) => void) {
|
||||
container.innerHTML = '';
|
||||
if (pcs.length === 0) {
|
||||
container.innerHTML = '<div style="padding: 10px; color: var(--text-muted); font-size: 13px;">불출 가능한 대기 PC 재고가 없습니다.</div>';
|
||||
container.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
pcs.forEach(p => {
|
||||
const item = document.createElement('div');
|
||||
item.style.padding = '8px 12px';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.fontSize = '13px';
|
||||
item.style.borderBottom = '1px solid #F3F4F6';
|
||||
item.className = 'suggestion-item';
|
||||
item.innerHTML = `
|
||||
<div style="font-weight: 700; color: var(--primary-color);">${p.asset_code} (${p.model_name || '모델명 없음'})</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted);">
|
||||
사양: CPU ${p.cpu || '-'} / RAM ${p.ram || '-'} / 위치: ${p.location || '-'}
|
||||
</div>
|
||||
`;
|
||||
item.addEventListener('click', () => onSelect(p));
|
||||
container.appendChild(item);
|
||||
});
|
||||
container.classList.remove('hidden');
|
||||
}
|
||||
|
||||
private updateUI() {
|
||||
// 1. Hide/Show dynamic sections based on flow type
|
||||
const stockContainer = document.getElementById('stock-pc-search-container')!;
|
||||
const targetUserContainer = document.getElementById('target-user-search-container')!;
|
||||
const userPcsContainer = document.getElementById('user-pcs-container')!;
|
||||
const labelStep2 = document.getElementById('user-search-label')!;
|
||||
|
||||
if (this.currentFlowType === 'checkout') {
|
||||
stockContainer.classList.remove('hidden');
|
||||
targetUserContainer.classList.add('hidden');
|
||||
userPcsContainer.classList.add('hidden');
|
||||
labelStep2.textContent = '2. 불출 대상 사원 검색';
|
||||
} else if (this.currentFlowType === 'return') {
|
||||
stockContainer.classList.add('hidden');
|
||||
targetUserContainer.classList.add('hidden');
|
||||
userPcsContainer.classList.remove('hidden');
|
||||
labelStep2.textContent = '2. 반납 대상 사원 검색';
|
||||
} else if (this.currentFlowType === 'move') {
|
||||
stockContainer.classList.add('hidden');
|
||||
targetUserContainer.classList.remove('hidden');
|
||||
userPcsContainer.classList.remove('hidden');
|
||||
labelStep2.textContent = '2. 인계 사원 검색';
|
||||
}
|
||||
|
||||
// 2. Update summary panels on the right
|
||||
const summaryUserName = document.getElementById('summary-user-name')!;
|
||||
const summaryUserDept = document.getElementById('summary-user-dept')!;
|
||||
if (this.selectedUser) {
|
||||
summaryUserName.textContent = this.selectedUser.user_name;
|
||||
summaryUserDept.textContent = `${this.selectedUser.dept_name} / 사번: ${this.selectedUser.emp_no || '-'}`;
|
||||
} else {
|
||||
summaryUserName.textContent = '선택된 사원 없음';
|
||||
summaryUserDept.textContent = '-';
|
||||
}
|
||||
|
||||
const summaryTargetCard = document.getElementById('summary-target-user-card')!;
|
||||
const summaryTargetUserName = document.getElementById('summary-target-user-name')!;
|
||||
const summaryTargetUserDept = document.getElementById('summary-target-user-dept')!;
|
||||
if (this.currentFlowType === 'move') {
|
||||
summaryTargetCard.classList.remove('hidden');
|
||||
if (this.selectedTargetUser) {
|
||||
summaryTargetUserName.textContent = this.selectedTargetUser.user_name;
|
||||
summaryTargetUserDept.textContent = `${this.selectedTargetUser.dept_name} / 사번: ${this.selectedTargetUser.emp_no || '-'}`;
|
||||
} else {
|
||||
summaryTargetUserName.textContent = '선택된 사원 없음';
|
||||
summaryTargetUserDept.textContent = '-';
|
||||
}
|
||||
} else {
|
||||
summaryTargetCard.classList.add('hidden');
|
||||
}
|
||||
|
||||
const summaryPcCode = document.getElementById('summary-pc-code')!;
|
||||
const summaryPcModel = document.getElementById('summary-pc-model')!;
|
||||
if (this.selectedPC) {
|
||||
summaryPcCode.textContent = this.selectedPC.asset_code;
|
||||
summaryPcModel.textContent = `${this.selectedPC.model_name || '모델명 없음'} (${this.selectedPC.cpu || '-'} / ${this.selectedPC.ram || '-'})`;
|
||||
} else {
|
||||
summaryPcCode.textContent = '선택된 PC 없음';
|
||||
summaryPcModel.textContent = '-';
|
||||
}
|
||||
|
||||
// 3. Render user's active PCs list on the right (For Return & Move)
|
||||
const userPcsList = document.getElementById('user-pcs-list')!;
|
||||
if (this.selectedUser && (this.currentFlowType === 'return' || this.currentFlowType === 'move')) {
|
||||
const allPcs = state.masterData.pc || [];
|
||||
const userPcs = allPcs.filter((p: any) =>
|
||||
(p.emp_no && p.emp_no.toString() === this.selectedUser.emp_no?.toString()) ||
|
||||
(p.user_current && p.user_current === this.selectedUser.user_name)
|
||||
);
|
||||
|
||||
if (userPcs.length === 0) {
|
||||
userPcsList.innerHTML = '<div style="font-size: 12px; color: var(--text-muted); padding: 8px 0;">이 사용자가 소유한 PC 자산이 없습니다.</div>';
|
||||
} else {
|
||||
userPcsList.innerHTML = userPcs.map(p => {
|
||||
const isSelected = this.selectedPC && this.selectedPC.id === p.id;
|
||||
return `
|
||||
<div class="user-pc-item ${isSelected ? 'selected' : ''}" data-id="${p.id}" style="padding: 10px; border: 1px solid ${isSelected ? 'var(--primary-color)' : 'var(--border-color)'}; border-radius: 4px; cursor: pointer; background: ${isSelected ? 'var(--primary-light)' : 'white'}; transition: all 0.2s;">
|
||||
<div style="font-weight: 700; font-size: 13px; color: ${isSelected ? 'var(--primary-color)' : 'var(--text-main)'};">${p.asset_code}</div>
|
||||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">
|
||||
${p.model_name || '모델명 없음'} | CPU: ${p.cpu || '-'} | RAM: ${p.ram || '-'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Bind clicks to list items
|
||||
userPcsList.querySelectorAll('.user-pc-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const pcId = item.getAttribute('data-id');
|
||||
const foundPC = userPcs.find(p => p.id === pcId);
|
||||
if (foundPC) {
|
||||
this.selectedPC = foundPC;
|
||||
this.updateUI();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
userPcsList.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
private renderHTML(): string {
|
||||
const overlayStyle = `
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4); display: flex; align-items: center; justify-content: center;
|
||||
z-index: 1000; transition: opacity 0.3s;
|
||||
`;
|
||||
const contentStyle = `
|
||||
background: white; border-radius: 12px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden; max-height: 90vh; width: 950px; display: flex; flex-direction: column;
|
||||
`;
|
||||
const labelStyle = 'display: block; font-size: 13px; font-weight: 700; color: var(--text-muted); margin-bottom: 8px;';
|
||||
const inputStyle = 'width: 100%; height: 38px; padding: 0 12px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 13px; outline: none; box-sizing: border-box;';
|
||||
const inputWithIconStyle = 'width: 100%; height: 38px; padding: 0 12px 0 36px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 13px; outline: none; box-sizing: border-box;';
|
||||
|
||||
return `
|
||||
<div id="pc-flow-modal" class="modal-overlay hidden" style="${overlayStyle}">
|
||||
<div class="modal-content" style="${contentStyle}">
|
||||
|
||||
<div class="modal-header" style="background: var(--primary-color); padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color);">
|
||||
<h2 style="margin: 0; font-size: 18px; font-weight: 800; color: white; display: flex; align-items: center; gap: 8px;">
|
||||
<i data-lucide="refresh-cw"></i> PC 이동/반납 (불출/반납/이동)
|
||||
</h2>
|
||||
<button id="btn-close-pc-flow-modal" class="btn-icon" aria-label="닫기" style="font-size: 28px; color: white; background: none; border: none; cursor: pointer; line-height: 1;">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" style="padding: 24px; overflow-y: auto; display: flex; gap: 24px;">
|
||||
<!-- 왼쪽 영역: 입력 폼 -->
|
||||
<div style="flex: 1.2; display: flex; flex-direction: column; gap: 20px;">
|
||||
|
||||
<!-- 1. 처리 유형 -->
|
||||
<div>
|
||||
<label style="${labelStyle}">1. 처리 유형 선택</label>
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<label class="flow-type-label active" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">
|
||||
<input type="radio" name="flow-type" value="checkout" checked style="display:none;" />
|
||||
불출 (지급)
|
||||
</label>
|
||||
<label class="flow-type-label" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">
|
||||
<input type="radio" name="flow-type" value="return" style="display:none;" />
|
||||
입고 (반납)
|
||||
</label>
|
||||
<label class="flow-type-label" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">
|
||||
<input type="radio" name="flow-type" value="move" style="display:none;" />
|
||||
이동 (이관)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 대상 사용자 검색 -->
|
||||
<div style="position: relative;">
|
||||
<label id="user-search-label" style="${labelStyle}">2. 대상 사원 검색</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="text" id="pc-flow-user-search" placeholder="사원명, 부서, 사번 검색..." style="${inputWithIconStyle}" />
|
||||
<i data-lucide="search" style="position: absolute; left: 10px; width: 16px; height: 16px; color: var(--text-muted);"></i>
|
||||
</div>
|
||||
<div id="pc-flow-user-suggestions" class="hidden" style="position: absolute; top: 100%; left: 0; right: 0; max-height: 200px; overflow-y: auto; background: white; border: 1px solid var(--border-color); border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 1000; margin-top: 4px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 새 인수자 검색 (이동 시 노출) -->
|
||||
<div id="target-user-search-container" class="hidden" style="position: relative;">
|
||||
<label style="${labelStyle}">새 인수 사원 검색</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="text" id="pc-flow-target-user-search" placeholder="사원명, 부서, 사번 검색..." style="${inputWithIconStyle}" />
|
||||
<i data-lucide="search" style="position: absolute; left: 10px; width: 16px; height: 16px; color: var(--text-muted);"></i>
|
||||
</div>
|
||||
<div id="pc-flow-target-user-suggestions" class="hidden" style="position: absolute; top: 100%; left: 0; right: 0; max-height: 200px; overflow-y: auto; background: white; border: 1px solid var(--border-color); border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 1000; margin-top: 4px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 재고 PC 검색 (불출 시 노출) -->
|
||||
<div id="stock-pc-search-container" style="position: relative;">
|
||||
<label style="${labelStyle}">3. 불출할 재고 PC 선택</label>
|
||||
<div style="position: relative; display: flex; align-items: center;">
|
||||
<input type="text" id="pc-flow-stock-search" placeholder="자산코드 또는 모델명 검색..." style="${inputWithIconStyle}" />
|
||||
<i data-lucide="monitor" style="position: absolute; left: 10px; width: 16px; height: 16px; color: var(--text-muted);"></i>
|
||||
</div>
|
||||
<div id="pc-flow-stock-suggestions" class="hidden" style="position: absolute; top: 100%; left: 0; right: 0; max-height: 200px; overflow-y: auto; background: white; border: 1px solid var(--border-color); border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); z-index: 1000; margin-top: 4px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 5. 상세 공통 입력 -->
|
||||
<div style="display: flex; gap: 16px;">
|
||||
<div style="flex: 1;">
|
||||
<label style="${labelStyle.replace('margin-bottom: 8px;', 'margin-bottom: 6px;')}">처리 일자</label>
|
||||
<input type="date" id="pc-flow-date" style="${inputStyle}" />
|
||||
</div>
|
||||
<div style="flex: 2;">
|
||||
<label style="${labelStyle.replace('margin-bottom: 8px;', 'margin-bottom: 6px;')}">상세 사유</label>
|
||||
<textarea id="pc-flow-details" rows="2" placeholder="미입력 시 기본 문구로 자동 입력됩니다." style="width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; font-family: inherit; font-size: 13px; resize: none; box-sizing: border-box; outline: none;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 오른쪽 영역: 선택 요약 & 사원 소유 자산 목록 -->
|
||||
<div style="flex: 0.8; border-left: 1px solid var(--border-color); padding-left: 24px; display: flex; flex-direction: column; gap: 16px;">
|
||||
<h3 style="margin: 0; font-size: 14px; font-weight: 800; border-bottom: 1px solid var(--border-color); padding-bottom: 8px;">선택 내역 요약</h3>
|
||||
|
||||
<!-- 사원 요약 카드 -->
|
||||
<div id="summary-user-card" style="padding: 12px; background: var(--bg-light); border: 1px solid var(--border-color); border-radius: 6px; display: flex; flex-direction: column; gap: 4px;">
|
||||
<div style="font-size: 11px; color: var(--text-muted);">대상 사원</div>
|
||||
<div id="summary-user-name" style="font-weight: 700; font-size: 14px;">선택된 사원 없음</div>
|
||||
<div id="summary-user-dept" style="font-size: 12px; color: var(--text-muted);">-</div>
|
||||
</div>
|
||||
|
||||
<!-- 인수 사원 요약 카드 (이동 전용) -->
|
||||
<div id="summary-target-user-card" class="summary-card hidden" style="padding: 12px; background: #EEF2F6; border: 1px solid var(--border-color); border-radius: 6px; display: flex; flex-direction: column; gap: 4px;">
|
||||
<div style="font-size: 11px; color: var(--text-muted);">새 인수 사원</div>
|
||||
<div id="summary-target-user-name" style="font-weight: 700; font-size: 14px;">선택된 사원 없음</div>
|
||||
<div id="summary-target-user-dept" style="font-size: 12px; color: var(--text-muted);">-</div>
|
||||
</div>
|
||||
|
||||
<!-- 대상 PC 자산 요약 카드 -->
|
||||
<div id="summary-pc-card" style="padding: 12px; background: var(--bg-light); border: 1px solid var(--border-color); border-radius: 6px; display: flex; flex-direction: column; gap: 4px;">
|
||||
<div style="font-size: 11px; color: var(--text-muted);">대상 PC 자산</div>
|
||||
<div id="summary-pc-code" style="font-weight: 700; font-size: 14px; color: var(--primary-color);">선택된 PC 없음</div>
|
||||
<div id="summary-pc-model" style="font-size: 12px; color: var(--text-muted);">-</div>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 보유 PC 목록 선택 (반납/이동 시) -->
|
||||
<div id="user-pcs-container" class="hidden" style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<div style="font-size: 12px; font-weight: 700; color: var(--text-muted);">사원 보유 PC 선택 (클릭하여 매핑)</div>
|
||||
<div id="user-pcs-list" style="display: flex; flex-direction: column; gap: 8px; max-height: 200px; overflow-y: auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer" style="padding: 16px 24px; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; gap: 12px; background: var(--bg-light);">
|
||||
<button id="btn-cancel-pc-flow-modal" class="btn btn-outline" style="height: 42px;">취소</button>
|
||||
<button id="btn-submit-pc-flow" class="btn btn-primary" style="height: 42px;">이동/반납 처리 완료</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.flow-type-label {
|
||||
transition: all 0.2s;
|
||||
border-color: var(--border-color);
|
||||
background: white;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.flow-type-label:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.flow-type-label.active {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.suggestion-item:hover {
|
||||
background-color: var(--primary-light) !important;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const pcFlowModal = PCFlowModal.getInstance();
|
||||
Reference in New Issue
Block a user