- 설계변경 내역서에서 회차 추가 때 당초/변경 고르기 - 당초면 행마다 당초 칸으로 Index·품명·규격·단위·수량·단가·금액(GC 포함) - 회차 추가 처리를 native-progress-view.js 로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
334 lines
16 KiB
JavaScript
334 lines
16 KiB
JavaScript
// 기성고 화면(native-extra.js 에서 옮김)과 원본 기성 「환경설정」(TfrmAccomOption — spec_pb1 §4: 주 메뉴 환경 설정 0x10ccddc 가
|
|
// 기성 문서일 때 · 기성 원가계산서 팝업 popupOptision 0xd479f4 Ctrl+U). 칸을 누르면 바로 옵션에 쓰고 「취소」면 연 때 값으로 되돌림(d45e1c).
|
|
// 공통적용(코드·수량 0·금액 0·비목순서·품명정렬·품명 두줄·Enter 이동·바탕색)은 다른 표와 같은 계산 환경 Display,
|
|
// 기성고(레벨·비고란 발췌근거·집계공종 진하게·사정금액·노재경 구분·실적 비목)는 AccomOption. 원본도 처리기가 없는
|
|
// 자동저장 분·가로세로 계산 확인·가로 계산방법·재료비 처리·이윤 사사오입은 값만 담는다(화면·계산 불변).
|
|
const EstProgressView = (() => {
|
|
const num = (v) => Number(v) || 0;
|
|
const COMMON = {
|
|
code: true,
|
|
gnZero: true,
|
|
costZero: true,
|
|
order: 0,
|
|
align: 0,
|
|
twoLine: false,
|
|
enter: 0,
|
|
color: "",
|
|
autoSave: false,
|
|
saveTime: 10,
|
|
};
|
|
const ACCOM = {
|
|
level: false,
|
|
gungu: false,
|
|
sumBold: false,
|
|
calcCheck: false,
|
|
horCalc: 0,
|
|
sajung: true,
|
|
gubun: false,
|
|
siljuk: false,
|
|
gcTo: 0,
|
|
eyunRound: false,
|
|
};
|
|
const get = (options) => ({
|
|
...COMMON,
|
|
...(options?.Display || {}),
|
|
...ACCOM,
|
|
...(options?.AccomOption || {}),
|
|
});
|
|
const ORDERS = ["TNGJ", "TGNJ", "NGJT", "GNJT"];
|
|
const PART = { T: "합계", N: "노무", G: "재료", J: "경비", S: "실적" };
|
|
|
|
// 비목 금액 열(금회): 비목순서대로, 실적은 실적 비목 표시 때만 경비 뒤에
|
|
function parts(o) {
|
|
const seq = [...(ORDERS[o.order] || ORDERS[0])];
|
|
if (o.siljuk) seq.splice(seq.indexOf("J") + 1, 0, "S");
|
|
return seq;
|
|
}
|
|
|
|
function view(state, project, { box, button, table }) {
|
|
const { esc } = EstApp,
|
|
o = get(project.options);
|
|
const shown = (v, zero) => (!zero && !num(v) ? "" : EstApp.number(v));
|
|
const qty = (v) => shown(v, o.gnZero),
|
|
cost = (v) => shown(v, o.costZero);
|
|
state.roundId ||= project.progress[0]?.id;
|
|
const round = project.progress.find((r) => r.id === state.roundId);
|
|
let body = '<p class="empty">회차를 추가하세요.</p>',
|
|
tools =
|
|
button("add-round", "기성 회차 추가") +
|
|
'<label>원본 기성 표 확인 <input type="file" accept=".estx" data-accom-inspect></label>' +
|
|
(project.progress.length ? button("clear-progress", "기성 전체 초기화") : "") +
|
|
button("accom-option", "환경 설정");
|
|
if (round) {
|
|
tools +=
|
|
`<select id="round-choice">${project.progress.map((r) => `<option value="${esc(r.id)}" ${r.id === round.id ? "selected" : ""}>${esc(r.name)}${r.locked ? " (확정)" : ""}</option>`).join("")}</select>` +
|
|
button(
|
|
round.locked ? "unlock-round" : "lock-round",
|
|
round.locked ? "확정 해제(이후 포함)" : "회차 확정·저장",
|
|
) +
|
|
button("fill-progress", "전량·비율 일괄 입력") +
|
|
button("rename-round", "회차 이름") +
|
|
button("delete-round", "회차 삭제") +
|
|
button("reset-round-inputs", "회차 입력 초기화") +
|
|
`<label><input id="progress-components" type="checkbox" ${round.separateComponents !== false ? "checked" : ""} ${round.locked ? "disabled" : ""}> 비목별 계산</label><label>금액 소수자리 <input id="progress-digits" type="number" min="0" max="4" value="${round.amountDigits ?? 0}" ${round.locked ? "disabled" : ""}></label>` +
|
|
`<label><input id="use-approved" type="checkbox" ${round.approved ? "checked" : ""} ${round.locked ? "disabled" : ""}> 누계에 사정 사용</label>`;
|
|
let output;
|
|
try {
|
|
output =
|
|
round.locked && round.result ? round.result : EstBusiness.progress(project, round.id);
|
|
} catch (e) {
|
|
output = { rows: [], errors: [e.message] };
|
|
}
|
|
state.business = output;
|
|
const page = EstBlockActions.progressPage(state, output.rows);
|
|
tools +=
|
|
button("progress-prev", "이전", state.progressPage === 0 ? "disabled" : "") +
|
|
`<span>${state.progressPage + 1} / ${page.pages}쪽 · ${page.total}행</span>` +
|
|
button("progress-next", "다음", state.progressPage + 1 >= page.pages ? "disabled" : "");
|
|
const seq = o.gubun ? parts(o) : ["T"];
|
|
const nameStyle = [
|
|
["", "text-align:right", "text-align:center"][o.align] || "",
|
|
o.twoLine ? "white-space:normal;max-width:16em" : "white-space:nowrap",
|
|
]
|
|
.filter(Boolean)
|
|
.join(";");
|
|
const headers = [
|
|
...(o.code ? ["코드"] : []),
|
|
...(o.level ? ["레벨"] : []),
|
|
"공종",
|
|
"도급수량",
|
|
"전회수량",
|
|
"금회 입력",
|
|
"입력방법",
|
|
...(o.sajung ? ["사정 입력", "입력방법"] : []),
|
|
...seq.map((p) => (p === "T" ? "금회금액" : `금회 ${PART[p]}`)),
|
|
...(o.sajung ? ["사정금액"] : []),
|
|
"누계수량",
|
|
"누계금액",
|
|
...(o.gungu ? ["비고"] : []),
|
|
];
|
|
body =
|
|
table(
|
|
headers,
|
|
page.rows.map(({ row, index }) => {
|
|
const editable = ![54, 55, 58, 59, 62, 67].includes(Number(row.LinkIndex));
|
|
const key = EstBusiness.rowKey(row, index),
|
|
input = round.inputs?.[key] || {};
|
|
const edit = (prefix) =>
|
|
`<input data-progress="${prefix}" data-key="${esc(key)}" type="number" step="any" value="${input[prefix] ?? row[prefix + "GN"] ?? 0}" ${round.locked || !editable || input[prefix + "Mode"] === "components" ? "disabled" : ""}>` +
|
|
(input[prefix + "Mode"] === "components"
|
|
? EstMath.components
|
|
.map(
|
|
(field, i) =>
|
|
`<label>${["노무", "재료", "경비", "실적"][i]}<input data-progress="${prefix + field}" data-key="${esc(key)}" type="number" step="any" value="${input[prefix + field] ?? 0}" ${round.locked ? "disabled" : ""}></label>`,
|
|
)
|
|
.join("")
|
|
: "");
|
|
const method = (prefix) =>
|
|
!editable
|
|
? "자동계산"
|
|
: `<select data-progress="${prefix}Mode" data-key="${esc(key)}" ${round.locked ? "disabled" : ""}>${[
|
|
["quantity", "수량"],
|
|
["amount", "금액"],
|
|
["ratio", "비율 %"],
|
|
["components", "비목 금액"],
|
|
]
|
|
.map(
|
|
([v, label]) =>
|
|
`<option value="${v}" ${v === (input[`${prefix}Mode`] || "quantity") ? "selected" : ""}>${label}</option>`,
|
|
)
|
|
.join("")}</select>`;
|
|
const bold = o.sumBold && num(row.Level) > 0 ? ";font-weight:bold" : "";
|
|
return [
|
|
...(o.code ? [esc(row.Code)] : []),
|
|
...(o.level ? [num(row.Level) || ""] : []),
|
|
`<span style="display:block;${nameStyle}${bold}">${esc(row.Name)}</span>`,
|
|
qty(row.GN),
|
|
qty(row.JunGN),
|
|
edit("Gum"),
|
|
method("Gum"),
|
|
...(o.sajung ? [edit("Sa"), method("Sa")] : []),
|
|
...seq.map((p) => cost(row[`Gum${p}C`])),
|
|
...(o.sajung ? [cost(row.SaTC)] : []),
|
|
qty(row.TotalGN),
|
|
cost(row.TotalTC),
|
|
...(o.gungu ? [esc(row.Bigo ?? "")] : []),
|
|
];
|
|
}),
|
|
) +
|
|
`<p class="business-note">${esc((output.errors || []).join(" / "))} 금회 도급액 ${EstApp.number(output.total)} · 기성비율 ${EstApp.number(output.ratio)}%</p>`;
|
|
body += EstProgressWonga.render(state, round, output);
|
|
}
|
|
const html = box(
|
|
"기성고",
|
|
tools,
|
|
body,
|
|
"전회는 직전 확정 회차 누계를 읽습니다. 금회·사정은 수량·금액·비율 중 하나를 입력해 계산합니다.",
|
|
);
|
|
return o.color ? `<div style="background:${esc(o.color)}">${html}</div>` : html;
|
|
}
|
|
|
|
// 환경설정 창: 칸을 바꾸면 바로 옵션에 쓰고(화면에 즉시), 「취소」 는 연 때 값으로 되돌림
|
|
let opened = null;
|
|
function dialog(state) {
|
|
const { esc } = EstApp,
|
|
o = get(state.project.options);
|
|
opened = {
|
|
Display: structuredClone(state.project.options.Display),
|
|
AccomOption: structuredClone(state.project.options.AccomOption),
|
|
};
|
|
const check = (name, label, extra = "") =>
|
|
`<label><input type="checkbox" data-ao="${name}" ${o[name] ? "checked" : ""} ${extra}> ${esc(label)}</label>`;
|
|
const pick = (name, label, choices, extra = "") =>
|
|
`<label>${esc(label)} <select data-ao="${name}" ${extra}>${choices.map((c, i) => `<option value="${i}" ${num(o[name]) === i ? "selected" : ""}>${esc(c)}</option>`).join("")}</select></label>`;
|
|
EstApp.showDialog(
|
|
"환경설정",
|
|
`<h4>공통적용</h4><div class="form-fields">${check("code", "코드표시")}${check("gnZero", '수량 "0" 표시')}${check("costZero", '금액 "0" 표시')}${check("autoSave", "데이타 자동저장")}<label>데이타 입력이 없으면 <input type="number" min="5" max="30" step="5" data-ao="saveTime" value="${num(o.saveTime)}" ${o.autoSave ? "" : "disabled"}> 분후 자동저장</label>${pick("order", "비목순서", ["합/노/재/경", "합/재/노/경", "노/재/경/합", "재/노/경/합"], o.gubun ? "" : "disabled")}${pick("align", "품명정렬", ["좌정렬", "우정렬", "중앙정렬"])}${pick("twoLine", "품명표기", ["한줄로 표시", "두줄로 표시"])}${pick("enter", "Enter 뒤 셀 이동", ["이동안함", "좌", "우", "위", "아래"])}<label>바탕색 <input type="color" data-ao="color" value="${esc(o.color || "#ffffff")}"></label></div>` +
|
|
`<h4>기성고</h4><div class="form-fields">${check("level", "레벨 표시")}${check("gungu", "비고란에 발췌근거 표시")}${check("sumBold", "집계공종 진하게 표시")}${check("calcCheck", "가로,세로 계산 확인")}${pick("horCalc", "데이타(가로) 계산방법", ["절하", "절상"])}${check("sajung", "사정금액 표시")}${check("gubun", "노,재,경 구분 표시")}${check("siljuk", "실적 비목 표시", o.gubun ? "" : "disabled")}</div>` +
|
|
`<h4>원가계산</h4><div class="form-fields">${pick("gcTo", "순직접공사비의 재료비 처리", ["간접재료비", "직접재료비"])}${check("eyunRound", "이윤 비율 사사오입")}</div>` +
|
|
`<button data-action="accom-option-ok">적 용</button> <button data-action="accom-option-cancel">취 소</button>`,
|
|
);
|
|
}
|
|
function set(name, value) {
|
|
const options = EstApp.state.project.options,
|
|
bucket = name in COMMON ? "Display" : "AccomOption";
|
|
const flag = typeof COMMON[name] === "boolean" || typeof ACCOM[name] === "boolean";
|
|
const typed = flag
|
|
? typeof value === "string"
|
|
? value === "1"
|
|
: Boolean(value)
|
|
: name === "color"
|
|
? value
|
|
: num(value);
|
|
options[bucket] = { ...(options[bucket] || {}), [name]: typed };
|
|
const box = document.getElementById("dialog-content");
|
|
if (name === "autoSave") box.querySelector('[data-ao="saveTime"]').disabled = !typed;
|
|
if (name === "gubun") {
|
|
box.querySelector('[data-ao="order"]').disabled = !typed;
|
|
box.querySelector('[data-ao="siljuk"]').disabled = !typed;
|
|
}
|
|
EstApp.render();
|
|
}
|
|
function close(apply) {
|
|
const options = EstApp.state.project.options;
|
|
if (opened) {
|
|
const now = { Display: options.Display, AccomOption: options.AccomOption };
|
|
for (const k of ["Display", "AccomOption"])
|
|
if (opened[k] === undefined) delete options[k];
|
|
else options[k] = opened[k];
|
|
// 적용: 연 때 값으로 돌린 뒤 바뀐 값을 한 번에 기록(되돌리기 한 단계)
|
|
if (apply) EstApp.change((project) => Object.assign(project.options, now), false);
|
|
}
|
|
opened = null;
|
|
EstApp.byId("dialog").close();
|
|
EstApp.render();
|
|
}
|
|
|
|
// Enter 뒤 셀 이동(ComboBoxEnter 62c+0x60): 표 안 입력칸에서 Enter 면 좌·우·위·아래 칸으로
|
|
function move(input, direction) {
|
|
const cell = input.closest("td"),
|
|
tr = cell?.parentElement;
|
|
if (!tr) return null;
|
|
const col = [...tr.children].indexOf(cell);
|
|
let target = null;
|
|
if (direction === 1 || direction === 2) {
|
|
const cells = [...tr.children];
|
|
for (
|
|
let c = col + (direction === 1 ? -1 : 1);
|
|
c >= 0 && c < cells.length;
|
|
c += direction === 1 ? -1 : 1
|
|
)
|
|
if ((target = cells[c].querySelector("input:not([disabled]),select:not([disabled])")))
|
|
break;
|
|
} else if (direction === 3 || direction === 4) {
|
|
for (
|
|
let r = direction === 3 ? tr.previousElementSibling : tr.nextElementSibling;
|
|
r;
|
|
r = direction === 3 ? r.previousElementSibling : r.nextElementSibling
|
|
)
|
|
if (
|
|
(target = r.children[col]?.querySelector("input:not([disabled]),select:not([disabled])"))
|
|
)
|
|
break;
|
|
}
|
|
return target;
|
|
}
|
|
function install() {
|
|
document.addEventListener("change", (event) => {
|
|
const t = event.target;
|
|
if (t.dataset?.ao === undefined) return;
|
|
set(t.dataset.ao, t.type === "checkbox" ? t.checked : t.value);
|
|
});
|
|
document.addEventListener("keydown", (event) => {
|
|
const state = EstApp.state;
|
|
if (state?.view !== "progress") return;
|
|
if (event.ctrlKey && event.key.toLowerCase() === "u") {
|
|
event.preventDefault();
|
|
dialog(state);
|
|
return;
|
|
}
|
|
const t = event.target;
|
|
if (event.key !== "Enter" || !t.matches?.("table.sheet input, table.sheet select")) return;
|
|
const direction = num(get(state.project.options).enter);
|
|
if (!direction) return;
|
|
const target = move(t, direction);
|
|
if (target) {
|
|
event.preventDefault();
|
|
t.dispatchEvent(new Event("change", { bubbles: true }));
|
|
setTimeout(() =>
|
|
(
|
|
document.querySelector(
|
|
`[data-key="${CSS.escape(target.dataset.key || "")}"][data-progress="${target.dataset.progress}"]`,
|
|
) || target
|
|
).focus(),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
if (typeof document !== "undefined" && document.addEventListener) install();
|
|
|
|
// 기성 회차 추가: 설계변경 내역서면 「도급내역을 선택하세요.」(당초/변경 — 닫기 없음, 고른 값으로 회차를 만듦)
|
|
function addRound(state) {
|
|
const project = state.project,
|
|
push = (loadMethod) => {
|
|
const next = {
|
|
id: EstApp.uid(),
|
|
name: EstBusiness.newProgressName(project.progress),
|
|
inputs: {},
|
|
approved: false,
|
|
locked: false,
|
|
...(loadMethod && { loadMethod }),
|
|
};
|
|
EstApp.change(() => {
|
|
project.progress.push(next);
|
|
state.roundId = next.id;
|
|
}, false);
|
|
};
|
|
if (project.options.Mode !== "설계변경") return push();
|
|
EstActions.form(
|
|
"도급내역을 선택하세요.",
|
|
[
|
|
{
|
|
name: "method",
|
|
label: "설계변경한 내역서입니다. 도급내역으로 불러올 데이타를 선택하세요.",
|
|
value: "origin",
|
|
choices: [
|
|
["origin", "당초 내역서"],
|
|
["change", "변경 내역서"],
|
|
],
|
|
},
|
|
],
|
|
(values) => push(values.method),
|
|
);
|
|
}
|
|
|
|
function handle(name, state) {
|
|
if (name === "accom-option") dialog(state);
|
|
else if (name === "accom-option-ok") close(true);
|
|
else if (name === "accom-option-cancel") close(false);
|
|
else return false;
|
|
return true;
|
|
}
|
|
return { view, get, parts, move, handle, dialog, addRound };
|
|
})();
|