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 = '