Files
Aislo/tools/est-test/native-progress-wonga.js
T

251 lines
10 KiB
JavaScript

// 원본 기성 원가계산서(TfrmWongaAccomH 0xd48408 / V 0xd46914) 편집 — 회차별 원가 서식 사본(round.wonga)에만 적용.
// 한줄 삽입·추가(0xd49758: 새 행 Index 0·CalcIndex 0·Checked 끔, 앞 행 끝줄 표지 이어받기), 한줄 삭제(확인 뒤, 재계산 없음),
// 내용 삭제(LinkIndex·Cost·Biul·Youl·VSusic·Diget 비움), 선택항목 절하(0xd094dc, 기성은 도급액 행도 허용),
// 선택데이타 절하(popupTrunc: 직전 자리수를 그대로, CalcBase 행 제외), 공사비역산 자리수(목표 = 10^n 미만 절하 → 이윤 역산),
// 초기화(사본 지움 = 도급 원가 서식), 되살리기(직전 조작 전), 산출근거 약어로 표시(A8: 수식칸 편집 잠금만).
// 원본의 번호·참조 재작성(0xc97398·0xc971e8)은 하지 않는다 — 웹 수식은 Gubun·이름으로 찾으므로 새 행은 기호를 직접 적는다.
const EstProgressWonga = (() => {
const num = (value) => Number(value) || 0;
function template(project, round) {
return round.contract?.wonga || project.tables.Wonga || [];
}
function edit(project, round, mutate) {
if (round.locked) throw Error("확정한 회차는 고칠 수 없습니다.");
round.wongaUndo = round.wonga ? structuredClone(round.wonga) : null;
round.wonga ||= structuredClone(template(project, round));
return mutate(round.wonga);
}
function blank(list, at) {
const row = { Index: 0, Gubun: "", Name: "", Youl: "", VSusic: "", Cost: 0, CalcIndex: 0 };
const prev = list[at - 1],
end = num(prev?.EndLineIndex);
row.EndLineIndex = end === 1 ? 1 : end === 3 ? 3 : 0;
if (end === 3) prev.EndLineIndex = 0;
row.Checked = false;
return row;
}
const ops = {
insert(project, round, at) {
edit(project, round, (list) => list.splice(at, 0, blank(list, at)));
},
append(project, round) {
edit(project, round, (list) => list.push(blank(list, list.length)));
},
remove(project, round, at) {
edit(project, round, (list) => {
if (!list[at]) throw Error("삭제할 줄을 고르세요.");
list.splice(at, 1);
});
},
clear(project, round, at) {
edit(project, round, (list) => {
if (!list[at]) throw Error("내용을 지울 줄을 고르세요.");
Object.assign(list[at], {
LinkIndex: 0,
Cost: 0,
Biul: "",
Youl: "",
VSusic: "",
Diget: 0,
Checked: false,
});
});
},
position(project, round, at, method, digits) {
edit(project, round, (list) =>
EstWongaTools.position(
{ tables: { Wonga: list }, options: project.options },
[at],
method,
digits,
true,
),
);
},
trunc(project, round, at) {
edit(project, round, (list) => {
if (!list[at]) throw Error("절하할 줄을 고르세요.");
if (!list[at].CalcBase) list[at].Diget = num(project.options.WongaLastDiget);
});
},
total(project, round, digits) {
if (round.locked) throw Error("확정한 회차는 고칠 수 없습니다.");
if (!Number.isInteger(digits) || digits < 0 || digits > 9)
throw Error("자리수는 0~9 사이로 입력하세요.");
if (digits) round.wongaTotalDigits = digits;
else delete round.wongaTotalDigits;
},
init(project, round) {
if (round.locked) throw Error("확정한 회차는 고칠 수 없습니다.");
round.wongaUndo = round.wonga ? structuredClone(round.wonga) : null;
delete round.wonga;
delete round.wongaTotalDigits;
},
undo(project, round) {
if (round.locked) throw Error("확정한 회차는 고칠 수 없습니다.");
if (!("wongaUndo" in round)) throw Error("되살릴 조작이 없습니다.");
if (round.wongaUndo) round.wonga = round.wongaUndo;
else delete round.wonga;
delete round.wongaUndo;
},
field(project, round, at, name, value) {
edit(project, round, (list) => {
if (!list[at] || !["Gubun", "Name", "Youl", "VSusic", "Cost"].includes(name))
throw Error("고칠 칸이 없습니다.");
list[at][name] = name === "Cost" ? num(String(value).replaceAll(",", "")) : value;
});
},
};
// 화면
function render(state, round, output) {
const { esc, number } = EstApp,
project = state.project,
list = round.wonga || template(project, round),
locked = round.locked,
lockFormula = project.options.WongaSNameShow,
pick = state.progressWongaRow;
const b = (action, label) =>
`<button data-action="${action}" ${locked ? "disabled" : ""}>${label}</button>`;
const tools = [
b("pw-insert", "한줄 삽입"),
b("pw-append", "한줄 추가"),
b("pw-delete", "한줄 삭제"),
b("pw-clear", "내용 삭제"),
b("pw-position", "선택항목 절하하기"),
b("pw-trunc", "선택데이타 절하하기"),
b("pw-total", "공사비역산 자리수 맞추기"),
b("pw-init", "원가계산서 초기화"),
b("pw-undo", "되살리기"),
`<label><input type="checkbox" data-pw-sname ${lockFormula ? "checked" : ""}> 산출근거 약어로 표시</label>`,
round.wonga ? "<span>회차 서식 사용 중</span>" : "",
round.wongaTotalDigits ? `<span>역산 ${round.wongaTotalDigits}자리</span>` : "",
].join("");
const input = (i, name, value, disabled) =>
`<input data-pw-field="${name}" data-i="${i}" value="${esc(value ?? "")}" ${locked || disabled ? "disabled" : ""}${name === "Name" || name === "VSusic" ? "" : ' size="8"'}>`;
const body = (output.costs || [])
.map((row, i) => {
const tpl = list[i] || {};
return `<tr><td><input type="radio" name="pw-row" data-pw-pick="${i}" ${pick === i ? "checked" : ""}></td><td>${input(i, "Gubun", tpl.Gubun, false)}</td><td>${input(i, "Name", tpl.Name, false)}</td><td>${input(i, "Youl", tpl.Youl, false)}</td><td>${input(i, "VSusic", tpl.VSusic, lockFormula)}</td><td>${num(tpl.Diget) ? `${tpl.Diget}/${num(tpl.DigetIndex)}` : ""}</td><td>${number(row.ContractCost)}</td><td>${number(row.JunCost)}</td><td>${number(row.GumCost)}</td><td>${number(row.SaCost)}</td><td>${number(row.TotalCost)}</td></tr>`;
})
.join("");
return `<h4>기성 원가계산서</h4><div class="business-tools">${tools}</div><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></tr></thead><tbody>${body}</tbody></table>`;
}
function round(state) {
return state.project.progress.find((r) => r.id === state.roundId);
}
function at(state) {
const i = state.progressWongaRow;
if (!Number.isInteger(i)) throw Error("기성 원가계산서에서 줄을 고르세요.");
return i;
}
const names = new Set([
"pw-insert",
"pw-append",
"pw-delete",
"pw-clear",
"pw-position",
"pw-trunc",
"pw-total",
"pw-init",
"pw-undo",
]);
function handle(name, state) {
if (!names.has(name)) return false;
const r = round(state);
if (!r) throw Error("기성 회차를 고르세요.");
const run = (fn) => EstApp.change((project) => fn(project, project.progress.find((x) => x.id === r.id)));
if (name === "pw-insert") run((p, x) => ops.insert(p, x, at(state)));
else if (name === "pw-append") run((p, x) => ops.append(p, x));
else if (name === "pw-delete") {
const i = at(state);
if (confirm("현재 줄을 삭제하시겠습니까?")) {
run((p, x) => ops.remove(p, x, i));
state.progressWongaRow = undefined;
}
} else if (name === "pw-clear") run((p, x) => ops.clear(p, x, at(state)));
else if (name === "pw-trunc") run((p, x) => ops.trunc(p, x, at(state)));
else if (name === "pw-init") {
if (confirm("이 회차의 원가계산서를 도급 원가 서식으로 되돌리시겠습니까?")) run(ops.init);
} else if (name === "pw-undo") run(ops.undo);
else if (name === "pw-position") {
const i = at(state);
EstActions.form(
"선택항목 절하하기",
[
{
name: "method",
label: "처리방식",
value: 2,
choices: [
[1, "사사오입"],
[2, "절하"],
[3, "절상"],
[0, "초기화"],
],
},
{
name: "digits",
label: "자리 (0 원 · 1 십원 … 6 백만원)",
type: "number",
value: num(state.project.options.WongaLastDiget),
},
],
(values) =>
run((p, x) => ops.position(p, x, i, Number(values.method), Number(values.digits))),
);
} else if (name === "pw-total")
EstActions.form(
"공사비역산 자리수 맞추기",
[
{
name: "digits",
label: "도급액 절하 자리 (0 해제 · 3 천원 · 4 만원 …)",
type: "number",
value: r.wongaTotalDigits ?? 3,
},
],
(values) => run((p, x) => ops.total(p, x, Number(values.digits))),
);
return true;
}
function install() {
document.addEventListener("change", (event) => {
const t = event.target,
state = EstApp.state;
if (t.dataset.pwPick !== undefined) {
state.progressWongaRow = Number(t.dataset.pwPick);
return;
}
if (t.dataset.pwSname !== undefined) {
EstApp.change((project) => (project.options.WongaSNameShow = t.checked), false);
return;
}
if (t.dataset.pwField === undefined) return;
const r = round(state);
try {
EstApp.change((project) =>
ops.field(
project,
project.progress.find((x) => x.id === r.id),
Number(t.dataset.i),
t.dataset.pwField,
t.value,
),
);
} catch (error) {
state.notice = error.message;
EstApp.render();
}
});
}
if (typeof document !== "undefined" && document.addEventListener) install();
return { template, ops, render, handle };
})();