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

700 lines
29 KiB
JavaScript

const EstActions = (() => {
let pending = null,
clipboard = [],
clipboardTable = "",
restoring = false;
function form(title, fields, callback) {
const { esc, showDialog } = EstApp;
showDialog(
title,
`<div class="form-fields">${fields.map((f) => `<label>${esc(f.label)}${f.choices ? `<select name="${f.name}">${f.choices.map(([v, label]) => `<option value="${esc(v)}" ${String(v) === String(f.value) ? "selected" : ""}>${esc(label)}</option>`).join("")}</select>` : `<input name="${f.name}" type="${f.type || "text"}" value="${esc(f.value ?? "")}" step="any">`}</label>`).join("")}</div><button data-action="dialog-apply">적용</button>`,
);
pending = () =>
callback(
Object.fromEntries(
fields.map((f) => {
const node = document.querySelector(`#dialog-content [name="${f.name}"]`);
return [f.name, f.type === "number" ? Number(node.value) : node.value];
}),
),
);
}
function selected() {
return [...EstApp.state.selection].sort((a, b) => a - b);
}
function activeRows() {
const { state } = EstApp;
const rows = state.project.tables[state.view];
if (!rows) throw Error("작업 표를 먼저 선택하세요.");
return rows;
}
async function undo(redo = false) {
const { state } = EstApp,
source = redo ? state.future : state.history,
target = redo ? state.history : state.future;
if (!source.length || restoring) return;
const entry = source.at(-1),
revision = state.revision,
documentKey = state.documentKey;
restoring = true;
try {
const project = typeof entry === "string" ? JSON.parse(entry) : await EstApp.restore(entry);
if (revision !== state.revision || documentKey !== state.documentKey) return;
target.push(EstApp.snapshot());
if (target.length > 8) target.shift();
source.pop();
const id = state.project.id;
state.project = project;
if (id) state.project.id = id;
state.revision++;
state.selection.clear();
EstApp.cache();
EstApp.render();
} finally {
restoring = false;
}
}
function insertRow() {
const rows = activeRows(),
{ state } = EstApp;
EstApp.change(() => {
const row = EstModel.newRow(state.view, rows, state.project);
if (state.view === "MachMainDat") row.Index = -3;
const group = EstNative.groups(rows).get(state.group);
const at =
selected().at(-1) ?? (group ? group.start + group.rows.length - 1 : rows.length - 1);
rows.splice(at + 1, 0, row);
state.selection = new Set([at + 1]);
});
}
function insertGroup() {
const { state } = EstApp,
rows = activeRows();
form(
"새 묶음 만들기",
[
{ name: "code", label: "코드", value: `U${Date.now().toString(36)}` },
{ name: "name", label: "품명" },
{ name: "size", label: "규격" },
{ name: "unit", label: "단위", value: "식" },
],
(values) => {
if (!values.code || rows.some((r) => r.Code === values.code && Number(r.Index) > 0))
throw Error("코드를 입력하거나 중복 코드를 바꾸세요.");
EstApp.change(() => {
const index = Math.max(0, ...rows.map((r) => Number(r.Index) || 0)) + 1;
const row = {
...EstModel.newRow(state.view, rows, state.project),
Index: index,
Code: values.code,
Name: values.name,
Size: values.size,
Unite: values.unit,
GN: 0,
};
if (state.view === "CostMainDat")
row.LineData = `\x0e${values.name}\x0e \x13${values.size}\x13 \x15${values.unit}\x15`;
rows.push(row);
state.group = values.code;
});
},
);
}
function deleteRows() {
const { state } = EstApp,
rows = activeRows(),
indexes = new Set(selected());
if (!indexes.size) throw Error("삭제할 행을 선택하세요.");
for (const group of ["HeadMainDat", "CostMainDat", "MachMainDat"].includes(state.view)
? EstNative.groups(rows).values()
: [])
if (indexes.has(group.start))
for (let i = group.start; i < group.start + group.rows.length; i++) indexes.add(i);
const codes = [...indexes].map((i) => rows[i].Code).filter(Boolean),
kind = Number(
Object.entries(EstNative.sourceTables).find(([, table]) => table === state.view)?.[0] ||
{ HeadMainDat: 103, CostMainDat: 104, MachMainDat: 105 }[state.view],
);
if (
kind &&
Object.entries(state.project.tables).some(([table, list]) =>
list.some((row, i) =>
table !== state.view || !indexes.has(i)
? row.LinkIndex === kind && codes.includes(row.Code)
: false,
),
)
)
throw Error("다른 항목에서 사용 중입니다. 연결을 먼저 해제하세요.");
if (!confirm(`${indexes.size}개 행을 삭제하시겠습니까?`)) return;
EstApp.change(() => {
[...indexes].sort((a, b) => b - a).forEach((i) => rows.splice(i, 1));
state.selection.clear();
});
}
async function action(name, node) {
const { state, api, esc, number, showDialog } = EstApp,
project = state.project;
state.notice = "";
if (name === "new") {
if (confirm("저장하지 않은 입력을 비우고 새 내역서를 만드시겠습니까?"))
EstApp.setProject(EstNative.empty());
} else if (name === "sample") {
if (confirm("현재 작업을 REA 샘플로 바꾸시겠습니까?"))
EstApp.setProject(await api("/api/sample"));
} else if (name === "open") EstApp.byId("open-file").click();
else if (name === "save") await EstApp.save();
else if (name === "save-as")
form(
"다른 이름으로 저장",
[{ name: "name", label: "작업 이름", value: project.name }],
async (values) => {
const nextName = values.name.trim();
const input = document.querySelector('#dialog-content [name="name"]');
input.setCustomValidity(nextName ? "" : "작업 이름을 입력하세요.");
if (!input.reportValidity()) throw Error("작업 이름을 입력하세요.");
EstApp.change(() => (project.name = nextName), false);
await EstApp.save(true);
},
);
else if (name === "undo") await undo();
else if (name === "redo") await undo(true);
else if (name === "add-row") insertRow();
else if (name === "add-group") insertGroup();
else if (name === "delete-row") deleteRows();
else if (name === "copy-row" || name === "cut-row") {
const rows = activeRows(),
indices = new Set(selected());
if (["HeadMainDat", "CostMainDat", "MachMainDat"].includes(state.view))
for (const group of EstNative.groups(rows).values())
if (indices.has(group.start))
for (let i = group.start; i < group.start + group.rows.length; i++) indices.add(i);
clipboard = [...indices].sort((a, b) => a - b).map((i) => structuredClone(rows[i]));
if (!clipboard.length) throw Error("복사할 행을 선택하세요.");
clipboardTable = state.view;
if (name === "cut-row") deleteRows();
} else if (name === "paste-row") {
if (!clipboard.length) throw Error("복사한 행이 없습니다.");
if (clipboardTable !== state.view)
throw Error("복사한 행과 같은 종류의 자료에 붙여넣으세요.");
const rows = activeRows();
EstApp.change(() => {
let at = (selected().at(-1) ?? rows.length - 1) + 1;
const grouped = ["HeadMainDat", "CostMainDat", "MachMainDat"].includes(state.view);
if (grouped && clipboard.some((r) => Number(r.Index) > 0))
for (const group of EstNative.groups(rows).values())
if (at > group.start && at <= group.start + group.rows.length) {
at = group.start + group.rows.length;
break;
}
const copies = structuredClone(clipboard);
let next = EstModel.newRow(state.view, rows, state.project).PkID - 1;
let groupCode = "",
nextIndex = Math.max(0, ...rows.map((r) => Number(r.Index) || 0));
copies.forEach((r) => {
r.PkID = ++next;
r.NewItem = true;
delete r.PreviousCodes;
if (["StuffcDat", "LaborcDat", "SilJukDat", "EstiMateDat"].includes(state.view)) {
const base = r.Code || "U";
let suffix = next;
do {
r.Code = `${base}_${suffix++}`;
} while (
rows.some((row) => row.Code === r.Code) ||
copies.some((row) => row !== r && row.Code === r.Code)
);
}
if (grouped && Number(r.Index) > 0 && r.Code) {
const base = r.Code;
let suffix = next;
do {
r.Code = `${base}_${suffix++}`;
} while (
rows.some((row) => row.Code === r.Code) ||
copies.some((row) => row !== r && Number(row.Index) > 0 && row.Code === r.Code)
);
r.Index = ++nextIndex;
groupCode = r.Code;
}
if (grouped && groupCode) r.TitleCode = groupCode;
});
rows.splice(at, 0, ...copies);
});
} else if (name === "recalculate") {
if (project.options.AutoCalc === false)
throw Error("내역서 자동계산이 꺼져 있어 전체 재계산을 실행할 수 없습니다.");
EstApp.calculate();
EstApp.cache();
EstApp.render();
} else if (name === "find") EstApp.byId("search").focus();
else if (name === "level") {
if (!selected().length) throw Error("레벨을 바꿀 행을 선택하세요.");
const level = Number(node.dataset.level);
EstApp.change(() => selected().forEach((i) => (activeRows()[i].Level = level)));
} else if (name === "export-xlsx") await EstExchange.exportExcel();
else if (name === "export-report-xlsx") await EstReport.exportExcel();
else if (name === "export-estx") await EstExchange.exportNative();
else if (name === "export-jdl") await EstExchange.exportJdl();
else if (name === "import-jdl") EstApp.byId("jdl-file").click();
else if (name === "export-xml")
form(
"조달청 XML 작성정보",
[
{ name: "company", label: "작성업체명", value: project.exportMeta?.company || "" },
{
name: "kind",
label: "공사구분",
value: project.exportMeta?.kind || "",
choices: [
["", "선택하세요"],
...[
"토목공사",
"건축공사",
"설비공사",
"전기공사",
"통신공사",
"조경공사",
"소방공사",
"문화재수리공사",
"국가유산수리공사",
].map((v) => [v, v]),
],
},
{ name: "number", label: "공사관리번호", value: project.exportMeta?.number || "" },
],
(meta) => {
const output = EstC3r.output(project, meta);
if (
output.warnings.length &&
!confirm(
output.warnings.slice(0, 15).join("\n") +
`\n총 ${output.warnings.length}건. 단가 전개 결과를 XML로 저장할까요?`,
)
)
return;
EstApp.change(() => (project.exportMeta = meta), false);
EstApp.download(`${project.name}.xml`, output.xml, "application/xml;charset=utf-8");
},
);
else if (name === "import-xml") EstApp.byId("xml-file").click();
else if (name === "import-xlsx") EstApp.byId("excel-file").click();
else if (name === "print") await EstReport.open();
else if (name === "print-document") EstReport.print();
else if (name === "csv") {
const rows = activeRows(),
keys = EstModel.columns(state.view);
const quote = (value) => `"${String(value ?? "").replaceAll('"', '""')}"`;
const csv = [
keys.map((key) => EstModel.labels[key] || key),
...rows.map((row) =>
keys.map((key) =>
typeof row[key] === "number"
? row[key]
: /^[=+\-@]/.test(String(row[key] || ""))
? `'${row[key]}`
: EstText.clean(row[key]),
),
),
]
.map((row) => row.map(quote).join(","))
.join("\r\n");
EstApp.download(
`${state.project.name}_${state.view}.csv`,
"\uFEFF" + csv,
"text/csv;charset=utf-8",
);
} else if (name === "projects") {
const projects = await api("/api/projects");
showDialog(
"저장한 작업",
projects.length
? projects
.map(
(p) =>
`<p><button data-action="load-project" data-id="${p.id}">${esc(p.name)}</button> <small>${esc(p.modified)}</small></p>`,
)
.join("")
: "저장된 작업이 없습니다.",
);
} else if (name === "load-project") {
if (confirm("현재 작업을 저장한 작업으로 바꾸시겠습니까?")) {
EstApp.setProject(await api(`/api/projects/${node.dataset.id}`));
EstApp.byId("dialog").close();
}
} else if (name === "parity") {
const result = await api("/api/parity");
showDialog(
"REA 샘플 대조",
`<p><b>${result.checked}개 중 ${result.passed}개 일치 · 불일치 ${result.failed}개</b></p><p>보관된 EST 샘플의 저장값과 계산 결과를 비교했습니다. 원본 실행 비교는 포함하지 않습니다.</p>${result.errors.length ? `<p>${esc(result.errors.join(" / "))}</p>` : ""}<table><thead><tr><th>영역</th><th>검사 수</th><th>일치</th></tr></thead><tbody>${[...new Set(result.results.map((r) => r.group))].map((group) => `<tr><td>${esc(group)}</td><td>${result.results.filter((r) => r.group === group).length}</td><td>${result.results.filter((r) => r.group === group && r.pass).length}</td></tr>`).join("")}</tbody></table>`,
);
} else if (name === "settings") EstSettings.show();
else if (name === "raw-line") {
const row = activeRows()[Number(node.dataset.row)];
showDialog(
"산출근거 편집",
`<p>표지는 짝으로 지정합니다. 구간지정 A~Z는 결과 앞에, 구간합계는 합산할 같은 문자를 감쌉니다.</p><div class="marker-tools">${["품명", "규격", "단위", "단가", "변수", "다른줄", "결과", "합계", "구간지정", "구간합계"].map((m) => `<button data-marker="${m}">${m}</button>`).join("")}</div><textarea id="line-editor" rows="10">${esc(EstText.display(row.LineData))}</textarea><button data-action="line-apply" data-row="${node.dataset.row}">적용</button>`,
);
} else if (name === "line-apply") {
const text = EstApp.byId("line-editor").value;
EstApp.change(() => (activeRows()[Number(node.dataset.row)].LineData = EstText.encode(text)));
EstApp.byId("dialog").close();
} else if (name === "template") {
const templates = project.wongaTemplates || [];
showDialog(
"원가 서식 선택",
`<p>현재 원가 표를 바꾸므로 작업을 저장한 뒤 선택하세요.</p>${templates.map((t, i) => `<button data-action="use-template" data-row="${i}">원본 서식 ${t.index}</button>`).join("")}<button data-action="basic-wonga">비목 기초 뼈대 만들기</button>`,
);
} else if (name === "use-template") {
if (confirm("현재 원가 표를 선택한 서식으로 바꾸시겠습니까?"))
EstApp.change(
() =>
(project.tables.Wonga = structuredClone(
project.wongaTemplates[Number(node.dataset.row)].rows,
)),
);
EstApp.byId("dialog").close();
} else if (name === "basic-wonga") {
if (confirm("현재 원가 표를 기본 뼈대로 바꾸시겠습니까?"))
EstApp.change(() => {
project.options.TotalFix = false;
project.tables.Wonga = [
{ Gubun: "N", Name: "직접 노무비", BaseField: "NC", Cost: 0 },
{ Gubun: "G", Name: "직접 재료비", BaseField: "GC", Cost: 0 },
{ Gubun: "J", Name: "직접 경비", BaseField: "JC", Cost: 0 },
{ Gubun: "S", Name: "표준시장단가", BaseField: "SC", Cost: 0 },
{ Gubun: "T", Name: "순공사비", VSusic: "N+G+J+S" },
{ Gubun: "R", Name: "도급액", VSusic: "T", LinkIndex: 55 },
];
});
EstApp.byId("dialog").close();
} else if (name === "capture-baseline") {
if (
confirm(
"현재 계산값을 당초·계약 내역으로 보존하시겠습니까? 기존 당초 사본이 있으면 교체됩니다.",
)
)
EstApp.change(() => (state.project = EstBusiness.baseline(project)));
} else if (name === "capture-bid") {
if (confirm("현재 단가를 입찰 당시 단가로 기록하시겠습니까?"))
EstApp.change(() =>
project.tables.EstMainDat.forEach((row) =>
EstMath.units.forEach((key) => (row[`A${key}`] = EstMath.num(row[key]))),
),
);
} else if (name === "recalculate-original")
form(
"당초분 재계산",
[
{
name: "method",
value: String(project.options.OldRecalcMethod || 0),
label: "계산 방법",
choices: [
["0", "신규공종만 계산"],
["1", "변경된 단가만"],
["2", "당초분 전체"],
],
},
],
(values) =>
EstApp.change(() => {
project.baseline = EstBusiness.original(project, Number(values.method));
project.options.OldRecalcMethod = Number(values.method);
}),
);
else if (name === "bid-rates")
form(
"비목별 낙찰률",
[
{ name: "N", label: "노무 %", type: "number", value: 100 },
{ name: "G", label: "재료 %", type: "number", value: 100 },
{ name: "J", label: "경비 %", type: "number", value: 100 },
{ name: "S", label: "실적 %", type: "number", value: 100 },
],
(values) => {
if (Object.values(values).some((n) => !Number.isFinite(n) || n < 0))
throw Error("낙찰률은 0 이상의 수치입니다.");
EstApp.change(
() =>
(state.project = EstBusiness.bid(project, [values.N, values.G, values.J, values.S])),
);
},
);
else if (name === "restore-bid") {
if (confirm("당초단가(100%)로 복원하시겠습니까?"))
EstApp.change(() => {
state.project = EstBusiness.restoreBid(project);
});
} else if (name === "add-quantity")
EstApp.change(() => {
project.quantities.push({
Code: EstQuantity.newCode(project.quantities),
Name: "새 수량",
TargetCode: "",
formula: "1",
digits: 2,
});
}, false);
else if (name === "apply-quantity") {
const totals = EstQuantity.totals(project, state.quantitySheet);
if (confirm("연결된 내역 수량을 산출 결과로 바꾸시겠습니까?"))
EstApp.change(() => EstQuantity.applyTotals(project, totals));
} else if (name === "add-round") {
const next = {
id: EstApp.uid(),
name: EstBusiness.newProgressName(project.progress),
inputs: {},
approved: false,
locked: false,
};
EstApp.change(() => {
project.progress.push(next);
state.roundId = next.id;
}, false);
} else if (name === "lock-round") {
if (
confirm("이 회차를 확정하고 저장하시겠습니까? 전회 누계로 보존되고 직접 편집이 잠깁니다.")
) {
EstApp.change(() => (state.project = EstProgress.lock(project, state.roundId)), false);
await EstApp.save();
}
} else if (name === "add-annual")
EstApp.change(
() =>
project.annual.push({
name: EstBusiness.newAnnualName(project.annual),
formula: "[기준]",
}),
false,
);
else if (name === "move-annual") {
const index = Number(node.dataset.row),
next = index + Number(node.dataset.direction);
if (index >= 0 && index < project.annual.length && next >= 0 && next < project.annual.length)
EstApp.change(() => {
[project.annual[index], project.annual[next]] = [
project.annual[next],
project.annual[index],
];
}, false);
} else if (name === "open-annual") {
const result = EstBusiness.annualCopy(project, Number(node.dataset.row));
if (confirm("현재 화면을 차수 내역 사본으로 바꾸시겠습니까?")) {
EstApp.setProject(result);
EstApp.navigate("EstMainDat");
}
} else if (name === "delete-extra") {
if (confirm("이 항목을 삭제하시겠습니까?"))
EstApp.change(
() => project[node.dataset.section].splice(Number(node.dataset.row), 1),
false,
);
} else if (name === "dialog-apply" && pending) {
await pending();
pending = null;
EstApp.byId("dialog").close();
} else if (name === "close-dialog") EstApp.byId("dialog").close();
else await EstBusinessActions.handle(name, node);
}
function install() {
EstSchedule.install();
EstExchange.install();
EstBusinessActions.install();
EstIssues.install();
document.addEventListener("click", async (event) => {
try {
const marker = event.target.closest("[data-marker]");
if (marker) {
const editor = EstApp.byId("line-editor"),
start = editor.selectionStart,
end = editor.selectionEnd,
tag = `⟦${marker.dataset.marker}⟧`;
editor.setRangeText(tag + editor.value.slice(start, end) + tag, start, end, "select");
editor.focus();
return;
}
const view = event.target.closest("[data-view]");
if (view) {
EstApp.navigate(view.dataset.view);
return;
}
const group = event.target.closest("[data-group]");
if (group) {
EstApp.navigate(EstApp.state.view, group.dataset.group);
return;
}
const node = event.target.closest("[data-action]");
if (node) {
await action(node.dataset.action, node);
node.closest("details")?.removeAttribute("open");
}
} catch (error) {
EstApp.state.notice = error.message;
EstApp.render();
}
});
document.addEventListener("change", (event) => {
const node = event.target,
{ state } = EstApp,
project = state.project;
state.notice = "";
try {
if (EstBusinessActions.change(node)) return;
if (node.matches("[data-select]")) {
const index = Number(node.dataset.select);
node.checked ? state.selection.add(index) : state.selection.delete(index);
EstApp.render();
return;
}
if (node.id === "project-name") {
EstApp.change(() => (project.name = node.value), false);
return;
}
if (node.id === "automatic" || node.id === "refresh-sources") {
EstApp.change(
() =>
(project.options[node.id === "automatic" ? "AutoCalc" : "SourceRefresh"] =
node.checked),
);
return;
}
if (node.id === "round-choice") {
state.roundId = node.value;
EstApp.render();
return;
}
if (node.id === "escalation-component") {
state.escalationComponent = node.value;
EstApp.render();
return;
}
if (node.id === "use-approved") {
const round = project.progress.find((r) => r.id === state.roundId);
if (round.locked) throw Error("확정 회차입니다.");
EstApp.change(() => (round.approved = node.checked), false);
return;
}
if (node.matches("[data-progress]")) {
const round = project.progress.find((r) => r.id === state.roundId);
if (round.locked) throw Error("확정 회차입니다.");
if (EstProgress.ambiguousKeys(project.tables.EstMainDat).has(node.dataset.key))
throw Error("기성 내역 식별자가 중복되어 입력할 수 없습니다.");
const value = node.type === "number" ? Number(node.value) : node.value;
if (node.type === "number" && !Number.isFinite(value)) throw Error("수치 입력 오류");
EstApp.change(() => {
round.inputs[node.dataset.key] ||= {};
round.inputs[node.dataset.key][node.dataset.progress] = value;
}, false);
return;
}
if (node.matches("[data-extra]")) {
const value = node.type === "number" ? Number(node.value) : node.value;
if (node.type === "number" && !Number.isFinite(value)) throw Error("수치 입력 오류");
EstApp.change(() => {
const row = project[node.dataset.extra][Number(node.dataset.row)];
row[node.dataset.field] = value;
if (node.dataset.extra === "quantities" && node.dataset.field === "TargetCode")
row.TargetKey = "";
}, false);
return;
}
if (node.matches("[data-table][data-field]")) {
const row = project.tables[node.dataset.table][Number(node.dataset.row)],
key = node.dataset.field;
const numeric = EstModel.numeric.has(key),
value = numeric
? node.value === ""
? null
: Number(node.value)
: key === "LineData"
? EstText.encode(node.value)
: node.value;
if (
numeric &&
value !== null &&
(!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER)
)
throw Error("유효한 수치를 입력하세요.");
if (key === "Level" && (value < 0 || value > 9 || !Number.isInteger(value)))
throw Error("레벨은 0~9 정수입니다.");
if (key === "Precision" && (value < 0 || value > 6 || !Number.isInteger(value)))
throw Error("소수 자릿수는 0~6 정수입니다.");
EstApp.remember();
if (key === "Code") {
const previous = row.Code;
EstModel.editCode(project, node.dataset.table, Number(node.dataset.row), value);
if (state.group === previous && Number(row.Index) > 0) state.group = row.Code;
} else if (key === "SCheck")
EstBusinessActions.setClassification(project, row, node, value);
else row[key] = value;
if (state.view === "escalation" && EstMath.units.includes(key))
project.options.SourceRefresh = false;
if (["StuffcDat", "LaborcDat", "EstiMateDat", "SilJukDat"].includes(node.dataset.table)) {
if (key === "TC") {
row.InputTC = value;
row.ValueIndex = 4;
}
if (["NC", "GC", "JC", "SC"].includes(key)) row[`Input${key}`] = value;
project.options.SourceRefresh = true;
}
if (project.options.AutoCalc !== false) EstApp.calculate();
EstApp.cache();
setTimeout(() => EstApp.render(), 0);
}
} catch (error) {
state.notice = error.message;
EstApp.render();
}
});
EstApp.byId("open-file").addEventListener("change", async (event) => {
try {
await EstApp.open(event.target.files[0]);
} catch (error) {
EstApp.state.notice = error.message;
EstApp.render();
} finally {
event.target.value = "";
}
});
EstApp.byId("search").addEventListener("input", (event) => {
EstApp.state.query = event.target.value;
EstApp.state.page = 0;
EstApp.render();
});
EstApp.byId("previous-page").addEventListener("click", () => {
EstApp.state.page--; EstApp.render();
});
EstApp.byId("next-page").addEventListener("click", () => {
EstApp.state.page++; EstApp.render();
});
document.addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "s") {
event.preventDefault();
EstApp.save().catch((error) => {
EstApp.state.notice = error.message; EstApp.render();
});
}
});
document.addEventListener("dblclick", (event) => {
const input = event.target.closest('[data-field="Code"]');
if (!input) return;
const row = EstApp.state.project.tables[input.dataset.table][Number(input.dataset.row)],
kind = Number(row.LinkIndex);
const table =
{ 103: "HeadMainDat", 104: "CostMainDat", 105: "MachMainDat" }[kind] ||
EstNative.sourceTables[kind];
if (table) {
EstApp.navigate(table, row.Code);
if (![103, 104, 105].includes(kind)) {
EstApp.state.query = row.Code;
EstApp.render();
}
}
});
EstAttachments.install();
EstStandard.install();
}
return { install, form, undo };
})();