feat: 모든 카테고리(HW, SW, SW 사용자) DB 일괄 덮어쓰기 저장 기능 구현
This commit is contained in:
150
src/views/Dashboard/SwDashboard.ts
Normal file
150
src/views/Dashboard/SwDashboard.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { state } from '../../core/state';
|
||||
import { SoftwareAsset } from '../../core/excelHandler';
|
||||
import { openSwDashboardDetail, openSwUsageDetail } from '../../components/Modal/DashboardDetailModal';
|
||||
import { normalizeDate } from '../../core/utils';
|
||||
|
||||
declare var Chart: any;
|
||||
|
||||
export function renderSwDashboard(container: HTMLElement) {
|
||||
let subQty = 0, subUsed = 0, subExp = 0, subTotal = 0;
|
||||
let permQty = 0, permUsed = 0, permExp = 0, permTotal = 0;
|
||||
|
||||
const currentYear = new Date().getFullYear().toString();
|
||||
const corps = ['한맥', '삼안', '바론'];
|
||||
const categories = ['업무공통', '개발S/W', '디자인', '설계S/W'];
|
||||
|
||||
const costByCorp: Record<string, number> = { '한맥': 0, '삼안': 0, '바론': 0 };
|
||||
const costByCat: Record<string, number> = {};
|
||||
categories.forEach(c => costByCat[c] = 0);
|
||||
|
||||
state.masterData.sw.forEach(sw => {
|
||||
const assigned = state.masterData.swUsers.filter(u => u.swId === sw.id).length;
|
||||
const qty = typeof sw.수량 === 'number' ? sw.수량 : parseInt(sw.수량||'0', 10);
|
||||
const priceStr = sw.금액 ? String(sw.금액).replace(/,/g, '') : '0';
|
||||
const price = parseInt(priceStr, 10) || 0;
|
||||
|
||||
if (sw.type === '구독SW') {
|
||||
subQty += qty; subUsed += assigned; subTotal++;
|
||||
if (isSWExpiring(sw)) subExp++;
|
||||
} else {
|
||||
permQty += qty; permUsed += assigned; permTotal++;
|
||||
if (isSWExpiring(sw)) permExp++;
|
||||
}
|
||||
|
||||
if (sw.구매일 && sw.구매일.startsWith(currentYear)) {
|
||||
if (costByCorp[sw.법인] !== undefined) costByCorp[sw.법인] += price;
|
||||
if (sw.분야 && costByCat[sw.분야] !== undefined) costByCat[sw.분야] += price;
|
||||
}
|
||||
});
|
||||
|
||||
const subPer = subQty > 0 ? Math.round((subUsed/subQty)*100) : 0;
|
||||
const permPer = permQty > 0 ? Math.round((permUsed/permQty)*100) : 0;
|
||||
const subExpPer = subTotal > 0 ? Math.round((subExp/subTotal)*100) : 0;
|
||||
const permExpPer = permTotal > 0 ? Math.round((permExp/permTotal)*100) : 0;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="view-container">
|
||||
<h3 class="dashboard-section-title">소프트웨어 라이선스 현황</h3>
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
||||
<div class="dashboard-card" data-action="sub-usage" style="cursor:pointer; min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 소프트웨어 사용율</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">${subQty}카피 중 ${subUsed}개 할당</div>
|
||||
<div style="font-size: 2rem; font-weight:700; color:var(--dash-primary);">${subPer}%</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
|
||||
<div style="width: ${subPer}%; height: 100%; background-color: var(--dash-primary);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card" data-action="perm-usage" style="cursor:pointer; min-height:auto;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">영구 소프트웨어 사용율</span>
|
||||
<div style="font-size: 0.8125rem; color:var(--text-muted); margin-bottom: 1rem;">${permQty}카피 중 ${permUsed}개 할당</div>
|
||||
<div style="font-size: 2rem; font-weight:700; color:var(--dash-primary);">${permPer}%</div>
|
||||
<div style="width: 100%; height: 4px; background-color: var(--border-color); border-radius: 2px; overflow: hidden; margin-top: 0.5rem;">
|
||||
<div style="width: ${permPer}%; height: 100%; background-color: var(--dash-primary);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-layout-2col" style="margin-bottom: 1.5rem;">
|
||||
<div class="dashboard-card" data-action="sub-exp" style="flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer; min-height:auto;">
|
||||
<div style="flex:1;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">구독 SW 만료 예정 (30일 이내)</span>
|
||||
<div style="font-size: 1.5rem; font-weight:700; color:${subExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'}; margin-top:0.5rem;">${subExp}개 제품</div>
|
||||
</div>
|
||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${subExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 0.875rem; color:var(--text-muted); font-weight:600;">${subExpPer}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-card" data-action="perm-exp" style="flex-direction:row; justify-content:space-between; align-items:center; cursor:pointer; min-height:auto;">
|
||||
<div style="flex:1;">
|
||||
<span style="font-size:1rem; font-weight:700; color:var(--text-main);">유지보수 만료 예정 (30일 이내)</span>
|
||||
<div style="font-size: 1.5rem; font-weight:700; color:${permExp > 0 ? 'var(--dash-danger)' : 'var(--text-main)'}; margin-top:0.5rem;">${permExp}개 제품</div>
|
||||
</div>
|
||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: conic-gradient(var(--dash-danger) ${permExpPer}%, var(--border-color) 0); display:flex; justify-content:center; align-items:center;">
|
||||
<div style="width: 48px; height: 48px; border-radius: 50%; background: var(--white); display:flex; justify-content:center; align-items:center;">
|
||||
<span style="font-size: 0.875rem; color:var(--text-muted); font-weight:600;">${permExpPer}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="dashboard-section-title">${currentYear}년 도입 비용 분석</h3>
|
||||
<div class="dashboard-layout-2col">
|
||||
<div class="dashboard-card">
|
||||
<h4 style="margin-bottom:1rem; font-size:0.9rem; color:var(--text-muted);">법인별 도입 금액 (원)</h4>
|
||||
<canvas id="chart-sw-corp"></canvas>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<h4 style="margin-bottom:1rem; font-size:0.9rem; color:var(--text-muted);">분야별 도입 금액 (원)</h4>
|
||||
<canvas id="chart-sw-cat"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
if (typeof Chart === 'undefined') return;
|
||||
const ctxCorp = (document.getElementById('chart-sw-corp') as HTMLCanvasElement)?.getContext('2d');
|
||||
const ctxCat = (document.getElementById('chart-sw-cat') as HTMLCanvasElement)?.getContext('2d');
|
||||
if (ctxCorp) {
|
||||
const chart = new Chart(ctxCorp, {
|
||||
type: 'bar',
|
||||
data: { labels: corps, datasets: [{ data: corps.map(c => costByCorp[c]), backgroundColor: 'rgba(30, 81, 73, 0.8)', borderRadius: 4 }] },
|
||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
|
||||
});
|
||||
state.activeCharts.push(chart);
|
||||
}
|
||||
if (ctxCat) {
|
||||
const chart = new Chart(ctxCat, {
|
||||
type: 'bar',
|
||||
data: { labels: categories, datasets: [{ data: categories.map(c => costByCat[c]), backgroundColor: 'rgba(59, 130, 246, 0.8)', borderRadius: 4 }] },
|
||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
|
||||
});
|
||||
state.activeCharts.push(chart);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
container.querySelector('[data-action="sub-usage"]')?.addEventListener('click', () => openSwUsageDetail('구독 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '구독SW')));
|
||||
container.querySelector('[data-action="perm-usage"]')?.addEventListener('click', () => openSwUsageDetail('영구 소프트웨어 사용 목록', state.masterData.sw.filter(sw => sw.type === '영구SW')));
|
||||
container.querySelector('[data-action="sub-exp"]')?.addEventListener('click', () => openSwDashboardDetail('구독 SW 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '구독SW' && isSWExpiring(sw))));
|
||||
container.querySelector('[data-action="perm-exp"]')?.addEventListener('click', () => openSwDashboardDetail('유지보수 만료 예정 목록', state.masterData.sw.filter(sw => sw.type === '영구SW' && isSWExpiring(sw))));
|
||||
}
|
||||
|
||||
function isSWExpiring(sw: SoftwareAsset) {
|
||||
if (sw.type === '구독SW' && sw.구독일) {
|
||||
const parts = sw.구독일.split('~');
|
||||
if (parts.length > 1) {
|
||||
const endMs = new Date(normalizeDate(parts[1])).getTime();
|
||||
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
||||
return diffDays >= 0 && diffDays <= 30;
|
||||
}
|
||||
} else if (sw.type === '영구SW' && sw.비고 && sw.비고.includes('유지보수: ~')) {
|
||||
try {
|
||||
const endMs = new Date(normalizeDate(sw.비고.split('~')[1])).getTime();
|
||||
const diffDays = (endMs - Date.now()) / (1000 * 60 * 60 * 24);
|
||||
return diffDays >= 0 && diffDays <= 30;
|
||||
} catch { return false; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user