212 lines
8.6 KiB
JavaScript
212 lines
8.6 KiB
JavaScript
// 단가 목록 도구(spec_pb2): 중복 항목 정리(TfrmStuffc popupSamecheck 0xf61d94), 단가정의 엑셀변환(TfrmSiljuk popupGunguToExcel),
|
|
// 할증·낙찰·복원(TfrmSetDiget 모드 51·52, btnRestor 0x110919c), 바꾸기(barbtnChange → TfrmChangeValue: Main Data 로 이 표만 갱신),
|
|
// SUM(barbtnSum → 항목선택계산 LinkIndex 62 줄, TfrmSetGabBiIndex SUM 모드).
|
|
const EstPriceTools = (() => {
|
|
const num = (v) => Number(v) || 0;
|
|
const key = (s) => String(s ?? "").replace(/[ |]/g, "");
|
|
const TABLE_KIND = { StuffcDat: 5, LaborcDat: 6, SilJukDat: 21, EstiMateDat: 101 };
|
|
|
|
// 같은 품명·규격·단위(빈칸·| 무시) 묶음에서 TC 가장 큰 행 하나만(같으면 뒤 행) 남김. 묻지 않음, 다시 셈 없음.
|
|
function samecheck(rows) {
|
|
const keep = new Map();
|
|
rows.forEach((row, i) => {
|
|
const k = `${key(row.Name)}\u0000${key(row.Size)}\u0000${key(row.Unite)}`,
|
|
prev = keep.get(k);
|
|
if (prev == null || !(num(rows[prev].TC) > num(row.TC))) keep.set(k, i);
|
|
});
|
|
const kept = new Set(keep.values());
|
|
const removed = rows.length - kept.size;
|
|
return { rows: rows.filter((_, i) => kept.has(i)), removed };
|
|
}
|
|
|
|
// 표준시장단가 EXPLEAN 이 있는 행만 「코드 / 번호. 품명 (규격) [단위]」 + 설명 줄
|
|
function gungu(rows) {
|
|
const out = [];
|
|
for (const r of rows) {
|
|
const text = String(r.EXPLEAN ?? "").trim();
|
|
if (!text) continue;
|
|
out.push([
|
|
String(r.Code ?? ""),
|
|
`${r.Index ?? ""}. ${r.Name ?? ""} (${r.Size ?? ""}) [${r.Unite ?? ""}]`,
|
|
]);
|
|
for (const line of text.split(/\r?\n/)) out.push(["", line]);
|
|
out.push(["", ""]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// 할증(Ratio)·낙찰(NachalRatio) 율 쓰기. v = 100 이면 원래값(그 율 칸 100, 자리 해제), 복원 = 0.
|
|
function setRate(rows, indices, field, v) {
|
|
const list = indices.length >= 2 ? indices : rows.map((_, i) => i);
|
|
for (const i of list) {
|
|
const row = rows[i];
|
|
if (!num(row.TC) && !String(row.Name ?? "").trim()) continue;
|
|
if (!num(row.OTC)) row.OTC = row.TC;
|
|
for (const f of [field].flat()) row[f] = v;
|
|
if (v === 100) Object.assign(row, { Diget: 0, DigetIndex: 0 });
|
|
}
|
|
return list.length;
|
|
}
|
|
|
|
// 항목선택계산(62) 줄
|
|
function sumRow(view, row, o) {
|
|
const banned =
|
|
view === "EstMainDat"
|
|
? [
|
|
5, 6, 21, 51, 52, 53, 54, 56, 57, 58, 59, 61, 101, 103, 104, 105, 211, 212, 213, 214,
|
|
215, 217, 218,
|
|
]
|
|
: [5, 6, 21, 51, 52, 53, 61, 101, 103, 104, 105, 211, 212, 213, 214, 215, 217, 218];
|
|
if (
|
|
(view === "EstMainDat" && num(row.Level) > 0) ||
|
|
(view === "HeadMainDat" && num(row.Index) > 0) ||
|
|
(banned.includes(num(row.LinkIndex)) &&
|
|
(String(row.Name ?? "").trim() || String(row.Size ?? "").trim()))
|
|
)
|
|
throw Error("현재 라인에는 합계(SUM)를 할수가 없습니다.");
|
|
if ((o.base === 0) !== (o.target === 0)) throw Error("비목별항목은 비목별적용만 가능합니다.");
|
|
if (!o.picked.length) throw Error("더할항목의 색인들을 선택하세요");
|
|
if (o.picked.length > 5) throw Error("더할항목의 색인들은 6개이상 선택할수없습니다");
|
|
Object.assign(row, {
|
|
Name: o.name,
|
|
Size: o.size,
|
|
LinkIndex: 62,
|
|
Code: `A${o.picked.map((n) => String(n).padStart(2, "0")).join("")}`,
|
|
GN: o.rate,
|
|
Biul: o.rate / 100,
|
|
Unite: "%",
|
|
OCheck: o.base,
|
|
SCheck: o.target,
|
|
});
|
|
}
|
|
function nextSKind(rows, at, view) {
|
|
let start = at,
|
|
end = at;
|
|
const head = (r) => (view === "EstMainDat" ? num(r.Level) > 0 : num(r.Index) > 0);
|
|
while (start > 0 && !head(rows[start - 1])) start--;
|
|
while (end + 1 < rows.length && !head(rows[end + 1])) end++;
|
|
return Math.max(0, ...rows.slice(start, end + 1).map((r) => num(r.SKind))) + 1;
|
|
}
|
|
|
|
async function run(name, state) {
|
|
const view = state.view,
|
|
rows = state.project.tables[view],
|
|
picked = [...state.selection].filter((i) => rows?.[i]).sort((a, b) => a - b);
|
|
if (name === "price-samecheck") {
|
|
if (view !== "StuffcDat") throw Error("자재단가 화면에서 쓰세요.");
|
|
let removed = 0;
|
|
EstApp.change((project) => {
|
|
const out = samecheck(project.tables.StuffcDat);
|
|
project.tables.StuffcDat = out.rows;
|
|
removed = out.removed;
|
|
});
|
|
state.notice = `중복 항목 ${removed}개를 지웠습니다.`;
|
|
} else if (name === "price-gungu-excel") {
|
|
const list = gungu(state.project.tables.SilJukDat || []);
|
|
if (!list.length) throw Error("단가정의(설명)가 있는 표준시장단가가 없습니다.");
|
|
const response = await fetch("/api/export-report-xlsx", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
name: "단가정의",
|
|
columns: ["코드", "단가정의"],
|
|
rows: list,
|
|
errors: [],
|
|
}),
|
|
});
|
|
if (!response.ok) throw Error((await response.json()).error);
|
|
EstApp.download(
|
|
`${state.project.name || "공사"}_단가정의.xlsx`,
|
|
await response.blob(),
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
);
|
|
} else if (name === "price-ratio" || name === "price-nachal" || name === "price-restore") {
|
|
if (!TABLE_KIND[view]) throw Error("자재·노임·표준시장·견적 단가 화면에서 쓰세요.");
|
|
// 복원(원본은 할증·낙찰 창 안 단추): 두 율 모두 0
|
|
const field =
|
|
name === "price-nachal"
|
|
? "NachalRatio"
|
|
: name === "price-ratio"
|
|
? "Ratio"
|
|
: ["Ratio", "NachalRatio"];
|
|
let v = 0;
|
|
if (name !== "price-restore") {
|
|
const input = prompt(
|
|
name === "price-nachal" ? "낙찰율을 입력하세요(%)" : "할증율을 입력하세요(%)",
|
|
"",
|
|
);
|
|
if (input == null) return;
|
|
v = Number(input);
|
|
if (!Number.isFinite(v)) throw Error("율은 수치로 입력하세요.");
|
|
}
|
|
if (
|
|
!confirm(
|
|
v === 100 || v === 0 ? "원래값으로 복원하시겠습니까?" : "설정값을 적용하시겠습니까?",
|
|
)
|
|
)
|
|
return;
|
|
let n = 0;
|
|
EstApp.change((project) => (n = setRate(project.tables[view], picked, field, v)));
|
|
state.notice = `${n}개 행에 적용했습니다.`;
|
|
} else if (name === "price-change") {
|
|
const kind = TABLE_KIND[view];
|
|
if (!kind) throw Error("자재·노임·표준시장·견적 단가 화면에서 쓰세요.");
|
|
if (
|
|
!confirm(
|
|
"선택한 Main데이타의 단가로 바꾸시겠습니까?\n바꾸기후 전체재계산을 해야 금액이 바뀝니다.",
|
|
)
|
|
)
|
|
return;
|
|
const source = await EstApp.api("/api/main-data");
|
|
let missing = 0;
|
|
EstApp.change((project) => {
|
|
missing = EstDataUpdate.table(project.tables[view], source.tables[view] || [], kind);
|
|
});
|
|
state.notice = missing
|
|
? `${missing} 개의 찾지못한 데이타가 있습니다. 확인란에 체크된 데이타를 검토하세요.`
|
|
: "Main 데이타의 단가로 바꿨습니다.";
|
|
} else if (name === "price-sum") {
|
|
if (!["EstMainDat", "HeadMainDat"].includes(view) || !picked.length)
|
|
throw Error("내역서·일위대가에서 SUM 줄로 바꿀 행을 고르세요.");
|
|
const row = rows[picked[0]];
|
|
const letters = prompt("더할 색인(A~Z, 최대 5개, 예 AC)", "")?.toUpperCase();
|
|
if (letters == null) return;
|
|
const rate = Number(prompt("적용율(%)", "100"));
|
|
const o = {
|
|
name: prompt("명칭", row.Name || "잡재료비") ?? "",
|
|
size: row.Size ?? "",
|
|
rate: Number.isFinite(rate) ? rate : 0,
|
|
base: num(row.OCheck),
|
|
target: num(row.SCheck),
|
|
picked: [...new Set(letters.replace(/[^A-Z]/g, ""))].map((c) => c.charCodeAt(0) - 64),
|
|
};
|
|
sumRow(view, structuredClone(row), o);
|
|
EstApp.change((project) => {
|
|
const list = project.tables[view];
|
|
sumRow(view, list[picked[0]], o);
|
|
list[picked[0]].SKind = nextSKind(list, picked[0], view);
|
|
});
|
|
} else return false;
|
|
EstApp.render();
|
|
return true;
|
|
}
|
|
const names = new Set([
|
|
"price-samecheck",
|
|
"price-gungu-excel",
|
|
"price-ratio",
|
|
"price-nachal",
|
|
"price-restore",
|
|
"price-change",
|
|
"price-sum",
|
|
]);
|
|
function handle(name, state) {
|
|
if (!names.has(name)) return false;
|
|
run(name, state).catch((error) => {
|
|
state.notice = error.message;
|
|
EstApp.render();
|
|
});
|
|
return true;
|
|
}
|
|
return { samecheck, gungu, setRate, sumRow, handle };
|
|
})();
|