feat(flow-logs): fix text overlapping, show full asset code, filter by current month and support JSON logs

This commit is contained in:
2026-06-11 11:14:04 +09:00
parent 525dbd77d4
commit 565802f55b
6 changed files with 1408 additions and 107 deletions

View File

@@ -19,6 +19,7 @@ let corpChartInstance4p: any = null;
let totalServerMismatchByPurposeChartInstance4p: any = null;
let serverServiceChartInstance4p: any = null;
let serverStatusChartInstance4p: any = null;
let pcFlowChartInstance: any = null;
// ─── 서버 용도별 카테고리 분류 헬퍼 ───
function categorizePurpose(purpose: string): string {
@@ -563,12 +564,82 @@ function buildServerStatusTableRows(list: any[]): string {
export function renderHwDashboard(container: HTMLElement) {
const allHw = state.masterData.hw || [];
// --- PC FLOW LOGS DATA PREP ---
const logs = state.masterData.logs || [];
const now = new Date();
const currentYearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
let currentMonthCheckout = 0;
let currentMonthReturn = 0;
let currentMonthMove = 0;
let totalCheckout = 0;
let totalReturn = 0;
let totalMove = 0;
const flowLogs = logs.filter((log: any) => {
const details = log.details || '';
const isFlow = details.includes('[불출]') || details.includes('[반납]') || details.includes('[입고]') || details.includes('[이동]') || details.includes('[이관]');
if (isFlow) {
const logDate = log.log_date || '';
const isCurrentMonth = logDate.startsWith(currentYearMonth);
if (details.includes('[불출]')) {
totalCheckout++;
if (isCurrentMonth) currentMonthCheckout++;
} else if (details.includes('[반납]') || details.includes('[입고]')) {
totalReturn++;
if (isCurrentMonth) currentMonthReturn++;
} else if (details.includes('[이동]') || details.includes('[이관]')) {
totalMove++;
if (isCurrentMonth) currentMonthMove++;
}
return true;
}
return false;
});
const recentFlowLogs = flowLogs.slice(0, 5);
let recentFlowLogsHtml = '';
if (recentFlowLogs.length === 0) {
recentFlowLogsHtml = '<tr><td colspan="4" style="text-align:center; padding:1.5rem; color:#94A3B8; font-size:12px;">최근 유동 이력이 없습니다.</td></tr>';
} else {
recentFlowLogs.forEach((log: any) => {
const details = log.details || '';
let badgeHtml = '';
if (details.includes('[불출]')) {
badgeHtml = '<span style="background:#E0F2FE;color:#0369A1;padding:2px 6px;border-radius:4px;font-size:11px;font-weight:700;">불출</span>';
} else if (details.includes('[반납]') || details.includes('[입고]')) {
badgeHtml = '<span style="background:#DCFCE7;color:#15803D;padding:2px 6px;border-radius:4px;font-size:11px;font-weight:700;">입고</span>';
} else if (details.includes('[이동]') || details.includes('[이관]')) {
badgeHtml = '<span style="background:#FEF3C7;color:#B45309;padding:2px 6px;border-radius:4px;font-size:11px;font-weight:700;">이동</span>';
}
const cleanDetails = details.replace(/^\[(불출|반납|입고|이동|이관)\]\s*/, '');
recentFlowLogsHtml += `
<tr style="border-bottom: 1px solid #F1F5F9; font-size: 13px;">
<td style="padding: 8px; color: #64748B;">${log.log_date || '-'}</td>
<td style="padding: 8px;">${badgeHtml}</td>
<td style="padding: 8px; font-weight: 600; color: #334155;">${log.log_user || '시스템'}</td>
<td style="padding: 8px; color: #475569;" title="${details}">${cleanDetails}</td>
</tr>
`;
});
}
// --- PC DATA PREP ---
const pcs = allHw.filter(a => {
const cat = a[ASSET_SCHEMA.CATEGORY.key] || '';
const type = a[ASSET_SCHEMA.ASSET_TYPE.key] || '';
const job = a[ASSET_SCHEMA.USER_POSITION.key] || '';
return (cat === 'PC' || type === '개인PC' || type === '노트북' || type === '공용PC') && job !== '재고PC';
const status = a[ASSET_SCHEMA.HW_STATUS.key] || '';
const user = a[ASSET_SCHEMA.CURRENT_USER.key] || '';
return (cat === 'PC' || type === '개인PC' || type === '노트북' || type === '공용PC') &&
job !== '재고PC' &&
status === '사용중' &&
user.trim() !== '';
});
const jobScores: Record<string, { totalScore: number; count: number; avg: number }> = {};
@@ -824,7 +895,7 @@ export function renderHwDashboard(container: HTMLElement) {
'</div>' +
'<div class="slider-controls">' +
'<button id="slider-prev" class="slider-nav-btn" disabled><i data-lucide="chevron-left"></i></button>' +
'<span id="slider-indicator" class="slider-indicator">1 / 4</span>' +
'<span id="slider-indicator" class="slider-indicator">1 / 5</span>' +
'<button id="slider-next" class="slider-nav-btn"><i data-lucide="chevron-right"></i></button>' +
'</div>' +
'</div>' +
@@ -1082,8 +1153,6 @@ export function renderHwDashboard(container: HTMLElement) {
'<div style="flex: 1; min-height: 0; position: relative;"><canvas id="chart-server-status-4p" style="position: absolute; top:0; left:0; width:100%; height:100%;"></canvas></div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
@@ -1108,7 +1177,10 @@ export function renderHwDashboard(container: HTMLElement) {
serverServiceGroups,
serverStatusGroups,
purposeServerUnders,
purposeServerOvers
purposeServerOvers,
totalCheckout,
totalReturn,
totalMove
);
// 기획서 보기 버튼 클릭 이벤트 바인딩
@@ -1191,7 +1263,10 @@ function initCharts(
serviceGroups: any,
statusGroups: any,
purposeServerUnders?: any,
purposeServerOvers?: any
purposeServerOvers?: any,
totalCheckout?: number,
totalReturn?: number,
totalMove?: number
) {
// 직무별 점수
const jobCtx = document.getElementById('chart-job-scores') as HTMLCanvasElement;
@@ -1654,4 +1729,39 @@ function initCharts(
}
});
}
// PC 유동 비율 도넛 차트
const flowCtx = document.getElementById('chart-pc-flow-stats') as HTMLCanvasElement;
if (flowCtx && typeof Chart !== 'undefined') {
const tCheckout = totalCheckout || 0;
const tReturn = totalReturn || 0;
const tMove = totalMove || 0;
if (pcFlowChartInstance) {
pcFlowChartInstance.destroy();
pcFlowChartInstance = null;
}
pcFlowChartInstance = new Chart(flowCtx, {
type: 'doughnut',
data: {
labels: ['불출', '입고(반납)', '이동(이관)'],
datasets: [{
data: [tCheckout, tReturn, tMove],
backgroundColor: ['#3B82F6', '#10B981', '#F59E0B'],
borderWidth: 0,
hoverOffset: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { padding: 10, usePointStyle: true, boxWidth: 10 } }
},
cutout: '75%',
animation: { animateScale: true, animateRotate: true }
}
});
}
}