Files
Aislo/tools/est-test/native-business-actions.js
T

320 lines
12 KiB
JavaScript
Raw 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.
const EstBusinessActions = (() => {
let purpose = "",
documentKey;
const currentRound = () =>
EstApp.state.project.progress.find((round) => round.id === EstApp.state.roundId);
function numeric(value) {
const n = Number(value);
if (!Number.isFinite(n) || Math.abs(n) > Number.MAX_SAFE_INTEGER)
throw Error("유효한 수치를 입력하세요.");
return n;
}
function change(node) {
const { state } = EstApp;
if (node.dataset.option === "EscDesignTotal") {
const value = numeric(node.value);
if (value < 0) throw Error("조사 도급액은 0 이상입니다.");
EstApp.change(() => (state.project.options.EscDesignTotal = value), false);
return true;
}
if (node.dataset.escAuto !== undefined) {
EstApp.change(
() =>
(state.project.tables.EstMainDat[Number(node.dataset.escAuto)].EscAuto = node.checked),
false,
);
return true;
}
if (node.id === "progress-components" || node.id === "progress-digits") {
const round = currentRound();
if (round.locked) throw Error("확정 회차입니다.");
const value = node.id === "progress-components" ? node.checked : numeric(node.value);
if (node.id === "progress-digits" && (!Number.isInteger(value) || value < 0 || value > 4))
throw Error("금액 소수자리는 0~4입니다.");
EstApp.change(
() =>
(round[node.id === "progress-components" ? "separateComponents" : "amountDigits"] =
value),
false,
);
return true;
}
return false;
}
async function handle(name, node) {
const { state } = EstApp,
project = state.project,
round = currentRound();
if (name === "clear-estimate-quantities") {
if (state.view !== "EstMainDat") throw Error("내역서에서 수량을 초기화하세요.");
const rows = project.tables.EstMainDat,
selected = [...state.selection];
EstActions.form(
"내역 수량 초기화",
[
{
name: "scope",
label: "초기화 범위 (비율·합계 자동행 제외)",
value: selected.length ? "selected" : "all",
choices: [
["selected", "선택한 상세 행"],
["all", "모든 상세 행"],
],
},
],
(values) => {
const indices = (
values.scope === "selected" ? selected : rows.map((_, index) => index)
).filter((index) => {
const row = rows[index];
return (
row &&
!Number(row.Level) &&
![51, 52, 54, 55, 57, 58, 59, 62, 67, 68].includes(Number(row.LinkIndex)) &&
!["%", "%"].includes(row.Unite)
);
});
if (!indices.length) throw Error("초기화할 상세 행이 없습니다.");
EstApp.change(() => indices.forEach((index) => (rows[index].GN = 0)));
},
);
} else if (name === "clear-progress") {
if (confirm("모든 기성 회차와 확정 결과를 초기화하시겠습니까?"))
EstApp.change(() => {
project.progress = [];
state.roundId = undefined;
}, false);
} else if (name === "set-row-precision") {
const rows = project.tables[state.view],
indices = [...state.selection].sort((a, b) => a - b);
if (!rows || !EstModel.columns(state.view).includes("DigetIndex"))
throw Error("이 자료에는 자릿수 설정이 없습니다.");
if (!indices.length || indices.some((index) => !rows[index]))
throw Error("설정할 행을 선택하세요.");
EstActions.form(
"선택 행 자릿수",
[
{
name: "method",
label: "처리방식",
value: rows[indices[0]].DigetIndex || 0,
choices: [
[0, "자동"],
[1, "사사오입"],
[2, "절사"],
[3, "올림식"],
],
},
{
name: "digits",
label: "소수 자릿수 (0~6)",
type: "number",
value: Math.max(0, Number(rows[indices[0]].Diget || 1) - 1),
},
],
(values) => {
const method = Number(values.method),
digits = Number(values.digits);
if (
![0, 1, 2, 3].includes(method) ||
!Number.isInteger(digits) ||
digits < 0 ||
digits > 6
)
throw Error("자릿수는 0~6이고 처리방식을 선택해야 합니다.");
EstApp.change(() => {
for (const index of indices) {
rows[index].DigetIndex = method;
rows[index].Diget = method ? digits + 1 : 0;
}
});
},
);
} else if (name === "apply-all-quantities") {
const totals = EstQuantity.allTotals(project);
if (confirm("모든 수량산출서와 수식표의 연결 수량을 합산해 내역에 반영하시겠습니까?"))
EstApp.change(() => EstQuantity.applyTotals(project, totals));
} else if (name === "move-quantity") {
const index = Number(node.dataset.row),
direction = Number(node.dataset.direction),
next = index + direction;
if (
!Number.isInteger(index) ||
![-1, 1].includes(direction) ||
index < 0 ||
next < 0 ||
index >= project.quantities.length ||
next >= project.quantities.length
)
throw Error("수량 수식표 이동 위치 오류");
EstApp.change(() => {
[project.quantities[index], project.quantities[next]] = [
project.quantities[next],
project.quantities[index],
];
}, false);
} else if (name === "move-estimate-up" || name === "move-estimate-down") {
if (state.view !== "EstMainDat") throw Error("내역서에서만 행을 이동할 수 있습니다.");
const direction = name === "move-estimate-up" ? -1 : 1;
const indices = EstBusiness.moveEstimateRows(
structuredClone(project.tables.EstMainDat),
state.selection,
direction,
);
if (indices.every((index) => state.selection.has(index))) return;
EstApp.change(() => {
state.selection = new Set(
EstBusiness.moveEstimateRows(project.tables.EstMainDat, state.selection, direction),
);
});
} else if (name === "unlock-round") {
if (confirm("이 회차와 이후 회차의 확정을 해제하시겠습니까? 입력값은 유지됩니다."))
EstApp.change(() => (state.project = EstProgress.unlock(project, round.id)), false);
} else if (name === "delete-round") {
if (round.locked) throw Error("확정을 먼저 해제하세요.");
if (project.progress.at(-1) !== round) throw Error("마지막 회차부터 삭제하세요.");
if (confirm("선택한 기성 회차를 삭제하시겠습니까?"))
EstApp.change(() => {
project.progress.pop();
state.roundId = project.progress.at(-1)?.id;
}, false);
} else if (name === "rename-round") {
if (round.locked) throw Error("확정을 먼저 해제하세요.");
EstActions.form(
"기성 회차 이름",
[{ name: "name", label: "회차 이름", value: round.name }],
(values) => {
const name = values.name.trim();
if (
!name ||
project.progress.some(
(item) => item !== round && String(item.name || "").trim() === name,
)
)
throw Error("회차 이름은 비어 있거나 중복될 수 없습니다.");
EstApp.change(() => (round.name = name), false);
},
);
} else if (name === "fill-progress") {
if (round.locked) throw Error("확정 회차입니다.");
EstActions.form(
"기성 일괄 입력",
[
{
name: "prefix",
label: "대상",
value: "Gum",
choices: [
["Gum", "금회기성"],
["Sa", "금회사정"],
],
},
{
name: "mode",
label: "방법",
value: "ratio",
choices: [
["ratio", "비율 입력"],
["full", "전량"],
],
},
{ name: "ratio", label: "비율 %", type: "number", value: 100 },
],
(values) => {
const value = numeric(values.ratio);
const ambiguous = EstProgress.ambiguousKeys(project.tables.EstMainDat);
const hasAmbiguous = project.tables.EstMainDat.some(
(row, index) =>
Number(row.Level || 0) <= 0 &&
Number(row.CalcIndex || 0) < 2 &&
![54, 55, 58, 59, 62, 67].includes(Number(row.LinkIndex)) &&
!(values.mode === "full" && Number(row.LinkIndex) === 57) &&
ambiguous.has(EstBusiness.rowKey(row, index)),
);
if (hasAmbiguous) throw Error("기성 내역 식별자가 중복되어 일괄 입력할 수 없습니다.");
EstApp.change(() => {
project.tables.EstMainDat.forEach((row, index) => {
if (
Number(row.Level) > 0 ||
Number(row.CalcIndex) >= 2 ||
[54, 55, 58, 59, 62, 67].includes(Number(row.LinkIndex))
)
return;
if (values.mode === "full" && Number(row.LinkIndex) === 57) return;
const key = EstBusiness.rowKey(row, index);
round.inputs[key] ||= {};
round.inputs[key][values.prefix + "Mode"] =
values.mode === "full" ? "quantity" : "ratio";
round.inputs[key][values.prefix] =
values.mode === "full" ? Number(row.GN) || 0 : value;
});
}, false);
},
);
} else if (name === "load-progress-exclusion") {
const locked = project.progress.filter((round) => round.locked && round.result);
if (!locked.length) throw Error("확정한 기성 회차가 없습니다.");
EstActions.form(
"기성 누계수량 불러오기",
[
{
name: "id",
label: "기성 회차",
value: locked.at(-1).id,
choices: locked.map((round) => [round.id, round.name]),
},
],
(values) => {
const result = locked.find((round) => round.id === values.id).result;
const updates = EstBusiness.progressExclusions(project, result);
EstApp.change(() => {
for (const { index, value } of updates)
project.tables.EstMainDat[index].EscProgress = value;
}, false);
},
);
} else if (name === "load-contract" || name === "load-bid") {
purpose = name;
documentKey = state.documentKey;
document.getElementById("business-file").click();
}
}
function install() {
const input = document.createElement("input");
input.type = "file";
input.id = "business-file";
input.accept = ".json,.estx";
input.hidden = true;
document.body.append(input);
input.addEventListener("change", async () => {
try {
const file = input.files[0];
if (!file) return;
const raw = /\.estx$/i.test(file.name)
? await EstApp.api("/api/import?name=" + encodeURIComponent(file.name), {
method: "POST",
body: await file.arrayBuffer(),
})
: JSON.parse(await file.text());
const incoming = EstModel.legacy(raw);
if (documentKey !== EstApp.state.documentKey)
throw Error("문서가 변경되어 불러오기를 취소했습니다.");
EstApp.change(
() =>
(EstApp.state.project =
purpose === "load-contract"
? EstBusiness.importContract(EstApp.state.project, incoming)
: EstBusiness.importBid(EstApp.state.project, incoming)),
false,
);
} catch (error) {
EstApp.state.notice = error.message;
EstApp.render();
} finally {
input.value = "";
}
});
}
return { handle, change, install };
})();