320 lines
12 KiB
JavaScript
320 lines
12 KiB
JavaScript
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 };
|
||
})();
|