Files
Aislo/tools/est-test/native-machine.js

391 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// REA f18804 → f16638 / f15868 / f15f40 / f16320 → f16ec8.
const EstMachine = (() => {
const { num, components, fixed, floating, even, sum } = EstMath;
const fuels = new Set([
"주연료",
"선박용경유",
"전력",
"경유",
"휘발유",
"중유",
"전",
"가스",
"가솔린",
"디젤",
"diesel",
"gasoline",
"heavyoil",
"lpg",
"l.p.g",
]);
function calculate(rows, criteria, options = {}) {
const settings = new Map(criteria.map((row) => [num(row.Index), num(row.TC)]));
const get = (index) => settings.get(index) ?? 0;
const output = structuredClone(rows),
titles = [],
errors = [];
let head = null,
body = [];
function finish() {
if (!head) return;
components.forEach(
(key) =>
(head[key] = floating(
body.filter((r) => num(r.CalcIndex) < 2).reduce((n, r) => n + num(r[key]), 0),
)),
);
head.TC = sum(head);
titles.push({
...head,
OTC: head.TC,
ONC: head.NC,
OGC: head.GC,
OJC: head.JC,
OSC: head.SC,
});
}
for (const row of output) {
const index = num(row.Index),
link = num(row.LinkIndex);
if (index > 0 && link !== 57) {
finish();
head = row;
body = [];
continue;
}
if (!head || row.Index == null) continue;
if (link === 52) {
finish();
const title = titles[titles.length - 1],
rate = num(row.GN) / 100;
components.forEach((key) => (head[key] = floating(num(title[key]) * rate)));
head.TC = sum(head);
Object.assign(title, head, { NachalRatio: num(row.GN) });
row.TU = title.OTC;
head = null;
body = [];
continue;
}
if (![0, 2].includes(num(row.CalcIndex))) {
body.push(row);
continue;
}
components.forEach((key) => (row[key] = 0));
const q = num(row.GN) / (["%", "%"].includes(row.Unite) ? 100 : 1),
rate = 1 + num(row.Ratio) / 100;
const digits = EstMath.even(get(index));
let value = 0,
field = "JC";
if (index === -3) {
const currency = String(row.Unite || "")[0];
if (currency === "\\") value = num(row.TU) * q * 1e-7;
else if ("$PEFLMY".includes(currency || "\u0000")) {
let exchange = get(currency.charCodeAt(0));
if (currency === "L" && !settings.has(76)) exchange = get(70);
if (currency === "Y") exchange /= 100;
if (!exchange) errors.push(`${head.Name}: 환율 없음 (${currency})`);
const cut = options.DollarCostCut ? 1000 : 1;
// f1580c receives two Currency integers; binary float multiplication can
// turn an exact thousand (226644000) into the previous thousand.
const converted =
Number(
(BigInt(even(num(row.TU) * 10000)) * BigInt(even(exchange * 10000))) /
BigInt(cut * 100000000),
) * cut;
value = converted * q * 1e-7;
} else value = num(row.TU) * q;
value = fixed((even(value * 10000) / 10000) * rate, digits);
} else if (index === -2) {
let unitPrice = num(row.TU),
quantity = q;
if (String(row.Name || "").includes("잡")) {
const fuel = [...body].reverse().find(
(r) =>
num(r.Index) === -2 &&
fuels.has(
String(r.Name || "")
.replace(/\s/g, "")
.toLowerCase(),
),
);
unitPrice = fuel ? num(fuel.GC) || (num(fuel.SCheck) === 3 ? num(fuel.JC) : 0) : 0;
row.TU = unitPrice;
row.Unite = "%";
quantity = num(row.GN) / 100;
}
value = fixed(even(even(unitPrice * quantity * 10000) * rate) / 10000, digits);
field = num(row.SCheck) === 3 ? "JC" : "GC";
} else if (index === -1) {
const coefficient = Math.trunc(get(num(row.NCGasu)) * 1e10) / 1e10;
let price = num(row.TU) * coefficient;
if (options.JDLCalcMethod) price = floating(price, 1);
value = fixed(even(even(price * q * 10000) * rate) / 10000, digits);
field = "NC";
}
row[field] = value;
row.TC = sum(row);
body.push(row);
}
finish();
return { rows: output, titles, errors };
}
let page = 0;
function usage(project) {
const titles = project.tables?.MachTitleDat || [];
const grps = EstNative.groups(project.tables?.MachMainDat || []);
const original = EstNative.groups(project.baseline?.tables?.MachMainDat || []);
const oldTitles = project.baseline?.tables?.MachTitleDat || [];
const oldByCode = new Map();
for (const row of oldTitles) {
const rows = oldByCode.get(row.Code) || [];
rows.push(row);
oldByCode.set(row.Code, rows);
}
return titles.map((title, index) => {
const code = title.Code || "";
const group = grps.get(code);
const appliedPrice = EstMath.num(title.TC);
const saved = oldByCode.get(code)?.length === 1 ? oldByCode.get(code)[0] : null;
const hasOriginal = Boolean(saved) || title.TC_ != null;
const originalPrice = saved
? EstMath.num(saved.TC)
: hasOriginal
? EstMath.num(title.TC_)
: null;
const difference = hasOriginal ? appliedPrice - originalPrice : null;
return {
index,
Code: code,
Name: title.Name || "",
Size: title.Size || "",
Unite: title.Unite || "",
appliedPrice,
originalPrice,
difference,
NC: EstMath.num(title.NC),
GC: EstMath.num(title.GC),
JC: EstMath.num(title.JC),
detailCount: group ? group.rows.length : 0,
status: !hasOriginal ? "신규(당초없음)" : Math.abs(difference) > 1e-6 ? "당초차이" : "일치",
canApply: false, // 당초 계약값은 비교용이며 현재 단가 덮어쓰기 적용을 지원하지 않음
canRestore: Boolean(group && original.has(code)),
};
});
}
function changes(project) {
const labels = { new: "신규", deleted: "삭제", changed: "변경", unchanged: "동일" };
const titles = project.tables?.MachTitleDat || [];
const old = project.baseline?.tables?.MachTitleDat || [];
const oldGroups = EstNative.groups(project.baseline?.tables?.MachMainDat || []);
const identities = EstRowMatch.identities(old, titles, (row) => String(row.Code || ""));
const previous = new Map(old.map((row, index) => [identities.original[index], row]));
const result = titles.map((title, index) => {
const appliedPrice = EstMath.num(title.TC);
const saved = previous.get(identities.current[index]);
previous.delete(identities.current[index]);
const hasOriginal = Boolean(saved) || title.TC_ != null;
const originalPrice = saved
? EstMath.num(saved.TC)
: hasOriginal
? EstMath.num(title.TC_)
: null;
const difference = hasOriginal ? appliedPrice - originalPrice : null;
const statusKey = !hasOriginal
? "new"
: Math.abs(difference) > 1e-6 ||
(saved &&
["Name", "Size", "Unite"].some(
(field) => (saved[field] || "") !== (title[field] || ""),
))
? "changed"
: "unchanged";
return {
index,
Code: title.Code || "",
Name: title.Name || "",
Size: title.Size || "",
Unite: title.Unite || "",
originalTC: originalPrice,
TC: appliedPrice,
difference,
status: labels[statusKey] || statusKey,
};
});
for (const title of previous.values())
result.push({
index: null,
Code: title.Code || "",
Name: title.Name || "",
Size: title.Size || "",
Unite: title.Unite || "",
originalTC: EstMath.num(title.TC),
TC: 0,
difference: -EstMath.num(title.TC),
status: labels.deleted,
canRecover: oldGroups.has(title.Code),
});
return result;
}
function applyRowPrice() {
throw Error(
"기계경비 호표는 MachMainDat 산출 결과이며 당초 계약값으로의 덮어쓰기 적용은 지원하지 않습니다.",
);
}
function applyAllPrices() {
throw Error(
"기계경비 호표는 MachMainDat 산출 결과이며 당초 계약값으로의 덮어쓰기 적용은 지원하지 않습니다.",
);
}
function ensureRestoredIds(saved, existing) {
const ids = saved.map((row) => row.PkID).filter((id) => id != null);
const unique = new Set(ids);
if (unique.size !== ids.length || existing.some((row) => unique.has(row.PkID)))
throw Error("기존 호표의 식별자와 충돌하여 복원할 수 없습니다.");
}
function restoreGroup(project, code) {
const current = project.tables?.MachMainDat || [],
baseline = project.baseline?.tables?.MachMainDat;
if (!Array.isArray(baseline))
throw Error("기계 호표의 당초 구성이 없습니다. 당초를 다시 확정하세요.");
const count = (rows) => rows.filter((row) => Number(row.Index) > 0 && row.Code === code).length;
if (!code || count(current) !== 1 || count(baseline) !== 1)
throw Error("복원할 기계 호표가 없거나 코드가 중복됩니다.");
const from = EstNative.groups(baseline).get(code),
to = EstNative.groups(current).get(code);
ensureRestoredIds(from.rows, [
...current.slice(0, to.start),
...current.slice(to.start + to.rows.length),
]);
return [
...current.slice(0, to.start),
...structuredClone(from.rows),
...current.slice(to.start + to.rows.length),
];
}
function recoverGroup(project, code) {
const baseline = project.baseline?.tables;
const original = baseline?.MachMainDat,
oldTitles = baseline?.MachTitleDat;
const current = project.tables.MachMainDat,
titles = project.tables.MachTitleDat;
if (!Array.isArray(original) || !Array.isArray(oldTitles))
throw Error("기계 호표의 당초 구성이 없습니다.");
const originals = original.filter((row) => Number(row.Index) > 0 && row.Code === code);
const savedTitles = oldTitles.filter((row) => row.Code === code);
if (
!code ||
originals.length !== 1 ||
savedTitles.length !== 1 ||
current.some((row) => Number(row.Index) > 0 && row.Code === code) ||
titles.some((row) => row.Code === code)
)
throw Error("복원할 호표가 없거나 코드가 중복됩니다.");
const group = EstNative.groups(original).get(code).rows;
ensureRestoredIds(group, current);
ensureRestoredIds(savedTitles, titles);
const position = (saved, existing, isHeader) => {
const at = saved.findIndex((row) => isHeader(row) && row.Code === code);
const positions = new Map();
existing.forEach((row, index) => {
if (isHeader(row) && !positions.has(row.Code)) positions.set(row.Code, index);
});
for (const row of saved.slice(at + 1))
if (isHeader(row) && positions.has(row.Code)) return positions.get(row.Code);
return existing.length;
};
const rowAt = position(original, current, (row) => Number(row.Index) > 0);
const titleAt = position(oldTitles, titles, () => true);
return {
rows: [...current.slice(0, rowAt), ...structuredClone(group), ...current.slice(rowAt)],
titles: [
...titles.slice(0, titleAt),
structuredClone(savedTitles[0]),
...titles.slice(titleAt),
],
};
}
function render() {
const { state, esc, number, byId } = EstApp,
isChange = state.view === "machine-change",
rows = isChange ? changes(state.project) : usage(state.project);
page = Math.max(0, Math.min(page, Math.ceil(rows.length / 100) - 1));
const visible = rows.slice(page * 100, page * 100 + 100),
value = (v) => (v == null ? "" : number(v));
if (isChange) {
byId("work-content").innerHTML =
`<div class="business-tools"><b>기계경비 설계변경</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-machine-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-machine-page="1" ${(page + 1) * 100 >= rows.length ? "disabled" : ""}>다음</button></div><table class="sheet"><thead><tr><th>구분</th><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>당초 금액</th><th>변경 금액</th><th>증감</th><th>작업</th></tr></thead><tbody>${visible.map((row) => `<tr><td>${esc(row.status)}</td><td>${esc(row.Code)}</td><td>${esc(row.Name)}</td><td>${esc(row.Size)}</td><td>${esc(row.Unite)}</td><td>${value(row.originalTC)}</td><td>${value(row.TC)}</td><td>${value(row.difference)}</td><td>${row.index == null ? (row.canRecover ? `<button data-machine-recover="${esc(row.Code)}">당초 호표 복원</button>` : "") : `<button data-machine-group="${esc(row.Code)}">호표 열기</button>`}</td></tr>`).join("")}</tbody></table>`;
return;
}
byId("work-content").innerHTML =
`<div class="business-tools"><b>기계경비 사용 내역</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-machine-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-machine-page="1" ${(page + 1) * 100 >= rows.length ? "disabled" : ""}>다음</button></div><p>자동계산이 켜져 있으면 당초 산식 복원 후 현재 기초단가로 다시 계산합니다.</p><table class="sheet"><thead><tr><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>적용 단가</th><th>당초 단가</th><th>차이</th><th>상태</th><th>노무비</th><th>재료비</th><th>경비</th><th>작업</th></tr></thead><tbody>${visible.map((row) => `<tr><td>${esc(row.Code)}</td><td>${esc(row.Name)}</td><td>${esc(row.Size)}</td><td>${esc(row.Unite)}</td><td>${value(row.appliedPrice)}</td><td>${value(row.originalPrice)}</td><td>${value(row.difference)}</td><td>${esc(row.status)}</td><td>${value(row.NC)}</td><td>${value(row.GC)}</td><td>${value(row.JC)}</td><td><button data-machine-group="${esc(row.Code)}">호표 열기</button>${row.canRestore ? ` <button data-machine-restore="${esc(row.Code)}">당초 산식 복원</button>` : ""}</td></tr>`).join("")}</tbody></table>`;
}
function install() {
document.addEventListener("click", (event) => {
const recover = event.target.closest("[data-machine-recover]");
if (recover) {
try {
const result = recoverGroup(EstApp.state.project, recover.dataset.machineRecover);
if (confirm("삭제한 기계 호표를 당초 구성으로 복원하시겠습니까?"))
EstApp.change(() => {
EstApp.state.project.tables.MachMainDat = result.rows;
EstApp.state.project.tables.MachTitleDat = result.titles;
});
} catch (error) {
EstApp.state.notice = error.message;
EstApp.render();
}
return;
}
const restore = event.target.closest("[data-machine-restore]");
if (restore) {
try {
const rows = restoreGroup(EstApp.state.project, restore.dataset.machineRestore);
if (
confirm(
"당초 기계 호표 구성을 복원하시겠습니까? 자동계산이 켜져 있으면 현재 기초단가로 다시 계산합니다.",
)
)
EstApp.change(() => (EstApp.state.project.tables.MachMainDat = rows));
} catch (error) {
EstApp.state.notice = error.message;
EstApp.render();
}
return;
}
const step = event.target.closest("[data-machine-page]");
if (step) {
page += Number(step.dataset.machinePage);
render();
return;
}
const node = event.target.closest("[data-machine-group]");
if (!node) return;
const code = node.dataset.machineGroup;
if (!code) return;
EstApp.navigate("MachMainDat", code);
});
}
return {
calculate,
usage,
changes,
applyRowPrice,
applyAllPrices,
restoreGroup,
recoverGroup,
render,
install,
};
})();