Files
Aislo/tools/est-test/native-report.js
T

255 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const EstReport = (() => {
let catalog = [];
function setting() {
const project = EstApp.state.project,
settings = project.printSettings || {};
return {
form: "current",
paper: "A4",
orientation: "landscape",
margin: 12,
font: 9,
company: "",
startPage: 1,
rowsPerPage: 30,
notes: "",
...settings,
};
}
function preview(all = false) {
const { state, esc } = EstApp;
const settings = setting(),
form = catalog.find((row) => String(row.Index) === settings.form),
output = form ? EstForms.data(form.Index, state.project, state) : null;
const number = (value) =>
new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 12 }).format(Number(value) || 0);
const root = document.createElement("div");
if (output) root.innerHTML = EstForms.grid(output);
else if (form) {
const total = state.project.calculation?.total || 0;
root.innerHTML = `<div class="document-text">${esc(settings.notes)}</div>${[8, 9].includes(form.Index) ? `<h2>공사금액 ${number(total)}원</h2><p>${esc(settings.company)}</p>` : ""}`;
} else if (state.project.tables[state.view]) {
const rows = all
? state.project.tables[state.view].map((row, index) => ({ row, index }))
: EstApp.visibleRows();
const common = ["Code", "Name", "Size", "Unite"];
const columns = {
EstMainDat: [...common, "GN", "TU", "TC", "NC", "GC", "JC", "SC"],
HeadMainDat: [...common, "GN", "NU", "GU", "JU", "SU", "TU", "NC", "GC", "JC", "SC", "TC"],
CostMainDat: ["LineData"],
MachMainDat: [...common, "GN", "TU", "NC", "GC", "JC", "TC"],
Wonga: ["Gubun", "Name", "Youl", "VSusic", "Cost"],
};
root.innerHTML = EstViews.grid(
state.view,
rows,
columns[state.view] ||
EstModel.columns(state.view).filter(
(key) =>
![
"ValueIndex",
"SelectField",
"Ratio",
"NachalRatio",
"Diget",
"DigetIndex",
].includes(key),
),
);
} else root.innerHTML = EstApp.byId("work-content").innerHTML;
root
.querySelectorAll("button,.row-number,.business-tools,select#quantity-sheet")
.forEach((node) => node.remove());
root.querySelectorAll("input,textarea,select").forEach((node) => {
const value =
node.tagName === "SELECT"
? node.selectedOptions[0]?.textContent || ""
: node.type === "number" && node.value !== ""
? number(node.value)
: EstText.clean(EstText.encode(node.value));
if (node.tagName === "TEXTAREA") node.parentElement.classList.add("col-LineData");
if (node.type === "number") node.parentElement.classList.add("numeric");
node.replaceWith(document.createTextNode(value));
});
root
.querySelectorAll("[contenteditable]")
.forEach((node) => node.removeAttribute("contenteditable"));
const title = form?.Name2 || EstApp.titles[state.view] || state.view;
const detail =
output?.detail ||
(state.view === "quantity"
? state.project.quantitySheets?.find((sheet) => sheet.Code === state.quantitySheet)?.Name
: state.view === "progress"
? state.project.progress.find((round) => round.id === state.roundId)?.name
: state.group
? EstNative.groups(state.project.tables[state.view] || []).get(state.group)?.rows[0]
?.Name
: "");
const totals = [
"progress-summary",
"progress-costs",
"escalation-summary",
"escalation-costs",
].includes(form?.Index)
? null
: state.project.calculation;
const errors = [...(totals?.errors || []), ...(output?.errors || [])];
const source = `<!doctype html><html lang="ko"><head><meta charset="UTF-8"><title>${esc(state.project.name)} · ${esc(title)}</title>
<style>@page{size:${settings.paper} ${settings.orientation};margin:${settings.margin}mm}*{box-sizing:border-box}body{font:${settings.font}pt "Malgun Gothic",sans-serif;color:#111;margin:0}
h1{font-size:17pt;margin:0 0 4mm}h2{font-size:12pt;margin:0 0 3mm}.summary{margin:2mm 0 4mm}
table{width:100%;border-collapse:collapse;font-size:inherit}thead{display:table-header-group}tr{break-inside:avoid}
th,td{border:1px solid #777;padding:1.5mm;vertical-align:top;overflow-wrap:anywhere}th{background:#eee}
.heading,.level{font-weight:bold;background:#f4f4f4}.selected{background:transparent}
.numeric{text-align:right;font-variant-numeric:tabular-nums}
.col-LineData{font-family:"굴림체",Consolas,monospace;white-space:pre-wrap;min-width:130mm}
.error-row{outline:1px solid #a00}.errors{color:#900;white-space:pre-wrap}.muted{color:#555}
.document-text{white-space:pre-wrap;line-height:1.7;min-height:40mm}.page{break-after:page}.page:last-child{break-after:auto}.footer{text-align:right;margin-top:3mm}
</style></head><body><h1>${esc(state.project.name)}</h1><h2>${esc(title)}</h2>
${detail ? `<h2>${esc(detail)}</h2>` : ""}${totals ? `<div class="summary">순공사비 ${number(totals.direct?.TC)}원 · 도급액 ${number(totals.total)}원</div>` : ""}
${paginate(root, settings)}${errors.length ? `<p class="errors">계산 오류 ${errors.length}건: ${esc(errors.join(" / "))}</p>` : ""}</body></html>`;
EstApp.byId("print-frame").srcdoc = source;
}
function paginate(root, settings) {
const table = root.querySelector("table");
if (!table || root.querySelectorAll("table").length !== 1) return root.innerHTML;
const rows = [...table.querySelectorAll("tbody>tr")],
pages = [];
for (let at = 0; at < rows.length; at += settings.rowsPerPage) {
const copy = table.cloneNode(false);
copy.innerHTML = table.querySelector("thead")?.outerHTML || "";
const body = document.createElement("tbody");
rows.slice(at, at + settings.rowsPerPage).forEach((row) => body.append(row.cloneNode(true)));
copy.append(body);
pages.push(
`<section class="page">${copy.outerHTML}<div class="footer">${EstApp.esc(settings.company)} ${settings.startPage + pages.length}</div></section>`,
);
}
return pages.join("");
}
async function open() {
const defaults = await EstApp.api("/api/print-catalog");
const original = new Map(
(EstApp.state.project.printCatalog || []).map((row) => [row.Index, row]),
);
catalog = defaults.map((row) => ({
...row,
...original.get(row.Index),
}));
catalog.push(
{ Index: "progress-summary", Name: "기성 내역서", Name2: "기성 내역서" },
{ Index: "progress-costs", Name: "기성 원가계산서", Name2: "기성 원가계산서" },
{ Index: "escalation-summary", Name: "물가변동 내역서", Name2: "물가변동 내역서" },
{ Index: "escalation-costs", Name: "물가변동 원가계산서", Name2: "물가변동 원가계산서" },
{ Index: "annual-summary", Name: "연차 배분표", Name2: "연차 배분표" },
);
const table = EstApp.state.project.tables[EstApp.state.view];
const s = setting(),
{ esc } = EstApp;
EstApp.showDialog(
"인쇄 미리보기",
`<div class="business-tools">
<label>서식 <select id="print-form"><option value="current">현재 화면</option>${catalog
.filter((row) => row.Index !== 151)
.map(
(row) =>
`<option value="${row.Index}" ${String(row.Index) === s.form ? "selected" : ""}>${esc(row.Name)}</option>`,
)
.join("")}</select></label>
<label>용지 <select id="print-paper">${["A4", "A3"].map((v) => `<option ${v === s.paper ? "selected" : ""}>${v}</option>`).join("")}</select></label>
<label>방향 <select id="print-orientation"><option value="landscape" ${s.orientation === "landscape" ? "selected" : ""}>가로</option><option value="portrait" ${s.orientation === "portrait" ? "selected" : ""}>세로</option></select></label>
${[
["margin", "여백(mm)", 0, 50],
["font", "글자(pt)", 6, 20],
["rowsPerPage", "페이지당 행", 1, 100],
["startPage", "시작 쪽", 1, 99999],
]
.map(
([key, label, min, max]) =>
`<label>${label}<input data-print="${key}" type="number" min="${min}" max="${max}" value="${s[key]}" style="width:65px"></label>`,
)
.join("")}
<label>업체명<input data-print="company" value="${esc(s.company)}"></label>
${table ? '<label>출력 범위 <select id="print-scope"><option value="current">현재 호표·검색 결과 전체 행</option><option value="all">현재 자료의 모든 호표·행</option></select></label>' : ""}
<button data-action="print-document">인쇄 / PDF 저장</button><span id="print-error" role="status"></span></div>
<label>문서 본문(표지·갑지·설명서·시방서·심사·공정 문서)<textarea data-print="notes" rows="3" style="width:100%">${esc(s.notes)}</textarea></label>
<iframe id="print-frame" title="인쇄할 문서" style="width:100%;height:65vh;border:1px solid #aaa"></iframe>`,
);
EstApp.byId("dialog-content")
.querySelectorAll("[data-print],#print-paper,#print-orientation,#print-form")
.forEach((node) =>
node.addEventListener("change", () => {
const key = node.dataset.print || node.id.slice(6),
value = node.type === "number" ? Number(node.value) : node.value;
try {
if (
node.type === "number" &&
(!Number.isFinite(value) ||
value < Number(node.min) ||
value > Number(node.max) ||
(["rowsPerPage", "startPage"].includes(key) && !Number.isInteger(value)))
)
throw Error("인쇄 설정 범위 오류");
EstApp.change(() => {
const next = { ...setting(), [key]: value };
if (key === "form") {
const found = catalog.find((row) => String(row.Index) === value);
const original = found && {
...found,
...EstApp.state.project.nativePrintChanges?.[value],
};
if (original) {
next.rowsPerPage = Math.max(
1,
Math.min(100, Number(original.PrintLineCount) || 30),
);
if (Number.isFinite(original.PrintLeft))
next.margin = Math.max(0, Math.min(50, original.PrintLeft));
}
}
if (key === "notes") {
EstApp.state.project.documents ||= {};
EstApp.state.project.documents[next.form] = value;
}
if (
["margin", "rowsPerPage"].includes(key) &&
next.form !== "current" &&
EstApp.state.project.printCatalog?.some((row) => String(row.Index) === next.form)
) {
EstApp.state.project.nativePrintChanges ||= {};
const changes = (EstApp.state.project.nativePrintChanges[next.form] ||= {});
if (key === "margin")
for (const side of ["PrintTop", "PrintBottom", "PrintLeft", "PrintRight"])
changes[side] = value;
else changes.PrintLineCount = value;
}
if (key === "form") next.notes = EstApp.state.project.documents?.[value] || "";
EstApp.state.project.printSettings = next;
}, false);
if (key === "form") {
const content = EstApp.byId("dialog-content"),
current = EstApp.state.project.printSettings;
for (const name of ["notes", "margin", "rowsPerPage"])
content.querySelector(`[data-print=${name}]`).value = current[name];
}
preview(EstApp.byId("print-scope")?.value === "all");
EstApp.byId("print-error").textContent = "";
} catch (error) {
EstApp.byId("print-error").textContent = error.message;
node.value = setting()[key];
}
}),
);
EstApp.byId("print-scope")?.addEventListener("change", (event) =>
preview(event.target.value === "all"),
);
preview();
}
function print() {
const frame = EstApp.byId("print-frame");
if (!frame?.contentDocument?.body) throw Error("인쇄 미리보기가 준비되지 않았습니다.");
frame.contentWindow.focus();
frame.contentWindow.print();
}
return { open, preview, print };
})();