feat: restructure navigation, customize list columns, and move action buttons to search bar
1. Restructured navigation hierarchy (Hardware, Software, Ops Support, etc.). 2. Customized table columns for all asset categories according to new specs. 3. Moved Template/Upload/Export/Add buttons to search bar with layout optimization. 4. Hidden Asset Code and Previous User from list views (Modal only). 5. Added Current/Previous User and detailed PC spec fields (GPU, HDD3/4).
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
import { state } from '../../core/state';
|
||||
import { openSwModal } from '../../components/Modal/SWModal';
|
||||
import { openSwUserModal } from '../../components/Modal/SWUserModal';
|
||||
import { sortAssets, dynamicSort, formatPrice } from '../../core/utils';
|
||||
import { sortAssets, dynamicSort, formatPrice, getActionButtonsHTML } from '../../core/utils';
|
||||
import { setupTableSorting, SortState } from '../../core/tableHandler';
|
||||
import { ASSET_SCHEMA } from '../../core/schema';
|
||||
import { CORP_LIST } from '../../components/Modal/SharedData';
|
||||
import { generateOptionsHTML } from '../../components/Modal/ModalUtils';
|
||||
import { createIcons, Edit2, Users, RefreshCcw } from 'lucide';
|
||||
import { createIcons, Edit2, Users, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } from 'lucide';
|
||||
|
||||
export function renderSwList(container: HTMLElement) {
|
||||
const isSub = state.activeSubTab === '구독SW';
|
||||
const fullList = sortAssets(isSub ? state.masterData.subSw : state.masterData.permSw);
|
||||
const isInternal = state.activeSubTab === '내부';
|
||||
const fullList = sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal);
|
||||
|
||||
let sortState: SortState = { key: '', direction: 'asc' };
|
||||
|
||||
@@ -17,11 +18,11 @@ export function renderSwList(container: HTMLElement) {
|
||||
filterBar.className = 'search-bar';
|
||||
filterBar.innerHTML = `
|
||||
<div class="search-item flex-1">
|
||||
<label>통합 검색 (제품명/부서)</label>
|
||||
<label>통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})</label>
|
||||
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label>분야</label>
|
||||
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
|
||||
<select id="filter-field">
|
||||
<option value="">전체 분야</option>
|
||||
<option value="업무공통">업무공통</option>
|
||||
@@ -31,38 +32,54 @@ export function renderSwList(container: HTMLElement) {
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<label>법인</label>
|
||||
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
|
||||
<select id="filter-corp">${generateOptionsHTML(CORP_LIST, '', true)}</select>
|
||||
</div>
|
||||
<button id="btn-reset-filters" class="btn btn-outline btn-reset">
|
||||
<i data-lucide="refresh-ccw"></i> 필터 초기화
|
||||
</button>
|
||||
${getActionButtonsHTML()}
|
||||
`;
|
||||
container.appendChild(filterBar);
|
||||
|
||||
const tableWrapper = document.createElement('div');
|
||||
tableWrapper.className = 'table-container';
|
||||
const table = document.createElement('table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center; width: 50px;">No.</th>
|
||||
<th style="text-align:center;" data-sort="상태">상태</th>
|
||||
<th style="text-align:center;" data-sort="분야">분야</th>
|
||||
<th style="text-align:center;" data-sort="법인">법인</th>
|
||||
<th style="text-align:center;" data-sort="부서">부서</th>
|
||||
<th style="text-align:center;" data-sort="제품명">제품명</th>
|
||||
<th style="text-align:center;" data-sort="구매일">구매일</th>
|
||||
<th style="text-align:center;" data-sort="시작일">시작일</th>
|
||||
<th style="text-align:center;" data-sort="만료일">만료일</th>
|
||||
<th style="text-align:center;" data-sort="금액">금액</th>
|
||||
<th style="text-align:center;" data-sort="수량">수량</th>
|
||||
<th style="text-align:center;">사용가능</th>
|
||||
<th style="text-align:center;">사용자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dynamic-tbody"></tbody>
|
||||
`;
|
||||
|
||||
if (isInternal) {
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center; width: 50px;">No.</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_FIELD.key}">${ASSET_SCHEMA.SW_FIELD.ui}</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.DEV_OBJ.key}">${ASSET_SCHEMA.DEV_OBJ.ui}</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_STATUS.key}">${ASSET_SCHEMA.SW_STATUS.ui}</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_TYPE.key}">${ASSET_SCHEMA.SW_TYPE.ui}</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dynamic-tbody"></tbody>
|
||||
`;
|
||||
} else {
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center; width: 50px;">No.</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PRODUCT_NAME.key}">자산명</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.ASSET_TYPE.key}">유형</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_STATUS.key}">${ASSET_SCHEMA.SW_STATUS.ui}</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.SW_FIELD.key}">${ASSET_SCHEMA.SW_FIELD.ui}</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.CURRENT_DEPT.key}">${ASSET_SCHEMA.CURRENT_DEPT.ui}</th>
|
||||
<th style="text-align:center;">현 사용자</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.PURCHASE_DATE.key}">구매연월</th>
|
||||
<th style="text-align:center;">시작일</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.EXPIRY_DATE.key}">만료일</th>
|
||||
<th style="text-align:center;" data-sort="${ASSET_SCHEMA.MEMO.key}">${ASSET_SCHEMA.MEMO.ui}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dynamic-tbody"></tbody>
|
||||
`;
|
||||
}
|
||||
|
||||
tableWrapper.appendChild(table);
|
||||
container.appendChild(tableWrapper);
|
||||
@@ -78,9 +95,11 @@ export function renderSwList(container: HTMLElement) {
|
||||
const corp = corpSelect ? corpSelect.value : '';
|
||||
|
||||
let filtered = fullList.filter(asset => {
|
||||
const matchKeyword = !keyword || (asset.제품명 || '').toLowerCase().includes(keyword) || (asset.부서 || '').toLowerCase().includes(keyword);
|
||||
const matchField = !field || asset.분야 === field;
|
||||
const matchCorp = !corp || asset.법인 === corp;
|
||||
const matchKeyword = !keyword ||
|
||||
(asset[ASSET_SCHEMA.PRODUCT_NAME.key] || '').toLowerCase().includes(keyword) ||
|
||||
(asset[ASSET_SCHEMA.CURRENT_DEPT.key] || '').toLowerCase().includes(keyword);
|
||||
const matchField = !field || asset[ASSET_SCHEMA.SW_FIELD.key] === field;
|
||||
const matchCorp = !corp || asset[ASSET_SCHEMA.PURCHASE_CORP.key] === corp;
|
||||
return matchKeyword && matchField && matchCorp;
|
||||
});
|
||||
|
||||
@@ -90,76 +109,46 @@ export function renderSwList(container: HTMLElement) {
|
||||
|
||||
tbody.innerHTML = '';
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="13" style="text-align:center; padding: 3rem; color: var(--text-muted);">검색 결과가 없습니다.</td></tr>`;
|
||||
tbody.innerHTML = `<tr><td colspan="${isInternal ? 6 : 11}" style="text-align:center; padding: 3rem; color: var(--text-muted);">검색 결과가 없습니다.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach((asset, idx) => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.sw_id === asset.id).length;
|
||||
const qty = typeof asset.수량 === 'number' ? asset.수량 : parseInt(asset.수량||'0', 10);
|
||||
const avail = qty - assigned;
|
||||
|
||||
let statusHtml = '';
|
||||
if (isSub) {
|
||||
let isExpired = false;
|
||||
if (asset.만료일) {
|
||||
const endDateStr = asset.만료일.replace(/\./g, '-');
|
||||
const endDate = new Date(endDateStr);
|
||||
if (!isNaN(endDate.getTime())) {
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
if (endDate < new Date()) isExpired = true;
|
||||
}
|
||||
}
|
||||
if (isExpired) statusHtml = `<span style="background: var(--danger, #ef4444); color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">만료</span>`;
|
||||
else statusHtml = `<span style="background: var(--primary-color, #1E5149); color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">사용중</span>`;
|
||||
if (isInternal) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.innerHTML = `
|
||||
<td style="text-align:center;">${idx+1}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_FIELD.key]||''}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.DEV_OBJ.key]||''}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_STATUS.key]||'보유중'}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_TYPE.key]||'내부'}</td>
|
||||
<td>${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
|
||||
`;
|
||||
tr.addEventListener('click', () => openSwModal(asset, 'view'));
|
||||
tbody.appendChild(tr);
|
||||
} else {
|
||||
let isMaintenance = false;
|
||||
if (asset.시작일 && asset.만료일) {
|
||||
const startDate = new Date(asset.시작일.replace(/\./g, '-'));
|
||||
const endDate = new Date(asset.만료일.replace(/\./g, '-'));
|
||||
const today = new Date();
|
||||
if (!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
if (today >= startDate && today <= endDate) isMaintenance = true;
|
||||
}
|
||||
}
|
||||
if (isMaintenance) statusHtml = `<span style="background: #3b82f6; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">유지보수</span>`;
|
||||
else statusHtml = `<span style="background: #6b7280; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: bold; white-space: nowrap;">보유중</span>`;
|
||||
}
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
const users = state.masterData.swUsers.filter(u => u.sw_id === asset.id);
|
||||
const userText = users.length > 0 ? `${users[0].user_name}${users.length > 1 ? ' 외 ' + (users.length - 1) : ''}` : '-';
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td style="text-align:center;">${idx+1}</td>
|
||||
<td style="text-align:center;">${statusHtml}</td>
|
||||
<td>${asset.분야||''}</td>
|
||||
<td>${asset.법인}</td>
|
||||
<td>${asset.부서||''}</td>
|
||||
<td>${asset.제품명}</td>
|
||||
<td style="text-align:center;">${asset.구매일||''}</td>
|
||||
<td style="text-align:center;">${asset.시작일||''}</td>
|
||||
<td style="text-align:center;">${asset.만료일||''}</td>
|
||||
<td style="text-align:right;">${formatPrice(asset.금액)}</td>
|
||||
<td style="text-align:center;">${qty}</td>
|
||||
<td style="text-align:center;"><strong style="color: ${avail > 0 ? 'var(--primary-color)' : 'var(--danger)'}">${avail}</strong></td>
|
||||
<td style="text-align:center;">
|
||||
<button class="btn-icon btn-user-mgmt" title="사용자 관리" style="margin: 0 auto; color: var(--primary-color);">
|
||||
<i data-lucide="users" style="width:18px; height:18px;"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
const userBtn = tr.querySelector('.btn-user-mgmt');
|
||||
userBtn?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
openSwUserModal(asset);
|
||||
});
|
||||
|
||||
tr.addEventListener('click', (e) => {
|
||||
openSwModal(asset, 'view');
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
tr.innerHTML = `
|
||||
<td style="text-align:center;">${idx+1}</td>
|
||||
<td>${asset[ASSET_SCHEMA.PRODUCT_NAME.key]||''}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.ASSET_TYPE.key]||'외부'}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_STATUS.key]||'사용중'}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.SW_FIELD.key]||''}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.CURRENT_DEPT.key]||''}</td>
|
||||
<td style="text-align:center;">${userText}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||''}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.PURCHASE_DATE.key]||''}</td>
|
||||
<td style="text-align:center;">${asset[ASSET_SCHEMA.EXPIRY_DATE.key]||''}</td>
|
||||
<td>${formatInline(asset[ASSET_SCHEMA.MEMO.key]||'-')}</td>
|
||||
`;
|
||||
tr.addEventListener('click', () => openSwModal(asset, 'view'));
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
});
|
||||
|
||||
setupTableSorting(table, sortState, (key, dir) => {
|
||||
@@ -167,7 +156,7 @@ export function renderSwList(container: HTMLElement) {
|
||||
updateTable();
|
||||
});
|
||||
|
||||
createIcons({ icons: { Edit2, Users, RefreshCcw } });
|
||||
createIcons({ icons: { Edit2, Users, RefreshCcw, Download, Upload, FileSpreadsheet, Plus } });
|
||||
};
|
||||
|
||||
document.getElementById('filter-keyword')?.addEventListener('input', updateTable);
|
||||
|
||||
Reference in New Issue
Block a user