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; document.querySelectorAll('.flow-type-label').forEach(l => { l.classList.toggle('active', l.contains(radioCheckout)); }); } // 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 = '
일치하는 사원이 없습니다.
'; container.classList.remove('hidden'); return; } users.forEach(u => { const item = document.createElement('div'); item.className = 'autocomplete-item'; item.innerHTML = `
${u.user_name}
부서: ${u.dept_name} | 사번: ${u.emp_no || '-'}
`; 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 = '
불출 가능한 대기 PC 재고가 없습니다.
'; container.classList.remove('hidden'); return; } pcs.forEach(p => { const item = document.createElement('div'); item.className = 'autocomplete-item'; item.innerHTML = `
${p.asset_code} (${p.model_name || '모델명 없음'})
사양: CPU ${p.cpu || '-'} / RAM ${p.ram || '-'} / 위치: ${p.location || '-'}
`; 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 = '
이 사용자가 소유한 PC 자산이 없습니다.
'; } else { userPcsList.innerHTML = userPcs.map(p => { const isSelected = this.selectedPC && this.selectedPC.id === p.id; return `
${p.asset_code}
${p.model_name || '모델명 없음'} | CPU: ${p.cpu || '-'} | RAM: ${p.ram || '-'}
`; }).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 { return ` `; } } export const pcFlowModal = PCFlowModal.getInstance();