feat: add all-team filter to mh analysis
This commit is contained in:
@@ -1481,7 +1481,8 @@
|
||||
|
||||
const RANK_WEIGHTS = { '수석': 1, '책임': 2, '선임': 3, '연구원': 4 };
|
||||
|
||||
let teamData = [];
|
||||
let teamData = [];
|
||||
const ALL_TEAM_VALUE = '__ALL__';
|
||||
let allTeams = [];
|
||||
let allPeopleData = [];
|
||||
let searchTeams = [];
|
||||
@@ -1637,16 +1638,20 @@
|
||||
const buildScopedPeopleData = (rows) => {
|
||||
const seen = new Set();
|
||||
return rows.map(r => ({
|
||||
name: String(r[columnMap.name] || '').trim(),
|
||||
team: String(r[columnMap.team] || '').trim()
|
||||
})).filter(p => {
|
||||
if (!p.name || !p.team) return false;
|
||||
const key = `${p.name}-${p.team}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
name: String(r[columnMap.name] || '').trim(),
|
||||
team: String(r[columnMap.team] || '').trim()
|
||||
})).filter(p => {
|
||||
if (!p.name || !p.team) return false;
|
||||
const key = `${p.name}-${p.team}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
}).map(p => ({
|
||||
...p,
|
||||
value: `${p.name}|${p.team}`,
|
||||
label: `${p.name} (${p.team})`
|
||||
}));
|
||||
};
|
||||
const findBizIndexByProject = (normalizedHeaders, projIdx) => {
|
||||
const bizKey = normalizeHeader('사업 종류');
|
||||
for (let i = projIdx - 1; i >= 0; i--) {
|
||||
@@ -2465,23 +2470,23 @@
|
||||
searchTeams = [...new Set(allRows.map(r => String(r[columnMap.team] || '').trim()))].filter(Boolean).sort();
|
||||
searchPeopleData = buildScopedPeopleData(allRows);
|
||||
|
||||
teamSelect.innerHTML = '<option value="">팀 선택</option>' + allTeams.map(t => `<option value="${t}">${t}</option>`).join('');
|
||||
|
||||
if (currentTeam && allTeams.includes(currentTeam)) {
|
||||
teamSelect.value = currentTeam;
|
||||
} else if (allTeams.length > 0) {
|
||||
teamSelect.value = allTeams[0];
|
||||
} else {
|
||||
teamSelect.value = '';
|
||||
}
|
||||
teamSelect.innerHTML = `<option value="${ALL_TEAM_VALUE}">전체</option>` + allTeams.map(t => `<option value="${t}">${t}</option>`).join('');
|
||||
|
||||
if (currentTeam === ALL_TEAM_VALUE) {
|
||||
teamSelect.value = ALL_TEAM_VALUE;
|
||||
} else if (currentTeam && allTeams.includes(currentTeam)) {
|
||||
teamSelect.value = currentTeam;
|
||||
} else {
|
||||
teamSelect.value = ALL_TEAM_VALUE;
|
||||
}
|
||||
|
||||
updateFilters();
|
||||
|
||||
if (teamSelect.value && currentPerson && allPeopleData.some(p => p.team === teamSelect.value && p.name === currentPerson)) {
|
||||
personSelect.value = currentPerson;
|
||||
} else {
|
||||
personSelect.value = '';
|
||||
}
|
||||
if (currentPerson && [...personSelect.options].some(option => option.value === currentPerson)) {
|
||||
personSelect.value = currentPerson;
|
||||
} else {
|
||||
personSelect.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function setScope(scope, preserveSelection = true) {
|
||||
@@ -2621,7 +2626,10 @@
|
||||
setScope(getTeamScope(item.dataset.team), false);
|
||||
teamSelect.value = item.dataset.team;
|
||||
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 = "";
|
||||
@@ -2646,19 +2654,25 @@
|
||||
|
||||
|
||||
|
||||
function updateFilters() {
|
||||
if (teamData.length === 0) return;
|
||||
const team = document.getElementById('team-select').value;
|
||||
const personSelect = document.getElementById('person-select');
|
||||
if (!team) {
|
||||
function updateFilters() {
|
||||
if (teamData.length === 0) return;
|
||||
const team = document.getElementById('team-select').value;
|
||||
const personSelect = document.getElementById('person-select');
|
||||
if (!team) {
|
||||
document.getElementById('main-title').innerText = '';
|
||||
personSelect.innerHTML = '<option value="">팀원을 선택하세요</option>';
|
||||
return;
|
||||
}
|
||||
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();
|
||||
personSelect.innerHTML = '<option value="">팀원을 선택하세요</option>' + teamPeople.map(p => `<option value="${p}">${p}</option>`).join('');
|
||||
}
|
||||
personSelect.innerHTML = '<option value="">팀원을 선택하세요</option>';
|
||||
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;
|
||||
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('');
|
||||
}
|
||||
|
||||
function resetMhView() {
|
||||
if (teamData.length < 2) return;
|
||||
@@ -2759,14 +2773,17 @@
|
||||
const latePeople = new Set();
|
||||
const latePeopleMap = {};
|
||||
|
||||
teamData.slice(1).forEach(row => {
|
||||
const d = dStr(row[columnMap.date]);
|
||||
if (!d || d < startStr || d > endStr || !isRowInScope(row) || String(row[columnMap.team] || '').trim() !== teamF) return;
|
||||
teamData.slice(1).forEach(row => {
|
||||
const d = dStr(row[columnMap.date]);
|
||||
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 userStateText = String(row[columnMap.userState] || '').trim();
|
||||
const isWeekend = lateFlagText.includes("\uC8FC\uB9D0");
|
||||
const name = String(row[columnMap.name] || '').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 sourceNormalLaborCost = columnMap.normalCost >= 0 ? parseNumber(row[columnMap.normalCost]) : 0;
|
||||
const sourceExtraLaborCost = columnMap.extraCost >= 0 ? parseNumber(row[columnMap.extraCost]) : 0;
|
||||
@@ -2783,24 +2800,24 @@
|
||||
stats.normalLaborCost += normalLaborCost;
|
||||
stats.extraLaborCost += 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 => {
|
||||
const val = parseNumber(row[s.time]);
|
||||
const projectCode = (s.code >= 0 ? String(row[s.code] || '') : '').trim();
|
||||
const pName = String(row[s.proj] || '').trim();
|
||||
const biz = (s.biz >= 0 ? String(row[s.biz] || '') : '').trim() || '공통';
|
||||
if (val > 0 && pName) {
|
||||
const ownershipInfo = getProjectOwnership(teamF, projectCode);
|
||||
stats.totalMH += val; pMap[name].total += val;
|
||||
let type = 'normal';
|
||||
if (isWeekend) { type = 'holiday'; pMap[name].holiday += val; pMap[name].holidayCount += 1; stats.extraHolMH += val; }
|
||||
const ownershipInfo = getProjectOwnership(teamF === ALL_TEAM_VALUE ? rowTeam : teamF, projectCode);
|
||||
stats.totalMH += val; pMap[personKey].total += val;
|
||||
let type = 'normal';
|
||||
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 { pMap[name].normal += val; stats.normalMH += val; }
|
||||
|
||||
if (!pMap[name].projects[pName]) {
|
||||
pMap[name].projects[pName] = {
|
||||
else if (s.extra) { type = 'extra'; pMap[personKey].extra += val; pMap[personKey].extraCount += 1; stats.extraHolMH += val; }
|
||||
|
||||
else { pMap[personKey].normal += val; stats.normalMH += val; }
|
||||
|
||||
if (!pMap[personKey].projects[pName]) {
|
||||
pMap[personKey].projects[pName] = {
|
||||
normal: 0,
|
||||
extra: 0,
|
||||
holiday: 0,
|
||||
@@ -2812,11 +2829,11 @@
|
||||
};
|
||||
}
|
||||
|
||||
pMap[name].projects[pName].total += val; pMap[name].projects[pName][type] += val;
|
||||
if (type === 'extra') pMap[name].projects[pName].extraCount += 1;
|
||||
if (type === 'holiday') pMap[name].projects[pName].holidayCount += 1;
|
||||
if (!pMap[name].projects[pName].firstDate || d < pMap[name].projects[pName].firstDate) pMap[name].projects[pName].firstDate = d;
|
||||
if (!pMap[name].projects[pName].lastDate || d > pMap[name].projects[pName].lastDate) pMap[name].projects[pName].lastDate = d;
|
||||
pMap[personKey].projects[pName].total += val; pMap[personKey].projects[pName][type] += val;
|
||||
if (type === 'extra') pMap[personKey].projects[pName].extraCount += 1;
|
||||
if (type === 'holiday') pMap[personKey].projects[pName].holidayCount += 1;
|
||||
if (!pMap[personKey].projects[pName].firstDate || d < pMap[personKey].projects[pName].firstDate) pMap[personKey].projects[pName].firstDate = d;
|
||||
if (!pMap[personKey].projects[pName].lastDate || d > pMap[personKey].projects[pName].lastDate) pMap[personKey].projects[pName].lastDate = d;
|
||||
|
||||
if (!mMap[biz]) mMap[biz] = {};
|
||||
|
||||
@@ -2832,7 +2849,7 @@
|
||||
if (!mMap[biz][pName].ownership) 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("선임") ? "선임" : "연구원";
|
||||
|
||||
@@ -2852,17 +2869,17 @@
|
||||
|
||||
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.memberNames = teamList.map(p => p.name);
|
||||
stats.lateNames = teamList.map(p => p.name).filter(name => latePeople.has(name));
|
||||
stats.lateDetails = stats.lateNames.map(name => ({
|
||||
name,
|
||||
label: `${name}(${latePeopleMap[name]?.count || 0})`,
|
||||
firstDate: latePeopleMap[name]?.firstDate || ''
|
||||
stats.memberNames = teamList.map(p => ({ name: p.name, label: p.displayName || p.name }));
|
||||
stats.lateNames = teamList.filter(p => latePeople.has(p.name)).map(p => p.displayName || p.name);
|
||||
stats.lateDetails = teamList.filter(p => latePeople.has(p.name)).map(p => ({
|
||||
name: p.name,
|
||||
label: `${p.displayName || p.name}(${latePeopleMap[p.name]?.count || 0})`,
|
||||
firstDate: latePeopleMap[p.name]?.firstDate || ''
|
||||
}));
|
||||
stats.lateCount = stats.lateNames.length;
|
||||
|
||||
@@ -2870,8 +2887,11 @@
|
||||
|
||||
|
||||
|
||||
const personRows = teamList.map(p => ({
|
||||
const personRows = teamList.map(p => ({
|
||||
name: p.name,
|
||||
team: p.team,
|
||||
displayName: p.displayName || p.name,
|
||||
personKey: p.personKey || p.name,
|
||||
rank: p.rank,
|
||||
normal: p.normal,
|
||||
extra: p.extra,
|
||||
@@ -3218,7 +3238,7 @@
|
||||
targetBadge.classList.add('hidden');
|
||||
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) {
|
||||
container.innerHTML = '<div class="text-slate-300 font-bold italic p-10 text-center w-full">표시할 데이터가 없습니다.</div>';
|
||||
return;
|
||||
@@ -3278,7 +3298,7 @@
|
||||
const over = total > summaryTargetH;
|
||||
const overHour = Math.max(0, total - summaryTargetH);
|
||||
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;
|
||||
return `
|
||||
<div class="mh-person-row border border-slate-200 rounded-xl p-3">
|
||||
@@ -3286,7 +3306,7 @@
|
||||
<div class="mh-person-summary">
|
||||
<div>
|
||||
<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 class="mh-person-summary-right">
|
||||
|
||||
Reference in New Issue
Block a user