Update cost-pdf.html: Refined team distribution logic and naming conventions.
This commit is contained in:
138
cost-pdf.html
138
cost-pdf.html
@@ -159,14 +159,14 @@
|
|||||||
};
|
};
|
||||||
const NON_MANAGED_FORMS = ['가족사지원', '공통', '기타', '시설관리', '연구개발', '현장자재', '현장지원', '공통(거더)', '공통(데크,가로보)', '품질'];
|
const NON_MANAGED_FORMS = ['가족사지원', '공통', '기타', '시설관리', '연구개발', '현장자재', '현장지원', '공통(거더)', '공통(데크,가로보)', '품질'];
|
||||||
const FORM_ALLOC_C_RULES = {
|
const FORM_ALLOC_C_RULES = {
|
||||||
'가족사지원': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
'가족사지원': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
||||||
'공통': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
'공통': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
||||||
'기타': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
'기타': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
||||||
'시설관리': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
'시설관리': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
||||||
'연구개발': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
'연구개발': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
||||||
'현장자재': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
'현장자재': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
||||||
'현장지원': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
'현장지원': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '거푸집', '가로보', '가설벤트'],
|
||||||
'공통(거더)': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'RFI.거더', 'RFI.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더'],
|
'공통(거더)': ['노출거더', '분절거더', '철도교', 'DPC.거더', 'DR.거더', 'DR.거더(v2.0)', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', 'SP.거더'],
|
||||||
'공통(데크,가로보)': ['강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '가로보'],
|
'공통(데크,가로보)': ['강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '가로보'],
|
||||||
'품질': ['노출거더', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '가로보']
|
'품질': ['노출거더', 'Inverted-T', 'Post-Tension 거더', 'Pre Beam', '강교 Deck', '캔틸레버Deck', 'Full-DepthDeck', 'PSCDeck', 'RCDeck', '가로보']
|
||||||
};
|
};
|
||||||
@@ -730,7 +730,20 @@
|
|||||||
const tmMap = {};
|
const tmMap = {};
|
||||||
settledProjects.forEach(p => {
|
settledProjects.forEach(p => {
|
||||||
Object.entries(p.byTeam).forEach(([tm, val]) => {
|
Object.entries(p.byTeam).forEach(([tm, val]) => {
|
||||||
if (!tmMap[tm]) tmMap[tm] = { name: tm, direct: 0, allocA: 0, allocB: 0, final: 0, hours: 0, breakdown: {}, allocTrace: [] };
|
if (!tmMap[tm]) tmMap[tm] = {
|
||||||
|
name: tm,
|
||||||
|
direct: 0,
|
||||||
|
allocA: 0,
|
||||||
|
allocB: 0,
|
||||||
|
final: 0,
|
||||||
|
hours: 0,
|
||||||
|
breakdown: {},
|
||||||
|
allocTrace: [],
|
||||||
|
allocBucket: {
|
||||||
|
A: { '지급임차료': 0, '전력비': 0, '일반경비': 0 },
|
||||||
|
B: { '지급임차료': 0, '전력비': 0, '일반경비': 0 }
|
||||||
|
}
|
||||||
|
};
|
||||||
tmMap[tm].direct += val;
|
tmMap[tm].direct += val;
|
||||||
tmMap[tm].hours += p.direct > 0 ? (p.hours * (val / p.direct)) : 0;
|
tmMap[tm].hours += p.direct > 0 ? (p.hours * (val / p.direct)) : 0;
|
||||||
tmMap[tm].breakdown[p.name] = (tmMap[tm].breakdown[p.name] || 0) + val;
|
tmMap[tm].breakdown[p.name] = (tmMap[tm].breakdown[p.name] || 0) + val;
|
||||||
@@ -738,14 +751,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const ensureTeam = (name) => {
|
const ensureTeam = (name) => {
|
||||||
if (!tmMap[name]) tmMap[name] = { name, direct: 0, allocA: 0, allocB: 0, final: 0, hours: 0, breakdown: {}, allocTrace: [] };
|
if (!tmMap[name]) tmMap[name] = {
|
||||||
|
name, direct: 0, allocA: 0, allocB: 0, final: 0, hours: 0, breakdown: {}, allocTrace: [],
|
||||||
|
allocBucket: {
|
||||||
|
A: { '지급임차료': 0, '전력비': 0, '일반경비': 0 },
|
||||||
|
B: { '지급임차료': 0, '전력비': 0, '일반경비': 0 }
|
||||||
|
}
|
||||||
|
};
|
||||||
return tmMap[name];
|
return tmMap[name];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 팀별 하이브리드 배분:
|
|
||||||
// 1) 공통 성격 풀은 팀 공수 비례
|
|
||||||
// 2) 관리 성격 풀은 계정별 고정비율
|
|
||||||
const commonPool = { A: 0, B: 0 };
|
|
||||||
const bucketA = { '지급임차료': 0, '전력비': 0, '일반경비': 0 };
|
const bucketA = { '지급임차료': 0, '전력비': 0, '일반경비': 0 };
|
||||||
const bucketB = { '지급임차료': 0, '전력비': 0, '일반경비': 0 };
|
const bucketB = { '지급임차료': 0, '전력비': 0, '일반경비': 0 };
|
||||||
const classifyAccount = (account) => {
|
const classifyAccount = (account) => {
|
||||||
@@ -756,72 +771,35 @@
|
|||||||
if (compact.includes('전력비') || compact.includes('전기료') || compact.includes('815')) return '전력비';
|
if (compact.includes('전력비') || compact.includes('전기료') || compact.includes('815')) return '전력비';
|
||||||
return '일반경비';
|
return '일반경비';
|
||||||
};
|
};
|
||||||
const isCommonPoolProject = (projectName) => String(projectName || '').includes('공통');
|
|
||||||
|
|
||||||
fEx.forEach(e => {
|
fEx.forEach(e => {
|
||||||
const bucket = classifyAccount(e.account);
|
const bucket = classifyAccount(e.account);
|
||||||
if (isA(e.projectName)) {
|
if (isA(e.projectName)) {
|
||||||
if (isCommonPoolProject(e.projectName)) commonPool.A += e.amount;
|
bucketA[bucket] += e.amount;
|
||||||
else bucketA[bucket] += e.amount;
|
|
||||||
} else if (isB(e.projectName)) {
|
} else if (isB(e.projectName)) {
|
||||||
if (isCommonPoolProject(e.projectName)) commonPool.B += e.amount;
|
bucketB[bucket] += e.amount;
|
||||||
else bucketB[bucket] += e.amount;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
laborWithCost.forEach(l => {
|
laborWithCost.forEach(l => {
|
||||||
if (isA(l.projectName)) {
|
if (isA(l.projectName)) {
|
||||||
if (isCommonPoolProject(l.projectName)) commonPool.A += l.cost;
|
bucketA['일반경비'] += l.cost;
|
||||||
else bucketA['일반경비'] += l.cost;
|
|
||||||
} else if (isB(l.projectName)) {
|
} else if (isB(l.projectName)) {
|
||||||
if (isCommonPoolProject(l.projectName)) commonPool.B += l.cost;
|
bucketB['일반경비'] += l.cost;
|
||||||
else bucketB['일반경비'] += l.cost;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 토글 OFF 상태일 때 분배 제외
|
|
||||||
if (!allocPoolA) commonPool.A = 0;
|
|
||||||
if (!allocPoolB) commonPool.B = 0;
|
|
||||||
if (!allocPoolA) Object.keys(bucketA).forEach(k => { bucketA[k] = 0; });
|
if (!allocPoolA) Object.keys(bucketA).forEach(k => { bucketA[k] = 0; });
|
||||||
if (!allocPoolB) Object.keys(bucketB).forEach(k => { bucketB[k] = 0; });
|
if (!allocPoolB) Object.keys(bucketB).forEach(k => { bucketB[k] = 0; });
|
||||||
|
|
||||||
const teamHoursMap = {};
|
|
||||||
let totalTeamHours = 0;
|
|
||||||
laborWithCost.forEach(l => {
|
|
||||||
if (isA(l.projectName) || isB(l.projectName)) return;
|
|
||||||
const t = normalizeTeamName(l.team);
|
|
||||||
if (t === '관리팀') return;
|
|
||||||
teamHoursMap[t] = (teamHoursMap[t] || 0) + (Number(l.hours) || 0);
|
|
||||||
totalTeamHours += (Number(l.hours) || 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
const distributeCommonByHours = (poolName, amount, allocKey) => {
|
|
||||||
if (!amount || totalTeamHours <= 0) return;
|
|
||||||
Object.entries(teamHoursMap).forEach(([team, hrs]) => {
|
|
||||||
const ratio = hrs / totalTeamHours;
|
|
||||||
const share = amount * ratio;
|
|
||||||
const row = ensureTeam(team);
|
|
||||||
row[allocKey] += share;
|
|
||||||
row.allocTrace.push({
|
|
||||||
projectName: `${poolName}/공통(공수)`,
|
|
||||||
directShare: amount,
|
|
||||||
ratio,
|
|
||||||
projectHours: totalTeamHours,
|
|
||||||
shareHours: hrs,
|
|
||||||
allocA: allocKey === 'allocA' ? share : 0,
|
|
||||||
allocB: allocKey === 'allocB' ? share : 0
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const distributeManagedByRatio = (poolName, bucketMap, allocKey) => {
|
const distributeManagedByRatio = (poolName, bucketMap, allocKey) => {
|
||||||
Object.entries(bucketMap).forEach(([bucket, amount]) => {
|
Object.entries(bucketMap).forEach(([bucket, amount]) => {
|
||||||
if (!amount) return;
|
if (!amount) return;
|
||||||
const ratios = TEAM_RATIOS[bucket] || TEAM_RATIOS['일반경비'];
|
const ratios = TEAM_RATIOS[bucket] || TEAM_RATIOS['일반경비'];
|
||||||
Object.entries(ratios).forEach(([team, ratio]) => {
|
Object.entries(ratios).forEach(([team, ratio]) => {
|
||||||
const share = amount * ratio;
|
const share = amount * ratio;
|
||||||
const row = ensureTeam(team);
|
const row = ensureTeam(normalizeTeamName(team));
|
||||||
row[allocKey] += share;
|
row[allocKey] += share;
|
||||||
|
row.allocBucket[allocKey === 'allocA' ? 'A' : 'B'][bucket] += share;
|
||||||
row.allocTrace.push({
|
row.allocTrace.push({
|
||||||
projectName: `${poolName}/${bucket}`,
|
projectName: `${poolName}/${bucket}`,
|
||||||
directShare: amount,
|
directShare: amount,
|
||||||
@@ -835,8 +813,6 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
distributeCommonByHours('POOL A', commonPool.A, 'allocA');
|
|
||||||
distributeCommonByHours('POOL B', commonPool.B, 'allocB');
|
|
||||||
distributeManagedByRatio('POOL A', bucketA, 'allocA');
|
distributeManagedByRatio('POOL A', bucketA, 'allocA');
|
||||||
distributeManagedByRatio('POOL B', bucketB, 'allocB');
|
distributeManagedByRatio('POOL B', bucketB, 'allocB');
|
||||||
|
|
||||||
@@ -1484,11 +1460,54 @@
|
|||||||
<h4 className="font-bold text-slate-800 text-base uppercase tracking-tighter italic">A/B 배분 계산과정</h4>
|
<h4 className="font-bold text-slate-800 text-base uppercase tracking-tighter italic">A/B 배분 계산과정</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-indigo-50 border border-indigo-100 rounded-2xl p-4 text-xs text-slate-700 leading-relaxed">
|
<div className="bg-indigo-50 border border-indigo-100 rounded-2xl p-4 text-xs text-slate-700 leading-relaxed">
|
||||||
|
{viewMode === 'team' ? (
|
||||||
|
<>
|
||||||
|
<div>기준식(팀별): <span className="font-black">지급임차료/전력비/일반경비 3개 계정 비율 분배</span></div>
|
||||||
|
<div className="mt-1">
|
||||||
|
A = 3개 계정 비율 합계 = <span className="font-black text-blue-700">{utils.formatWon(selectedDetail.allocA || 0)}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
B = 3개 계정 비율 합계 = <span className="font-black text-orange-700">{utils.formatWon(selectedDetail.allocB || 0)}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div>기준식: <span className="font-black">배분금액 = (배분공수 / 전체 수익공수) × 배분 기준 금액</span></div>
|
<div>기준식: <span className="font-black">배분금액 = (배분공수 / 전체 수익공수) × 배분 기준 금액</span></div>
|
||||||
<div className="mt-1">A = ({utils.formatHr(selectedDetail.hours || 0)} / {utils.formatHr(selectedDetail.allocMeta.revenueHrs || 0)}) × {utils.formatWon(selectedDetail.allocMeta.poolAVal || 0)} = <span className="font-black text-blue-700">{utils.formatWon(selectedDetail.allocA || 0)}</span></div>
|
<div className="mt-1">A = ({utils.formatHr(selectedDetail.hours || 0)} / {utils.formatHr(selectedDetail.allocMeta.revenueHrs || 0)}) × {utils.formatWon(selectedDetail.allocMeta.poolAVal || 0)} = <span className="font-black text-blue-700">{utils.formatWon(selectedDetail.allocA || 0)}</span></div>
|
||||||
<div>B = ({utils.formatHr(selectedDetail.hours || 0)} / {utils.formatHr(selectedDetail.allocMeta.revenueHrs || 0)}) × {utils.formatWon(selectedDetail.allocMeta.poolBVal || 0)} = <span className="font-black text-orange-700">{utils.formatWon(selectedDetail.allocB || 0)}</span></div>
|
<div>B = ({utils.formatHr(selectedDetail.hours || 0)} / {utils.formatHr(selectedDetail.allocMeta.revenueHrs || 0)}) × {utils.formatWon(selectedDetail.allocMeta.poolBVal || 0)} = <span className="font-black text-orange-700">{utils.formatWon(selectedDetail.allocB || 0)}</span></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-auto border border-slate-200 rounded-xl">
|
<div className="overflow-auto border border-slate-200 rounded-xl">
|
||||||
|
{viewMode === 'team' ? (
|
||||||
|
<table className="w-full min-w-[560px] text-[11px]">
|
||||||
|
<thead className="bg-slate-50 text-slate-500">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left">구분</th>
|
||||||
|
<th className="px-3 py-2 text-right text-blue-700">A 배분</th>
|
||||||
|
<th className="px-3 py-2 text-right text-orange-700">B 배분</th>
|
||||||
|
<th className="px-3 py-2 text-right">합계</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-100">
|
||||||
|
{['일반경비', '전력비', '지급임차료'].map((k) => {
|
||||||
|
const a = selectedDetail.allocBucket?.A?.[k] || 0;
|
||||||
|
const b = selectedDetail.allocBucket?.B?.[k] || 0;
|
||||||
|
const teamKeyRaw = String(selectedDetail.name || '');
|
||||||
|
const teamKey = TEAM_RATIOS[k]?.[teamKeyRaw] !== undefined ? teamKeyRaw : teamKeyRaw.replace(/팀$/, '');
|
||||||
|
const pct = ((TEAM_RATIOS[k]?.[teamKey] || 0) * 100).toFixed(0);
|
||||||
|
return (
|
||||||
|
<tr key={`${selectedDetail.name}_bucket_${k}`}>
|
||||||
|
<td className="px-3 py-2 font-bold text-slate-700">{k} <span className="text-slate-400">({pct}%)</span></td>
|
||||||
|
<td className="px-3 py-2 text-right text-blue-700">{utils.formatWon(a)}</td>
|
||||||
|
<td className="px-3 py-2 text-right text-orange-700">{utils.formatWon(b)}</td>
|
||||||
|
<td className="px-3 py-2 text-right font-black">{utils.formatWon(a + b)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
<table className="w-full min-w-[760px] text-[11px]">
|
<table className="w-full min-w-[760px] text-[11px]">
|
||||||
<thead className="bg-slate-50 text-slate-500">
|
<thead className="bg-slate-50 text-slate-500">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1513,6 +1532,7 @@
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user