342 lines
16 KiB
JavaScript
342 lines
16 KiB
JavaScript
const EstExtra = (() => {
|
|
function field(section, index, key, value, type = "number") {
|
|
return `<input data-extra="${section}" data-row="${index}" data-field="${key}" type="${type}" step="any" value="${EstApp.esc(value)}" aria-label="${EstApp.esc(key)}">`;
|
|
}
|
|
function button(action, text, extra = "") {
|
|
return `<button data-action="${action}" ${extra}>${text}</button>`;
|
|
}
|
|
function table(headers, rows) {
|
|
return `<table class="sheet"><thead><tr>${headers.map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
|
}
|
|
function box(title, tools, body, note = "") {
|
|
return `<div class="business-tools"><b>${title}</b>${tools}</div>${body}${note ? `<p class="business-note">${note}</p>` : ""}`;
|
|
}
|
|
function render(state) {
|
|
if (state.view === "schedule") {
|
|
EstSchedule.render();
|
|
return;
|
|
}
|
|
const { project, view } = state,
|
|
{ esc, number, byId } = EstApp;
|
|
let html = "";
|
|
if (view === "quantity") {
|
|
if (project.quantitySheets?.length && state.quantitySheet !== "manual") {
|
|
EstQuantity.render(state);
|
|
return;
|
|
}
|
|
const values = Object.create(null),
|
|
rows = project.quantities.map((row, index) => {
|
|
let result,
|
|
error = "";
|
|
try {
|
|
result = EstMath.floating(
|
|
EstExpression.evaluate(row.formula || "0", (key) => values[key]),
|
|
row.digits ?? 2,
|
|
1,
|
|
);
|
|
values[row.Code] = result;
|
|
} catch (e) {
|
|
error = e.message;
|
|
}
|
|
return [
|
|
field("quantities", index, "Code", row.Code, "text"),
|
|
field("quantities", index, "Name", row.Name, "text"),
|
|
field("quantities", index, "TargetCode", row.TargetCode, "text"),
|
|
field("quantities", index, "formula", row.formula, "text"),
|
|
field("quantities", index, "digits", row.digits ?? 2),
|
|
esc(error || number(result)),
|
|
button("delete-extra", "삭제", `data-section="quantities" data-row="${index}"`),
|
|
];
|
|
});
|
|
html =
|
|
EstQuantity.selector(state) +
|
|
box(
|
|
"수량산출서",
|
|
button("add-quantity", "산출 항목 추가") + button("apply-quantity", "내역에 수량 반영"),
|
|
table(["산출코드", "품명", "연결 내역코드", "수식", "소수자리", "결과", ""], rows),
|
|
"앞선 산출 항목을 [산출코드]로 참조합니다. 내역 반영은 연결된 코드의 산출값을 합산하여 수량에 반영합니다.",
|
|
);
|
|
} else if (view === "change") {
|
|
const changes = EstBusiness.changes(project);
|
|
html = box(
|
|
"설계변경",
|
|
button("capture-baseline", "현재 내역을 당초로 확정") +
|
|
button("load-contract", "당초·계약 내역 불러오기") +
|
|
button("recalculate-original", "당초 단가 재계산") +
|
|
button("bid-rates", "낙찰률 적용"),
|
|
table(
|
|
["구분", "코드", "품명", "당초 수량", "변경 수량", "당초 금액", "변경 금액", "증감"],
|
|
changes
|
|
.filter((row) => row.Code || row.Name)
|
|
.map((row) => [
|
|
esc({ new: "신규", deleted: "삭제", changed: "변경", unchanged: "동일" }[row.status]),
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
number(row.originalGN),
|
|
number(row.GN),
|
|
number(row.originalTC),
|
|
number(row.TC),
|
|
number(row.difference),
|
|
]),
|
|
),
|
|
project.baseline
|
|
? `당초: ${esc(project.baseline.name)}. 당초 자료는 별도 사본으로 보존합니다.`
|
|
: changes.some((row) => row.originalSource === "file")
|
|
? "원본 파일에 저장된 당초 수량·금액과 비교합니다."
|
|
: "현재 작업을 저장한 뒤 당초 내역을 확정하세요.",
|
|
);
|
|
} else if (view === "progress") {
|
|
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", "기성 회차 추가");
|
|
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", "회차 삭제") +
|
|
`<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;
|
|
body =
|
|
table(
|
|
[
|
|
"코드",
|
|
"공종",
|
|
"도급수량",
|
|
"전회수량",
|
|
"금회 입력",
|
|
"입력방법",
|
|
"사정 입력",
|
|
"입력방법",
|
|
"금회금액",
|
|
"사정금액",
|
|
"누계수량",
|
|
"누계금액",
|
|
],
|
|
output.rows
|
|
.map((row, index) => ({ row, index }))
|
|
.filter(
|
|
({ row }) =>
|
|
!EstMath.num(row.Level) &&
|
|
(row.Code ||
|
|
row.Name ||
|
|
[54, 55, 57, 58, 62, 67].includes(Number(row.LinkIndex))),
|
|
)
|
|
.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>`;
|
|
return [
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
number(row.GN),
|
|
number(row.JunGN),
|
|
edit("Gum"),
|
|
method("Gum"),
|
|
edit("Sa"),
|
|
method("Sa"),
|
|
number(row.GumTC),
|
|
number(row.SaTC),
|
|
number(row.TotalGN),
|
|
number(row.TotalTC),
|
|
];
|
|
}),
|
|
) +
|
|
`<p class="business-note">${esc((output.errors || []).join(" / "))} 금회 도급액 ${number(output.total)} · 기성비율 ${number(output.ratio)}%</p>`;
|
|
body += table(
|
|
["원가 비목", "도급금액", "전회", "금회", "사정", "누계"],
|
|
(output.costs || []).map((row) => [
|
|
esc(row.Name || row.Gubun),
|
|
number(row.ContractCost),
|
|
number(row.JunCost),
|
|
number(row.GumCost),
|
|
number(row.SaCost),
|
|
number(row.TotalCost),
|
|
]),
|
|
);
|
|
}
|
|
html = box(
|
|
"기성고",
|
|
tools,
|
|
body,
|
|
"전회는 직전 확정 회차 누계를 읽습니다. 금회·사정은 수량·금액·비율 중 하나를 입력해 계산합니다.",
|
|
);
|
|
} else if (view === "escalation") {
|
|
const output = EstBusiness.escalation(project);
|
|
state.business = output;
|
|
const comp = state.escalationComponent || "NU";
|
|
const choices = EstMath.units
|
|
.map(
|
|
(key, i) =>
|
|
`<option value="${key}" ${key === comp ? "selected" : ""}>${["노무", "재료", "경비", "실적"][i]}</option>`,
|
|
)
|
|
.join("");
|
|
const rows = project.tables.EstMainDat.map((row, index) => ({ row, index }))
|
|
.filter(
|
|
({ row }) =>
|
|
(row.Code || row.Name) &&
|
|
!EstMath.num(row.Level) &&
|
|
![54, 55, 58, 59].includes(Number(row.LinkIndex)),
|
|
)
|
|
.map(({ row, index }) => {
|
|
const old =
|
|
project.baseline?.tables.EstMainDat.find(
|
|
(r, i) => EstBusiness.rowKey(r, i) === EstBusiness.rowKey(row, index),
|
|
) || row,
|
|
result = output.rows[index];
|
|
return [
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
EstViews.cell("EstMainDat", index, `A${comp}`, row[`A${comp}`]),
|
|
number(project.baseline ? old[comp] : (row[comp + "_"] ?? row[comp])),
|
|
EstViews.cell("EstMainDat", index, comp, row[comp]),
|
|
number(result[`D${comp}`]),
|
|
number(result[`E${comp}`]),
|
|
number(result[`F${comp}`]),
|
|
EstViews.cell("EstMainDat", index, "EscPlanned", row.EscPlanned || 0),
|
|
EstViews.cell("EstMainDat", index, "EscActual", row.EscActual || 0),
|
|
EstViews.cell("EstMainDat", index, "EscProgress", row.EscProgress || 0),
|
|
number(result.EscExcluded),
|
|
number(result.EscApplied),
|
|
`<input type="checkbox" data-esc-auto="${index}" ${row.EscAuto !== false ? "checked" : ""} aria-label="적용제외 자동계산">`,
|
|
EstViews.cell("EstMainDat", index, "EscManualQuantity", row.EscManualQuantity ?? 0),
|
|
EstViews.cell("EstMainDat", index, "EscConstruction", row.EscConstruction ?? 0),
|
|
EstViews.cell("EstMainDat", index, "EscPS", row.EscPS ?? 0),
|
|
EstViews.cell("EstMainDat", index, "EscBefore", row.EscBefore ?? 0),
|
|
];
|
|
});
|
|
html = box(
|
|
"물가변동 품목조정",
|
|
`<select id="escalation-component">${choices}</select>` +
|
|
button("capture-bid", "현재 단가를 입찰단가로 기록") +
|
|
button("load-bid", "입찰단가 불러오기") +
|
|
button("load-contract", "계약 내역 불러오기") +
|
|
button("load-progress-exclusion", "기성 누계수량 불러오기") +
|
|
button("capture-baseline", "계약 내역 확정") +
|
|
`<label>조사 도급액(0=단가로 계산) <input type="number" step="any" data-option="EscDesignTotal" value="${project.options.EscDesignTotal ?? 0}"></label>`,
|
|
table(
|
|
[
|
|
"코드",
|
|
"품명",
|
|
"입찰 당시",
|
|
"계약 단가",
|
|
"변동 당시",
|
|
"등락률 %",
|
|
"등락폭",
|
|
"조정단가",
|
|
"예정공정수량",
|
|
"실행공정수량",
|
|
"기성수량",
|
|
"적용제외",
|
|
"적용수량",
|
|
"제외 자동",
|
|
"수동 제외수량",
|
|
"시공중 수량(참고)",
|
|
"PS 수량(참고)",
|
|
"선시공 수량(참고)",
|
|
],
|
|
rows,
|
|
),
|
|
`조사 도급액 ${number(output.designTotal)} · 계약 도급액 ${number(output.contractTotal)} · 조정 도급액 ${number(output.adjustedTotal)} · 적용제외(간접비 포함) ${number(output.excludedTotal)} · 적용대가 ${number(output.applied)} · 조정률 ${number(output.rate)}% · 조정금액 ${number(output.adjustment)}. 계약·입찰 파일은 동일 행 순서로 불러옵니다. ${esc(output.errors.join(" / "))}`,
|
|
);
|
|
} else if (view === "annual") {
|
|
const output = EstBusiness.annual(project, false);
|
|
state.business = output;
|
|
html = box(
|
|
"연차공사",
|
|
button("add-annual", "차수 추가"),
|
|
table(
|
|
["차수", "수량·금액 배분 수식", "순공사비", "도급액", "작업", ""],
|
|
project.annual.map((year, index) => [
|
|
field("annual", index, "name", year.name, "text"),
|
|
field("annual", index, "formula", year.formula, "text"),
|
|
number(output.results[index]?.project.calculation.direct.TC),
|
|
number(output.results[index]?.project.calculation.total),
|
|
button("open-annual", "차수 내역 열기", `data-row="${index}"`),
|
|
button("delete-extra", "삭제", `data-section="annual" data-row="${index}"`),
|
|
]),
|
|
),
|
|
`수식 예: [기준]*0.5, [기준]-[1차]. 같은 수식을 수량과 각 성분 금액에 따로 적용하고 금액은 원 미만 절사합니다. ${esc(output.errors.join(" / "))}`,
|
|
);
|
|
} else if (view === "resources") {
|
|
const output = EstResources.calculate(project);
|
|
html = box(
|
|
"동원인원·자재·장비",
|
|
"",
|
|
table(
|
|
["종류", "코드", "품명", "규격", "단위", "소요량 / 환산시간", "금액"],
|
|
output.rows.map((row) => [
|
|
esc({ 5: "자재", 6: "노임", 21: "표준", 101: "견적", 105: "기계" }[row.kind]),
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
esc(row.Size),
|
|
esc(row.Unite),
|
|
number(row.quantity),
|
|
number(row.amount),
|
|
]),
|
|
),
|
|
esc(output.errors.join(" / ")),
|
|
);
|
|
} else if (view === "drawing") {
|
|
const rows = project.tables.CadMainDat || [];
|
|
html = box(
|
|
"원본 도면·문서 자료",
|
|
"",
|
|
table(
|
|
["번호", "코드", "이름", "보존 필드"],
|
|
rows.map((row, index) => [
|
|
index + 1,
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
esc(
|
|
Object.keys(row)
|
|
.filter((key) => row[key]?.binary)
|
|
.join(", "),
|
|
),
|
|
]),
|
|
),
|
|
"가져온 도면 바이너리는 프로젝트에 보존됩니다. VEC0650/CAD 편집·출력의 재현 근거는 현재 자료에 없어 동작을 구현하지 않았습니다.",
|
|
);
|
|
}
|
|
byId("work-content").innerHTML = html;
|
|
}
|
|
return { render };
|
|
})();
|