feat: add all-team filter to mh analysis
This commit is contained in:
@@ -1482,6 +1482,7 @@
|
|||||||
const RANK_WEIGHTS = { '수석': 1, '책임': 2, '선임': 3, '연구원': 4 };
|
const RANK_WEIGHTS = { '수석': 1, '책임': 2, '선임': 3, '연구원': 4 };
|
||||||
|
|
||||||
let teamData = [];
|
let teamData = [];
|
||||||
|
const ALL_TEAM_VALUE = '__ALL__';
|
||||||
let allTeams = [];
|
let allTeams = [];
|
||||||
let allPeopleData = [];
|
let allPeopleData = [];
|
||||||
let searchTeams = [];
|
let searchTeams = [];
|
||||||
@@ -1645,7 +1646,11 @@
|
|||||||
if (seen.has(key)) return false;
|
if (seen.has(key)) return false;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
return true;
|
return true;
|
||||||
});
|
}).map(p => ({
|
||||||
|
...p,
|
||||||
|
value: `${p.name}|${p.team}`,
|
||||||
|
label: `${p.name} (${p.team})`
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
const findBizIndexByProject = (normalizedHeaders, projIdx) => {
|
const findBizIndexByProject = (normalizedHeaders, projIdx) => {
|
||||||
const bizKey = normalizeHeader('사업 종류');
|
const bizKey = normalizeHeader('사업 종류');
|
||||||
@@ -2465,19 +2470,19 @@
|
|||||||
searchTeams = [...new Set(allRows.map(r => String(r[columnMap.team] || '').trim()))].filter(Boolean).sort();
|
searchTeams = [...new Set(allRows.map(r => String(r[columnMap.team] || '').trim()))].filter(Boolean).sort();
|
||||||
searchPeopleData = buildScopedPeopleData(allRows);
|
searchPeopleData = buildScopedPeopleData(allRows);
|
||||||
|
|
||||||
teamSelect.innerHTML = '<option value="">팀 선택</option>' + allTeams.map(t => `<option value="${t}">${t}</option>`).join('');
|
teamSelect.innerHTML = `<option value="${ALL_TEAM_VALUE}">전체</option>` + allTeams.map(t => `<option value="${t}">${t}</option>`).join('');
|
||||||
|
|
||||||
if (currentTeam && allTeams.includes(currentTeam)) {
|
if (currentTeam === ALL_TEAM_VALUE) {
|
||||||
|
teamSelect.value = ALL_TEAM_VALUE;
|
||||||
|
} else if (currentTeam && allTeams.includes(currentTeam)) {
|
||||||
teamSelect.value = currentTeam;
|
teamSelect.value = currentTeam;
|
||||||
} else if (allTeams.length > 0) {
|
|
||||||
teamSelect.value = allTeams[0];
|
|
||||||
} else {
|
} else {
|
||||||
teamSelect.value = '';
|
teamSelect.value = ALL_TEAM_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFilters();
|
updateFilters();
|
||||||
|
|
||||||
if (teamSelect.value && currentPerson && allPeopleData.some(p => p.team === teamSelect.value && p.name === currentPerson)) {
|
if (currentPerson && [...personSelect.options].some(option => option.value === currentPerson)) {
|
||||||
personSelect.value = currentPerson;
|
personSelect.value = currentPerson;
|
||||||
} else {
|
} else {
|
||||||
personSelect.value = '';
|
personSelect.value = '';
|
||||||
@@ -2621,7 +2626,10 @@
|
|||||||
setScope(getTeamScope(item.dataset.team), false);
|
setScope(getTeamScope(item.dataset.team), false);
|
||||||
teamSelect.value = item.dataset.team;
|
teamSelect.value = item.dataset.team;
|
||||||
updateFilters();
|
updateFilters();
|
||||||
personSelect.value = item.dataset.name;
|
const targetValue = `${item.dataset.name}|${item.dataset.team}`;
|
||||||
|
personSelect.value = [...personSelect.options].some(option => option.value === targetValue)
|
||||||
|
? targetValue
|
||||||
|
: item.dataset.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
mainSearchInput.value = "";
|
mainSearchInput.value = "";
|
||||||
@@ -2655,6 +2663,12 @@
|
|||||||
personSelect.innerHTML = '<option value="">팀원을 선택하세요</option>';
|
personSelect.innerHTML = '<option value="">팀원을 선택하세요</option>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (team === ALL_TEAM_VALUE) {
|
||||||
|
document.getElementById('main-title').innerText = '전체';
|
||||||
|
const allPeople = buildScopedPeopleData(getScopedRows());
|
||||||
|
personSelect.innerHTML = '<option value="">전체 구성원</option>' + allPeople.map(p => `<option value="${p.value}">${p.label}</option>`).join('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.getElementById('main-title').innerText = team;
|
document.getElementById('main-title').innerText = team;
|
||||||
const teamPeople = [...new Set(getScopedRows().filter(r => String(r[columnMap.team] || '').trim() === team).map(r => String(r[columnMap.name] || '').trim()))].filter(Boolean).sort();
|
const teamPeople = [...new Set(getScopedRows().filter(r => String(r[columnMap.team] || '').trim() === team).map(r => String(r[columnMap.name] || '').trim()))].filter(Boolean).sort();
|
||||||
personSelect.innerHTML = '<option value="">팀원을 선택하세요</option>' + teamPeople.map(p => `<option value="${p}">${p}</option>`).join('');
|
personSelect.innerHTML = '<option value="">팀원을 선택하세요</option>' + teamPeople.map(p => `<option value="${p}">${p}</option>`).join('');
|
||||||
@@ -2761,12 +2775,15 @@
|
|||||||
|
|
||||||
teamData.slice(1).forEach(row => {
|
teamData.slice(1).forEach(row => {
|
||||||
const d = dStr(row[columnMap.date]);
|
const d = dStr(row[columnMap.date]);
|
||||||
if (!d || d < startStr || d > endStr || !isRowInScope(row) || String(row[columnMap.team] || '').trim() !== teamF) return;
|
const rowTeam = String(row[columnMap.team] || '').trim();
|
||||||
|
if (!d || d < startStr || d > endStr || !isRowInScope(row) || (teamF !== ALL_TEAM_VALUE && rowTeam !== teamF)) return;
|
||||||
const lateFlagText = String(row[columnMap.lateFlag] || '').trim();
|
const lateFlagText = String(row[columnMap.lateFlag] || '').trim();
|
||||||
const userStateText = String(row[columnMap.userState] || '').trim();
|
const userStateText = String(row[columnMap.userState] || '').trim();
|
||||||
const isWeekend = lateFlagText.includes("\uC8FC\uB9D0");
|
const isWeekend = lateFlagText.includes("\uC8FC\uB9D0");
|
||||||
const name = String(row[columnMap.name] || '').trim();
|
const name = String(row[columnMap.name] || '').trim();
|
||||||
const rank = String(row[columnMap.rank] || '연구원').trim();
|
const rank = String(row[columnMap.rank] || '연구원').trim();
|
||||||
|
const personKey = teamF === ALL_TEAM_VALUE ? `${name}|${rowTeam}` : name;
|
||||||
|
const personLabel = teamF === ALL_TEAM_VALUE ? `${name} (${rowTeam})` : name;
|
||||||
const hasSourceCostCols = columnMap.normalCost >= 0 || columnMap.extraCost >= 0;
|
const hasSourceCostCols = columnMap.normalCost >= 0 || columnMap.extraCost >= 0;
|
||||||
const sourceNormalLaborCost = columnMap.normalCost >= 0 ? parseNumber(row[columnMap.normalCost]) : 0;
|
const sourceNormalLaborCost = columnMap.normalCost >= 0 ? parseNumber(row[columnMap.normalCost]) : 0;
|
||||||
const sourceExtraLaborCost = columnMap.extraCost >= 0 ? parseNumber(row[columnMap.extraCost]) : 0;
|
const sourceExtraLaborCost = columnMap.extraCost >= 0 ? parseNumber(row[columnMap.extraCost]) : 0;
|
||||||
@@ -2783,24 +2800,24 @@
|
|||||||
stats.normalLaborCost += normalLaborCost;
|
stats.normalLaborCost += normalLaborCost;
|
||||||
stats.extraLaborCost += extraLaborCost;
|
stats.extraLaborCost += extraLaborCost;
|
||||||
stats.laborCost += normalLaborCost + extraLaborCost;
|
stats.laborCost += normalLaborCost + extraLaborCost;
|
||||||
if (!pMap[name]) pMap[name] = { name, rank, normal: 0, extra: 0, holiday: 0, extraCount: 0, holidayCount: 0, total: 0, projects: {} };
|
if (!pMap[personKey]) pMap[personKey] = { name, team: rowTeam, displayName: personLabel, personKey, rank, normal: 0, extra: 0, holiday: 0, extraCount: 0, holidayCount: 0, total: 0, projects: {} };
|
||||||
slots.forEach(s => {
|
slots.forEach(s => {
|
||||||
const val = parseNumber(row[s.time]);
|
const val = parseNumber(row[s.time]);
|
||||||
const projectCode = (s.code >= 0 ? String(row[s.code] || '') : '').trim();
|
const projectCode = (s.code >= 0 ? String(row[s.code] || '') : '').trim();
|
||||||
const pName = String(row[s.proj] || '').trim();
|
const pName = String(row[s.proj] || '').trim();
|
||||||
const biz = (s.biz >= 0 ? String(row[s.biz] || '') : '').trim() || '공통';
|
const biz = (s.biz >= 0 ? String(row[s.biz] || '') : '').trim() || '공통';
|
||||||
if (val > 0 && pName) {
|
if (val > 0 && pName) {
|
||||||
const ownershipInfo = getProjectOwnership(teamF, projectCode);
|
const ownershipInfo = getProjectOwnership(teamF === ALL_TEAM_VALUE ? rowTeam : teamF, projectCode);
|
||||||
stats.totalMH += val; pMap[name].total += val;
|
stats.totalMH += val; pMap[personKey].total += val;
|
||||||
let type = 'normal';
|
let type = 'normal';
|
||||||
if (isWeekend) { type = 'holiday'; pMap[name].holiday += val; pMap[name].holidayCount += 1; stats.extraHolMH += val; }
|
if (isWeekend) { type = 'holiday'; pMap[personKey].holiday += val; pMap[personKey].holidayCount += 1; stats.extraHolMH += val; }
|
||||||
|
|
||||||
else if (s.extra) { type = 'extra'; pMap[name].extra += val; pMap[name].extraCount += 1; stats.extraHolMH += val; }
|
else if (s.extra) { type = 'extra'; pMap[personKey].extra += val; pMap[personKey].extraCount += 1; stats.extraHolMH += val; }
|
||||||
|
|
||||||
else { pMap[name].normal += val; stats.normalMH += val; }
|
else { pMap[personKey].normal += val; stats.normalMH += val; }
|
||||||
|
|
||||||
if (!pMap[name].projects[pName]) {
|
if (!pMap[personKey].projects[pName]) {
|
||||||
pMap[name].projects[pName] = {
|
pMap[personKey].projects[pName] = {
|
||||||
normal: 0,
|
normal: 0,
|
||||||
extra: 0,
|
extra: 0,
|
||||||
holiday: 0,
|
holiday: 0,
|
||||||
@@ -2812,11 +2829,11 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pMap[name].projects[pName].total += val; pMap[name].projects[pName][type] += val;
|
pMap[personKey].projects[pName].total += val; pMap[personKey].projects[pName][type] += val;
|
||||||
if (type === 'extra') pMap[name].projects[pName].extraCount += 1;
|
if (type === 'extra') pMap[personKey].projects[pName].extraCount += 1;
|
||||||
if (type === 'holiday') pMap[name].projects[pName].holidayCount += 1;
|
if (type === 'holiday') pMap[personKey].projects[pName].holidayCount += 1;
|
||||||
if (!pMap[name].projects[pName].firstDate || d < pMap[name].projects[pName].firstDate) pMap[name].projects[pName].firstDate = d;
|
if (!pMap[personKey].projects[pName].firstDate || d < pMap[personKey].projects[pName].firstDate) pMap[personKey].projects[pName].firstDate = d;
|
||||||
if (!pMap[name].projects[pName].lastDate || d > pMap[name].projects[pName].lastDate) pMap[name].projects[pName].lastDate = d;
|
if (!pMap[personKey].projects[pName].lastDate || d > pMap[personKey].projects[pName].lastDate) pMap[personKey].projects[pName].lastDate = d;
|
||||||
|
|
||||||
if (!mMap[biz]) mMap[biz] = {};
|
if (!mMap[biz]) mMap[biz] = {};
|
||||||
|
|
||||||
@@ -2832,7 +2849,7 @@
|
|||||||
if (!mMap[biz][pName].ownership) mMap[biz][pName].ownership = ownershipInfo.ownership;
|
if (!mMap[biz][pName].ownership) mMap[biz][pName].ownership = ownershipInfo.ownership;
|
||||||
else if (ownershipInfo.ownership === '\uC8FC\uAD00') mMap[biz][pName].ownership = ownershipInfo.ownership;
|
else if (ownershipInfo.ownership === '\uC8FC\uAD00') mMap[biz][pName].ownership = ownershipInfo.ownership;
|
||||||
|
|
||||||
mMap[biz][pName].total += val; mMap[biz][pName].ps.add(name);
|
mMap[biz][pName].total += val; mMap[biz][pName].ps.add(personLabel);
|
||||||
|
|
||||||
const rKey = rank.includes("수석") ? "수석" : rank.includes("책임") ? "책임" : rank.includes("선임") ? "선임" : "연구원";
|
const rKey = rank.includes("수석") ? "수석" : rank.includes("책임") ? "책임" : rank.includes("선임") ? "선임" : "연구원";
|
||||||
|
|
||||||
@@ -2852,17 +2869,17 @@
|
|||||||
|
|
||||||
const wb = RANK_WEIGHTS[b.rank.includes('수석') ? '수석' : b.rank.includes('책임') ? '책임' : b.rank.includes('선임') ? '선임' : '연구원'] || 99;
|
const wb = RANK_WEIGHTS[b.rank.includes('수석') ? '수석' : b.rank.includes('책임') ? '책임' : b.rank.includes('선임') ? '선임' : '연구원'] || 99;
|
||||||
|
|
||||||
return (wa !== wb) ? wa - wb : a.name.localeCompare(b.name, 'ko');
|
return (wa !== wb) ? wa - wb : (a.displayName || a.name).localeCompare((b.displayName || b.name), 'ko');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
stats.personCount = teamList.length;
|
stats.personCount = teamList.length;
|
||||||
stats.memberNames = teamList.map(p => p.name);
|
stats.memberNames = teamList.map(p => ({ name: p.name, label: p.displayName || p.name }));
|
||||||
stats.lateNames = teamList.map(p => p.name).filter(name => latePeople.has(name));
|
stats.lateNames = teamList.filter(p => latePeople.has(p.name)).map(p => p.displayName || p.name);
|
||||||
stats.lateDetails = stats.lateNames.map(name => ({
|
stats.lateDetails = teamList.filter(p => latePeople.has(p.name)).map(p => ({
|
||||||
name,
|
name: p.name,
|
||||||
label: `${name}(${latePeopleMap[name]?.count || 0})`,
|
label: `${p.displayName || p.name}(${latePeopleMap[p.name]?.count || 0})`,
|
||||||
firstDate: latePeopleMap[name]?.firstDate || ''
|
firstDate: latePeopleMap[p.name]?.firstDate || ''
|
||||||
}));
|
}));
|
||||||
stats.lateCount = stats.lateNames.length;
|
stats.lateCount = stats.lateNames.length;
|
||||||
|
|
||||||
@@ -2872,6 +2889,9 @@
|
|||||||
|
|
||||||
const personRows = teamList.map(p => ({
|
const personRows = teamList.map(p => ({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
|
team: p.team,
|
||||||
|
displayName: p.displayName || p.name,
|
||||||
|
personKey: p.personKey || p.name,
|
||||||
rank: p.rank,
|
rank: p.rank,
|
||||||
normal: p.normal,
|
normal: p.normal,
|
||||||
extra: p.extra,
|
extra: p.extra,
|
||||||
@@ -3218,7 +3238,7 @@
|
|||||||
targetBadge.classList.add('hidden');
|
targetBadge.classList.add('hidden');
|
||||||
iconEl.setAttribute('data-lucide', 'layout-grid');
|
iconEl.setAttribute('data-lucide', 'layout-grid');
|
||||||
|
|
||||||
const rows = (personF ? personRows.filter(p => p.name === personF) : personRows) || [];
|
const rows = (personF ? personRows.filter(p => p.personKey === personF || p.name === personF) : personRows) || [];
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
container.innerHTML = '<div class="text-slate-300 font-bold italic p-10 text-center w-full">표시할 데이터가 없습니다.</div>';
|
container.innerHTML = '<div class="text-slate-300 font-bold italic p-10 text-center w-full">표시할 데이터가 없습니다.</div>';
|
||||||
return;
|
return;
|
||||||
@@ -3278,7 +3298,7 @@
|
|||||||
const over = total > summaryTargetH;
|
const over = total > summaryTargetH;
|
||||||
const overHour = Math.max(0, total - summaryTargetH);
|
const overHour = Math.max(0, total - summaryTargetH);
|
||||||
const projects = person.projects || [];
|
const projects = person.projects || [];
|
||||||
const personKey = encodeURIComponent(`${person.name}|${person.rank}`);
|
const personKey = encodeURIComponent(`${person.personKey || person.name}|${person.rank}`);
|
||||||
const isOpen = personF ? true : personChartOpenState[personKey] === true;
|
const isOpen = personF ? true : personChartOpenState[personKey] === true;
|
||||||
return `
|
return `
|
||||||
<div class="mh-person-row border border-slate-200 rounded-xl p-3">
|
<div class="mh-person-row border border-slate-200 rounded-xl p-3">
|
||||||
@@ -3286,7 +3306,7 @@
|
|||||||
<div class="mh-person-summary">
|
<div class="mh-person-summary">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base font-black text-slate-900 leading-tight">
|
<div class="text-base font-black text-slate-900 leading-tight">
|
||||||
${person.name} <span class="text-xs text-slate-500 ml-1">${person.rank}</span>${over ? `<span class="mh-person-over-inline"><i data-lucide="alert-circle" size="12"></i><span>초과 ${formatHour(overHour)}h</span></span>` : ''}
|
${person.displayName || person.name} <span class="text-xs text-slate-500 ml-1">${person.rank}</span>${over ? `<span class="mh-person-over-inline"><i data-lucide="alert-circle" size="12"></i><span>초과 ${formatHour(overHour)}h</span></span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mh-person-summary-right">
|
<div class="mh-person-summary-right">
|
||||||
|
|||||||
@@ -817,6 +817,20 @@ body {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-toolbar-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-toolbar-divider {
|
||||||
|
width: 1px;
|
||||||
|
align-self: stretch;
|
||||||
|
min-height: 36px;
|
||||||
|
background: #dbe2ea;
|
||||||
|
}
|
||||||
|
|
||||||
.list-mode-btn {
|
.list-mode-btn {
|
||||||
border: 1px solid #c7d2fe;
|
border: 1px solid #c7d2fe;
|
||||||
background: #eef2ff;
|
background: #eef2ff;
|
||||||
|
|||||||
@@ -1393,12 +1393,16 @@ function openListViewModal(event) {
|
|||||||
fieldsArea.innerHTML = `
|
fieldsArea.innerHTML = `
|
||||||
<div class="list-toolbar">
|
<div class="list-toolbar">
|
||||||
<div class="list-toolbar-row">
|
<div class="list-toolbar-row">
|
||||||
|
<div class="list-toolbar-group">
|
||||||
<button type="button" onclick="showCurrentListView()" class="list-mode-btn">현재 명단</button>
|
<button type="button" onclick="showCurrentListView()" class="list-mode-btn">현재 명단</button>
|
||||||
<div class="list-date-group">
|
</div>
|
||||||
|
<div class="list-toolbar-divider" aria-hidden="true"></div>
|
||||||
|
<div class="list-toolbar-group list-date-group">
|
||||||
<input type="date" id="list-snapshot-date" value="${escapeHtml(defaultDate)}" class="list-date-input">
|
<input type="date" id="list-snapshot-date" value="${escapeHtml(defaultDate)}" class="list-date-input">
|
||||||
<button type="button" onclick="loadSnapshotListView()" class="list-mode-btn">기준일 조회</button>
|
<button type="button" onclick="loadSnapshotListView()" class="list-mode-btn">기준일 조회</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-date-group">
|
<div class="list-toolbar-divider" aria-hidden="true"></div>
|
||||||
|
<div class="list-toolbar-group list-date-group">
|
||||||
<input type="date" id="list-compare-from" value="${escapeHtml(defaultDate)}" class="list-date-input">
|
<input type="date" id="list-compare-from" value="${escapeHtml(defaultDate)}" class="list-date-input">
|
||||||
<span class="list-date-separator">~</span>
|
<span class="list-date-separator">~</span>
|
||||||
<input type="date" id="list-compare-to" value="${escapeHtml(defaultDate)}" class="list-date-input">
|
<input type="date" id="list-compare-to" value="${escapeHtml(defaultDate)}" class="list-date-input">
|
||||||
|
|||||||
Reference in New Issue
Block a user