155 lines
7.0 KiB
JavaScript
155 lines
7.0 KiB
JavaScript
// 계약심사 입력은 작업본에 보관하고, 원래 내역은 사본을 열 때까지 유지한다.
|
|
const EstReview = (() => {
|
|
const fields = ["GN", "NU", "GU", "JU", "SU", "NC", "GC", "JC", "SC"];
|
|
const special = new Set([51, 52, 54, 55, 57, 58, 59, 62, 67, 68]);
|
|
const editable = (row) => !EstMath.num(row.Level) && !special.has(EstMath.num(row.LinkIndex));
|
|
const fixed = (row) => [1, 3].includes(EstMath.num(row.CalcIndex));
|
|
const allowed = (row) => ["GN", ...(fixed(row) ? EstMath.components : EstMath.units)];
|
|
const rowKey = (row, index) => `row:${EstBusiness.rowKey(row, index)}`;
|
|
|
|
function validate(project) {
|
|
const review = project.contractReview;
|
|
if (review == null) return;
|
|
if (
|
|
typeof review !== "object" ||
|
|
Array.isArray(review) ||
|
|
!review.edits ||
|
|
typeof review.edits !== "object" ||
|
|
Array.isArray(review.edits)
|
|
)
|
|
throw Error("계약심사 자료 형식 오류");
|
|
if (Object.keys(review.edits).length > 100000) throw Error("계약심사 항목 수 초과");
|
|
for (const [key, edit] of Object.entries(review.edits)) {
|
|
if (!key || key.length > 1000 || !edit || typeof edit !== "object" || Array.isArray(edit))
|
|
throw Error("계약심사 입력 형식 오류");
|
|
for (const [field, value] of Object.entries(edit)) {
|
|
if (
|
|
!fields.includes(field) ||
|
|
typeof value !== "number" ||
|
|
!Number.isFinite(value) ||
|
|
Math.abs(value) > Number.MAX_SAFE_INTEGER
|
|
)
|
|
throw Error(`계약심사 입력 오류: ${field}`);
|
|
}
|
|
}
|
|
}
|
|
function calculate(project) {
|
|
validate(project);
|
|
const edits = project.contractReview?.edits || {},
|
|
errors = [],
|
|
seen = new Set();
|
|
const working = EstNative.empty();
|
|
working.options = { ...project.options, SourceRefresh: false, TotalFix: false };
|
|
working.tables.EstMainDat = structuredClone(project.tables.EstMainDat);
|
|
working.tables.Wonga = structuredClone(project.tables.Wonga);
|
|
const baseline = EstNative.calculate(working);
|
|
working.tables.EstMainDat.forEach((row, index) => {
|
|
if (!editable(row)) return;
|
|
const key = rowKey(row, index);
|
|
if (seen.has(key)) errors.push(`심사 내역 식별자 중복: ${key}`);
|
|
seen.add(key);
|
|
const edit = edits[key];
|
|
if (!edit) return;
|
|
for (const field of Object.keys(edit)) {
|
|
if (!allowed(row).includes(field))
|
|
errors.push(`${row.Name || key}: 심사할 수 없는 항목 ${field}`);
|
|
else row[field] = edit[field];
|
|
}
|
|
if (fixed(row)) row.TC = EstMath.sum(row);
|
|
});
|
|
for (const key of Object.keys(edits))
|
|
if (!seen.has(key)) errors.push(`심사 대상 내역 없음: ${key}`);
|
|
const reviewed = EstNative.calculate(working);
|
|
errors.push(...baseline.calculation.errors, ...reviewed.calculation.errors);
|
|
const total = (p) =>
|
|
p.tables.Wonga.length
|
|
? EstMath.num(p.calculation.total)
|
|
: EstMath.num(p.calculation.direct.TC);
|
|
return {
|
|
baseline,
|
|
reviewed,
|
|
before: total(baseline),
|
|
after: total(reviewed),
|
|
errors: [...new Set(errors)],
|
|
};
|
|
}
|
|
function copy(project) {
|
|
const output = calculate(project);
|
|
if (output.errors.length) throw Error(output.errors.join(" / "));
|
|
const next = structuredClone(project);
|
|
delete next.id;
|
|
delete next.contractReview;
|
|
next.name = `${project.name} · 심사 내역`;
|
|
next.tables.EstMainDat = output.reviewed.tables.EstMainDat;
|
|
next.tables.Wonga = output.reviewed.tables.Wonga;
|
|
next.options.SourceRefresh = false;
|
|
next.calculation = output.reviewed.calculation;
|
|
return next;
|
|
}
|
|
function render(state) {
|
|
const { esc, number, byId } = EstApp,
|
|
project = state.project,
|
|
output = calculate(project);
|
|
const edits = project.contractReview?.edits || {};
|
|
const rows = output.baseline.tables.EstMainDat.map((row, index) => ({ row, index })).filter(
|
|
({ row }) => editable(row) && (row.Name || row.Code),
|
|
);
|
|
byId("work-content").innerHTML =
|
|
`<div class="business-tools"><b>계약심사</b><button data-review-copy>심사 내역 사본 열기</button><button data-review-clear>심사 입력 초기화</button><span>기존 ${number(output.before)} · 심사 ${number(output.after)} · 차액 ${number(output.after - output.before)}</span></div><p>심사 수량과 비목별 단가를 직접 입력합니다. 고정금액 행은 비목별 금액을 입력합니다. 원래 내역은 유지되며 심사 사본에서 변경됩니다.</p><p class="business-note">${esc(output.errors.join(" / "))}</p><table class="sheet"><thead><tr>${["코드", "품명", "기존 수량", "심사 수량", "노무", "재료", "경비", "실적", "기존 금액", "심사 금액", "차액"].map((value) => `<th>${value}</th>`).join("")}</tr></thead><tbody>${rows
|
|
.map(({ row, index }) => {
|
|
const key = rowKey(row, index),
|
|
input = edits[key] || {},
|
|
changed = output.reviewed.tables.EstMainDat[index];
|
|
const cell = (field) =>
|
|
`<input data-review="${field}" data-key="${esc(key)}" type="number" step="any" value="${esc(input[field] ?? EstMath.num(row[field]))}" aria-label="심사 ${esc(field)}">`;
|
|
return `<tr><td>${esc(row.Code)}</td><td>${esc(row.Name)}</td><td>${number(row.GN)}</td><td>${cell("GN")}</td>${allowed(
|
|
row,
|
|
)
|
|
.slice(1)
|
|
.map((field) => `<td>${cell(field)}</td>`)
|
|
.join(
|
|
"",
|
|
)}<td>${number(row.TC)}</td><td>${number(changed.TC)}</td><td>${number(EstMath.num(changed.TC) - EstMath.num(row.TC))}</td></tr>`;
|
|
})
|
|
.join("")}</tbody></table>`;
|
|
}
|
|
function install() {
|
|
document.addEventListener("change", (event) => {
|
|
const node = event.target;
|
|
if (!node.dataset.review) return;
|
|
const value = Number(node.value),
|
|
field = node.dataset.review,
|
|
key = node.dataset.key;
|
|
if (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER) {
|
|
EstApp.state.notice = "유효한 심사 수치를 입력하세요.";
|
|
EstApp.render();
|
|
return;
|
|
}
|
|
EstApp.change(() => {
|
|
const review = (EstApp.state.project.contractReview ||= { edits: {} });
|
|
(review.edits[key] ||= {})[field] = value;
|
|
}, false);
|
|
});
|
|
document.addEventListener("click", (event) => {
|
|
const project = EstApp.state.project;
|
|
try {
|
|
if (event.target.closest("[data-review-copy]")) {
|
|
const next = copy(project);
|
|
if (confirm("현재 작업 대신 심사 내역 사본을 여시겠습니까?")) {
|
|
EstApp.setProject(next);
|
|
EstApp.navigate("EstMainDat");
|
|
}
|
|
} else if (
|
|
event.target.closest("[data-review-clear]") &&
|
|
confirm("심사 입력을 초기화하시겠습니까?")
|
|
)
|
|
EstApp.change(() => delete project.contractReview, false);
|
|
} catch (error) {
|
|
EstApp.state.notice = error.message;
|
|
EstApp.render();
|
|
}
|
|
});
|
|
}
|
|
return { validate, calculate, copy, render, install };
|
|
})();
|