Refine project status workflows and dashboards
This commit is contained in:
@@ -134,7 +134,7 @@
|
||||
|
||||
<section class="panel">
|
||||
<div class="section-title">
|
||||
<h2>수익/비용/영업수지 그래프</h2>
|
||||
<h2>수금/비용/영업수지 그래프</h2>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="legend chart-legend" id="balanceLegend"></div>
|
||||
@@ -162,7 +162,7 @@
|
||||
};
|
||||
|
||||
const labels = {
|
||||
revenue_sum: "수익",
|
||||
revenue_sum: "수금",
|
||||
project_cost_sum: "원가(프로젝트)",
|
||||
support_cost_sum: "원가(지원부서)",
|
||||
support_sga_sum: "판관비(지원부서)",
|
||||
@@ -180,16 +180,18 @@
|
||||
function formatAxisLabel(value) {
|
||||
const numeric = Number(value || 0);
|
||||
if (!numeric) return "0.0";
|
||||
if (numeric >= 100000000) return `${(numeric / 100000000).toFixed(1)}억`;
|
||||
if (numeric >= 1000000) return `${(numeric / 1000000).toFixed(1)}백만`;
|
||||
if (numeric >= 1000) return `${(numeric / 1000).toFixed(1)}천`;
|
||||
const sign = numeric < 0 ? "-" : "";
|
||||
const absolute = Math.abs(numeric);
|
||||
if (absolute >= 100000000) return `${sign}${(absolute / 100000000).toFixed(1)}억`;
|
||||
if (absolute >= 1000000) return `${sign}${(absolute / 1000000).toFixed(1)}백만`;
|
||||
if (absolute >= 1000) return `${sign}${(absolute / 1000).toFixed(1)}천`;
|
||||
return formatNumber(numeric);
|
||||
}
|
||||
|
||||
function pickTickStep(maxValue) {
|
||||
const baseUnit = maxValue < 1000000 ? 1000 : 1000000;
|
||||
const units = [1, 2, 5];
|
||||
const raw = Math.max(maxValue / 5 / baseUnit, 1);
|
||||
const raw = Math.max(maxValue / baseUnit, 1);
|
||||
let power = 1;
|
||||
while (power * 10 <= raw) power *= 10;
|
||||
for (const unit of units) {
|
||||
@@ -199,6 +201,14 @@
|
||||
return power * 10 * baseUnit;
|
||||
}
|
||||
|
||||
function buildPositiveAxisScale(maxValue, tickCount = 4) {
|
||||
const safeMax = Math.max(Number(maxValue || 0), 1);
|
||||
const paddedMax = safeMax * (safeMax < 1000 ? 1.12 : 1.08);
|
||||
const tickStep = pickTickStep(paddedMax / tickCount);
|
||||
const tickMax = Math.max(tickStep, Math.ceil(paddedMax / tickStep) * tickStep);
|
||||
return { tickStep, tickMax };
|
||||
}
|
||||
|
||||
function renderLegend(targetId, keys) {
|
||||
const target = document.getElementById(targetId);
|
||||
if (!target) return;
|
||||
@@ -271,8 +281,7 @@
|
||||
}
|
||||
|
||||
function buildAxis(maxValue, width, height, margin) {
|
||||
const tickStep = pickTickStep(maxValue);
|
||||
const tickMax = Math.ceil(maxValue / tickStep) * tickStep;
|
||||
const { tickStep, tickMax } = buildPositiveAxisScale(maxValue, 4);
|
||||
let axis = "";
|
||||
for (let value = 0; value <= tickMax; value += tickStep) {
|
||||
const y = height - margin.bottom - ((height - margin.top - margin.bottom) * value) / tickMax;
|
||||
@@ -283,6 +292,21 @@
|
||||
return { axis, tickMax };
|
||||
}
|
||||
|
||||
function buildSignedAxis(maxPositiveValue, minNegativeValue, width, height, margin) {
|
||||
const rangeMax = Math.max(Math.abs(maxPositiveValue || 0), Math.abs(minNegativeValue || 0), 1);
|
||||
const { tickStep, tickMax } = buildPositiveAxisScale(rangeMax, 4);
|
||||
const plotHeight = height - margin.top - margin.bottom;
|
||||
const zeroY = margin.top + (plotHeight * tickMax) / (tickMax * 2);
|
||||
let axis = "";
|
||||
for (let value = -tickMax; value <= tickMax; value += tickStep) {
|
||||
const y = zeroY - (plotHeight * value) / (tickMax * 2);
|
||||
axis += `<line x1="${margin.left}" y1="${y}" x2="${width - margin.right}" y2="${y}" stroke="#d8dee5" stroke-dasharray="3 7" />`;
|
||||
axis += `<text x="${margin.left - 12}" y="${y + 4}" text-anchor="end" fill="#5a6672" font-size="11" font-weight="700">${formatAxisLabel(value)}</text>`;
|
||||
}
|
||||
axis += `<line x1="${margin.left}" y1="${zeroY}" x2="${width - margin.right}" y2="${zeroY}" stroke="#8ba0ae" stroke-width="1.2" />`;
|
||||
return { axis, tickMax, zeroY };
|
||||
}
|
||||
|
||||
function renderEmptyChart(svgId, message) {
|
||||
const svg = document.getElementById(svgId);
|
||||
if (!svg) return;
|
||||
@@ -375,12 +399,13 @@
|
||||
const margin = { top: 30, right: 24, bottom: 74, left: 98 };
|
||||
const plotWidth = width - margin.left - margin.right;
|
||||
const plotHeight = height - margin.top - margin.bottom;
|
||||
const maxValue = Math.max(...series.flatMap((item) => [
|
||||
const maxPositiveValue = Math.max(...series.flatMap((item) => [
|
||||
item.revenue_sum || 0,
|
||||
item.total_expense || 0,
|
||||
Math.abs(item.operating_balance || 0),
|
||||
Math.max(item.operating_balance || 0, 0),
|
||||
]), 1);
|
||||
const { axis, tickMax } = buildAxis(maxValue, width, height, margin);
|
||||
const minNegativeValue = Math.min(...series.map((item) => Math.min(item.operating_balance || 0, 0)), 0);
|
||||
const { axis, tickMax, zeroY } = buildSignedAxis(maxPositiveValue, minNegativeValue, width, height, margin);
|
||||
const groupWidth = plotWidth / Math.max(series.length, 1);
|
||||
const groupGap = groupWidth * 0.22;
|
||||
const innerGap = 0;
|
||||
@@ -399,13 +424,17 @@
|
||||
series.forEach((item, index) => {
|
||||
const baseX = margin.left + index * groupWidth;
|
||||
metrics.forEach((key, metricIndex) => {
|
||||
const value = key === "operating_balance" ? Math.abs(item[key] || 0) : (item[key] || 0);
|
||||
const barHeight = (plotHeight * value) / tickMax;
|
||||
const rawValue = Number(item[key] || 0);
|
||||
const value = key === "operating_balance" ? rawValue : Math.max(rawValue, 0);
|
||||
const barHeight = (plotHeight * Math.abs(value)) / (tickMax * 2);
|
||||
const x = baseX + groupStartOffset + metricIndex * (barWidth + innerGap);
|
||||
const y = height - margin.bottom - barHeight;
|
||||
const y = value < 0 ? zeroY : zeroY - barHeight;
|
||||
markup += `<rect x="${x}" y="${y}" width="${barWidth}" height="${barHeight}" rx="2" fill="${palette[key]}" filter="url(#balanceShadow)" />`;
|
||||
if (value > 0) {
|
||||
markup += `<text x="${x + barWidth / 2}" y="${Math.max(y - 8, margin.top + 12)}" text-anchor="middle" fill="#314555" font-size="9.5" font-weight="800">${formatAxisLabel(value)}</text>`;
|
||||
if (value !== 0) {
|
||||
const labelY = value < 0
|
||||
? Math.min(y + barHeight + 14, height - margin.bottom + 6)
|
||||
: Math.max(y - 8, margin.top + 12);
|
||||
markup += `<text x="${x + barWidth / 2}" y="${labelY}" text-anchor="middle" fill="#314555" font-size="9.5" font-weight="800">${formatAxisLabel(value)}</text>`;
|
||||
}
|
||||
});
|
||||
markup += `<text x="${baseX + groupWidth / 2}" y="${height - margin.bottom + 20}" text-anchor="middle" fill="#5a6672" font-size="11.5" font-weight="700">${item.label}</text>`;
|
||||
|
||||
Reference in New Issue
Block a user