feat(est): reproduce native tables and calculations
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
const EstExtra = (() => {
|
||||
function field(section, index, key, value, type = "number") {
|
||||
return `<input data-extra="${section}" data-row="${index}" data-field="${key}" type="${type}" step="any" value="${EstApp.esc(value)}" aria-label="${EstApp.esc(key)}">`;
|
||||
}
|
||||
function button(action, text, extra = "") {
|
||||
return `<button data-action="${action}" ${extra}>${text}</button>`;
|
||||
}
|
||||
function table(headers, rows) {
|
||||
return `<table class="sheet"><thead><tr>${headers.map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
||||
}
|
||||
function box(title, tools, body, note = "") {
|
||||
return `<div class="business-tools"><b>${title}</b>${tools}</div>${body}${note ? `<p class="business-note">${note}</p>` : ""}`;
|
||||
}
|
||||
function render(state) {
|
||||
const { project, view } = state,
|
||||
{ esc, number, byId } = EstApp;
|
||||
let html = "";
|
||||
if (view === "quantity") {
|
||||
const values = Object.create(null),
|
||||
rows = project.quantities.map((row, index) => {
|
||||
let result,
|
||||
error = "";
|
||||
try {
|
||||
result = EstMath.floating(
|
||||
EstExpression.evaluate(row.formula || "0", (key) => values[key]),
|
||||
row.digits ?? 2,
|
||||
1,
|
||||
);
|
||||
values[row.Code] = result;
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
return [
|
||||
field("quantities", index, "Code", row.Code, "text"),
|
||||
field("quantities", index, "Name", row.Name, "text"),
|
||||
field("quantities", index, "TargetCode", row.TargetCode, "text"),
|
||||
field("quantities", index, "formula", row.formula, "text"),
|
||||
field("quantities", index, "digits", row.digits ?? 2),
|
||||
esc(error || number(result)),
|
||||
button("delete-extra", "삭제", `data-section="quantities" data-row="${index}"`),
|
||||
];
|
||||
});
|
||||
html = box(
|
||||
"수량산출서",
|
||||
button("add-quantity", "산출 항목 추가") + button("apply-quantity", "내역에 수량 반영"),
|
||||
table(["산출코드", "품명", "연결 내역코드", "수식", "소수자리", "결과", ""], rows),
|
||||
"앞선 산출 항목을 [산출코드]로 참조합니다. 내역 반영은 연결된 코드의 산출값을 합산하여 수량에 반영합니다.",
|
||||
);
|
||||
} else if (view === "change") {
|
||||
const changes = EstBusiness.changes(project);
|
||||
html = box(
|
||||
"설계변경",
|
||||
button("capture-baseline", "현재 내역을 당초로 확정") +
|
||||
button("recalculate-original", "당초 단가 재계산") +
|
||||
button("bid-rates", "낙찰률 적용"),
|
||||
table(
|
||||
["코드", "품명", "당초 수량", "변경 수량", "당초 금액", "변경 금액", "증감"],
|
||||
changes
|
||||
.filter((row) => row.Code)
|
||||
.map((row) => [
|
||||
esc(row.Code),
|
||||
esc(row.Name),
|
||||
number(row.originalGN),
|
||||
number(row.GN),
|
||||
number(row.originalTC),
|
||||
number(row.TC),
|
||||
number(row.difference),
|
||||
]),
|
||||
),
|
||||
project.baseline
|
||||
? `당초: ${esc(project.baseline.name)}. 당초 자료는 별도 사본으로 보존합니다.`
|
||||
: "현재 작업을 저장한 뒤 당초 내역을 확정하세요.",
|
||||
);
|
||||
} else if (view === "progress") {
|
||||
state.roundId ||= project.progress[0]?.id;
|
||||
const round = project.progress.find((r) => r.id === state.roundId);
|
||||
let body = '<p class="empty">회차를 추가하세요.</p>',
|
||||
tools = button("add-round", "기성 회차 추가");
|
||||
if (round) {
|
||||
tools +=
|
||||
`<select id="round-choice">${project.progress.map((r) => `<option value="${esc(r.id)}" ${r.id === round.id ? "selected" : ""}>${esc(r.name)}${r.locked ? " (확정)" : ""}</option>`).join("")}</select>` +
|
||||
button("lock-round", "회차 확정") +
|
||||
`<label><input id="use-approved" type="checkbox" ${round.approved ? "checked" : ""} ${round.locked ? "disabled" : ""}> 누계에 사정 사용</label>`;
|
||||
let output;
|
||||
try {
|
||||
output =
|
||||
round.locked && round.result ? round.result : EstBusiness.progress(project, round.id);
|
||||
} catch (e) {
|
||||
output = { rows: [], errors: [e.message] };
|
||||
}
|
||||
state.business = output;
|
||||
body =
|
||||
table(
|
||||
[
|
||||
"코드",
|
||||
"공종",
|
||||
"도급수량",
|
||||
"전회수량",
|
||||
"금회 입력",
|
||||
"입력방법",
|
||||
"사정 입력",
|
||||
"입력방법",
|
||||
"금회금액",
|
||||
"사정금액",
|
||||
"누계수량",
|
||||
"누계금액",
|
||||
],
|
||||
output.rows
|
||||
.filter((r) => r.Code && !EstMath.num(r.Level))
|
||||
.map((row, index) => {
|
||||
const originalIndex = project.tables.EstMainDat.findIndex(
|
||||
(r) => r.PkID === row.PkID && r.Code === row.Code,
|
||||
),
|
||||
key = EstBusiness.rowKey(row, originalIndex),
|
||||
input = round.inputs?.[key] || {};
|
||||
const edit = (prefix) =>
|
||||
`<input data-progress="${prefix}" data-key="${esc(key)}" type="number" step="any" value="${input[prefix] ?? 0}" ${round.locked ? "disabled" : ""}>`;
|
||||
const method = (prefix) =>
|
||||
`<select data-progress="${prefix}Mode" data-key="${esc(key)}" ${round.locked ? "disabled" : ""}>${[
|
||||
["quantity", "수량"],
|
||||
["amount", "금액"],
|
||||
["ratio", "비율 %"],
|
||||
]
|
||||
.map(
|
||||
([v, label]) =>
|
||||
`<option value="${v}" ${v === (input[`${prefix}Mode`] || "quantity") ? "selected" : ""}>${label}</option>`,
|
||||
)
|
||||
.join("")}</select>`;
|
||||
return [
|
||||
esc(row.Code),
|
||||
esc(row.Name),
|
||||
number(row.GN),
|
||||
number(row.JunGN),
|
||||
edit("Gum"),
|
||||
method("Gum"),
|
||||
edit("Sa"),
|
||||
method("Sa"),
|
||||
number(row.GumTC),
|
||||
number(row.SaTC),
|
||||
number(row.TotalGN),
|
||||
number(row.TotalTC),
|
||||
];
|
||||
}),
|
||||
) +
|
||||
`<p class="business-note">${esc((output.errors || []).join(" / "))} 금회 도급액 ${number(output.total)} · 기성비율 ${number(output.ratio)}%</p>`;
|
||||
}
|
||||
html = box(
|
||||
"기성고",
|
||||
tools,
|
||||
body,
|
||||
"전회는 직전 확정 회차 누계를 읽습니다. 금회·사정은 수량·금액·비율 중 하나를 입력해 계산합니다.",
|
||||
);
|
||||
} else if (view === "escalation") {
|
||||
const output = EstBusiness.escalation(project);
|
||||
state.business = output;
|
||||
const comp = state.escalationComponent || "NU";
|
||||
const choices = EstMath.units
|
||||
.map(
|
||||
(key, i) =>
|
||||
`<option value="${key}" ${key === comp ? "selected" : ""}>${["노무", "재료", "경비", "실적"][i]}</option>`,
|
||||
)
|
||||
.join("");
|
||||
const rows = project.tables.EstMainDat.map((row, index) => ({ row, index }))
|
||||
.filter(({ row }) => row.Code && !EstMath.num(row.Level))
|
||||
.map(({ row, index }) => {
|
||||
const old =
|
||||
project.baseline?.tables.EstMainDat.find(
|
||||
(r) => r.PkID === row.PkID && r.Code === row.Code,
|
||||
) || row,
|
||||
result = output.rows[index];
|
||||
return [
|
||||
esc(row.Code),
|
||||
esc(row.Name),
|
||||
EstViews.cell("EstMainDat", index, `A${comp}`, row[`A${comp}`]),
|
||||
number(old[comp]),
|
||||
number(row[comp]),
|
||||
number(result[`D${comp}`]),
|
||||
number(result[`E${comp}`]),
|
||||
number(result[`F${comp}`]),
|
||||
EstViews.cell("EstMainDat", index, "EscPlanned", row.EscPlanned || 0),
|
||||
EstViews.cell("EstMainDat", index, "EscActual", row.EscActual || 0),
|
||||
EstViews.cell("EstMainDat", index, "EscProgress", row.EscProgress || 0),
|
||||
number(result.EscExcluded),
|
||||
number(result.EscApplied),
|
||||
];
|
||||
});
|
||||
html = box(
|
||||
"물가변동 품목조정",
|
||||
`<select id="escalation-component">${choices}</select>` +
|
||||
button("capture-bid", "현재 단가를 입찰단가로 기록") +
|
||||
button("capture-baseline", "계약 내역 확정"),
|
||||
table(
|
||||
[
|
||||
"코드",
|
||||
"품명",
|
||||
"입찰 당시",
|
||||
"계약 단가",
|
||||
"변동 당시",
|
||||
"등락률 %",
|
||||
"등락폭",
|
||||
"조정단가",
|
||||
"예정공정수량",
|
||||
"실행공정수량",
|
||||
"기성수량",
|
||||
"적용제외",
|
||||
"적용수량",
|
||||
],
|
||||
rows,
|
||||
),
|
||||
`계약 도급액 ${number(output.contractTotal)} · 조정 도급액 ${number(output.adjustedTotal)} · 적용제외(간접비 포함) ${number(output.excludedTotal)} · 적용대가 ${number(output.applied)} · 조정률 ${number(output.rate)}% · 조정금액 ${number(output.adjustment)}. ${esc(output.errors.join(" / "))}`,
|
||||
);
|
||||
} else if (view === "annual") {
|
||||
const output = EstBusiness.annual(project);
|
||||
state.business = output;
|
||||
html = box(
|
||||
"연차공사",
|
||||
button("add-annual", "차수 추가"),
|
||||
table(
|
||||
["차수", "수량 배분 수식", "순공사비", "도급액", "작업", ""],
|
||||
project.annual.map((year, index) => [
|
||||
field("annual", index, "name", year.name, "text"),
|
||||
field("annual", index, "formula", year.formula, "text"),
|
||||
number(output.results[index]?.project.calculation.direct.TC),
|
||||
number(output.results[index]?.project.calculation.total),
|
||||
button("open-annual", "차수 내역 열기", `data-row="${index}"`),
|
||||
button("delete-extra", "삭제", `data-section="annual" data-row="${index}"`),
|
||||
]),
|
||||
),
|
||||
`수식 예: [계약수량]*0.5, [계약수량]-[1차]. ${esc(output.errors.join(" / "))}`,
|
||||
);
|
||||
} else if (view === "resources") {
|
||||
const output = EstResources.calculate(project);
|
||||
html = box(
|
||||
"동원인원·자재·장비",
|
||||
"",
|
||||
table(
|
||||
["종류", "코드", "품명", "규격", "단위", "소요량 / 환산시간", "금액"],
|
||||
output.rows.map((row) => [
|
||||
esc({ 5: "자재", 6: "노임", 21: "표준", 101: "견적", 105: "기계" }[row.kind]),
|
||||
esc(row.Code),
|
||||
esc(row.Name),
|
||||
esc(row.Size),
|
||||
esc(row.Unite),
|
||||
number(row.quantity),
|
||||
number(row.amount),
|
||||
]),
|
||||
),
|
||||
esc(output.errors.join(" / ")),
|
||||
);
|
||||
} else if (view === "drawing") {
|
||||
const rows = project.tables.CadMainDat || [];
|
||||
html = box(
|
||||
"원본 도면·문서 자료",
|
||||
"",
|
||||
table(
|
||||
["번호", "코드", "이름", "보존 필드"],
|
||||
rows.map((row, index) => [
|
||||
index + 1,
|
||||
esc(row.Code),
|
||||
esc(row.Name),
|
||||
esc(
|
||||
Object.keys(row)
|
||||
.filter((key) => row[key]?.binary)
|
||||
.join(", "),
|
||||
),
|
||||
]),
|
||||
),
|
||||
"가져온 도면 바이너리는 프로젝트에 보존됩니다. VEC0650/CAD 편집·출력의 재현 근거는 현재 자료에 없어 동작을 구현하지 않았습니다.",
|
||||
);
|
||||
}
|
||||
byId("work-content").innerHTML = html;
|
||||
}
|
||||
return { render };
|
||||
})();
|
||||
Reference in New Issue
Block a user