- 새 M02_San_MatPick: 종 열쇠(이름 · 규격 · 단위 · 단서) · 고름 자리 = 개인 층 실무자료/재료고름/<파일 지문>.json([끝내기] 뒤 남음 · 같은 파일 다시 올리면 그대로) · 판(블록마다 재료찾기 판정 → 종 · 첫 판 한 번 셈 → 산근 저장본 칸) · 만들 때 얹기 · 이미 만든 SK 얹기(가져온 그대로인 줄만 · 바꿈 · 따로 고른 줄 그대로 · 풀면 가져온 그대로)
- 만들기: from-hopyo 가 그 파일 고름째 만듦(저장 안 한 판 고름 = 만들 때 저장) · 원본 지문 · 고른 줄 = 이음 + 재료찾기.고름 · 문서 셈 우리 값(엑셀 값 기억 없음) · 별도계상 = 품목만(줄 이음 · 단가 0 그대로)
- 문서 머리 가져온.재료(판정 · 후보 · 단서 · 처음 연결) · 가져온.지문
- 길: GET/POST hopyo-materials/{HP}(판 · [저장] · 만든 것 적용은 물음 예일 때만) · GET/POST material-picks/{지문}(SK 상세 물음)
- 화면: 단가산출 목록 위 「고를 재료」 판(후보 고름 · 찾기 · 고름 풀기 · n 블록에 적용 · 이어진 재료 접힘 · [저장]) · 저장 안 한 고름 = 떠날 때 물음 · 줄 쌓기 재료 줄 판정 배지 · 풍선말(판정 · 단서 · 후보) · 이름 단추 · [이음 고르기] 뒤 「같은 파일 n 줄」 물음
- 별도계상 설명 = 「별도계상 · 품목: 이름 규격」(엔진 이은 품목은 자재표 이름) — test_827_2 기대 글 고침(품목 이음 → 품목 이름)
- 시험 test_827_3 셋(파일 고름 블록 둘 · 다른 파일 0 · 별칭 · 마스터 그대로 · 풀기 · 다시 올림 유지 · 물음 예일 때만 · 따로 바꾼 줄 보존 · 별도계상 품목 + 단가 0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
211 lines
9.7 KiB
TypeScript
211 lines
9.7 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Hopyo_ImportedPanel.ts
|
|
* 가져온 식 단가산출 모달 머리 판(PLAN 793-3b · 요구 646 · 647 · 652) — `Hopyo_Imported` 가 엶 · 시트(`Sangeun_Detail`) 위.
|
|
* · 줄 1 = 「새」 · 원본(파일 · 시트 · 행) · 검산 표지(같음 / 다름 n · 줄마다 까닭) · 단가 기준 [우리 단가로] / [엑셀 단가로] ·
|
|
* 바꾸기 전후 금액 · [원본 글 보기]
|
|
* · 줄 2 = 이음 없는 자원 = 표지 + [이음 고르기]
|
|
* · 줄 2-2 = 소요량 · 계수 이음(797-2 · 후보 집계 + [이음 보기] → `Hopyo_QtyLinks` 모달)
|
|
* · 줄 2-3 = 못 이은 기계 → 내 기계(821-4) — 「내 기계 생성 n」 · 줄마다 「내 기계」 꼬리표 · 개인 층 밖 = 「만들어질 내 기계 n」
|
|
* · 경쟁사 로직 그대로 — 기초로 바꾸기 · 근거 기초 제안 없음(809-1 · 요구 703)
|
|
* 부품 = 단추 · 배지 · 글 · 입력 · 띠 · 표 · 모달(날 단추 · 입력 없음) · 서버 길은 부르는 쪽.
|
|
* ========================================================================== */
|
|
|
|
import { createBadge } from "@ui/ui_template_badge";
|
|
import { createButton } from "@ui/ui_template_button";
|
|
import { el } from "@ui/ui_template_elements";
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import { createTable } from "@ui/ui_template_table";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { createToolbar } from "@ui/ui_template_toolbar";
|
|
import { formatMoney } from "../common_util/common_util_money";
|
|
import { tallyBadges, type QtyTally } from "./M02_MasterTemplete_Hopyo_QtyLinks";
|
|
import {
|
|
gapText,
|
|
isImported,
|
|
wrongLines,
|
|
type ImportedDoc,
|
|
type Money4,
|
|
} from "./logic/M02_Hopyo_Imported_Model";
|
|
|
|
const TEXT = {
|
|
Imported: ["가져온 식", "Imported formula"],
|
|
Same: ["검산 같음", "Check matches"],
|
|
Diff: ["검산 다름 {n}", "Check differs {n}"],
|
|
NoCheck: ["검산 없음", "No check"],
|
|
Basis: ["단가 기준", "Price basis"],
|
|
ToOurs: ["우리 단가로", "Use our prices"],
|
|
ToExcel: ["엑셀 단가로", "Use Excel prices"],
|
|
Source: ["원본 글 보기", "Show source text"],
|
|
SourceTitle: ["원본 글", "Source text"],
|
|
Row: ["행", "Row"],
|
|
Text: ["글", "Text"],
|
|
Excel: ["엑셀", "Excel"],
|
|
Now: ["지금", "Now"],
|
|
Unlinked: ["이음 없는 자원 {n}", "Unlinked resources {n}"],
|
|
Quote: ["견적 {n}", "Quotes {n}"],
|
|
Link: ["이음 고르기", "Pick link"],
|
|
Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"],
|
|
QtyLinks: ["소요량 · 계수 이음 {n}", "Quantity / coefficient links {n}"],
|
|
QtyOpen: ["이음 보기", "Show links"],
|
|
MyMade: ["내 기계 생성 {n}", "My machines created {n}"],
|
|
MyUsed: ["내 기계 이음 {n}", "Linked to my machines {n}"],
|
|
MyPlan: ["만들어질 내 기계 {n}", "My machines to create {n}"],
|
|
MyTag: ["내 기계", "My machine"],
|
|
MyNew: ["이번에 만듦 · 값 확인 필요", "Created now · check values"],
|
|
MyOld: ["있던 내 기계에 이음", "Linked to an existing one"],
|
|
Close: ["닫기", "Close"],
|
|
} as const satisfies Record<string, readonly [string, string]>;
|
|
export const tx = (k: keyof typeof TEXT): string =>
|
|
TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0];
|
|
|
|
export interface PanelState {
|
|
doc: ImportedDoc;
|
|
/** 이음 없는 자원(서버 price 답) */
|
|
unlinked: { 자리: string; 이름: string; 까닭?: string; 견적?: boolean; 종?: string }[];
|
|
/** 소요량 · 계수 이음 집계(서버 qty 답 · 797-2) */
|
|
qty: QtyTally | null;
|
|
/** 마지막 단가 바꿈 전후 */
|
|
price: { 전: Money4 | null; 후: Money4 | null } | null;
|
|
busy: boolean;
|
|
readOnly: boolean;
|
|
}
|
|
|
|
export interface PanelActs {
|
|
price: (basis: "엑셀" | "우리") => void;
|
|
link: (spot: string, name: string) => void;
|
|
qty: () => void;
|
|
}
|
|
|
|
const won = (m: Money4 | null | undefined): string => (m ? formatMoney(m.계) : "—");
|
|
const muted = (s: string): HTMLElement => createText(s, "muted", 14);
|
|
|
|
/** [원본 글 보기] — 엑셀 글줄 · 엑셀 값 · 지금 셈 */
|
|
function showSource(doc: ImportedDoc): void {
|
|
const src = doc.가져온?.원본;
|
|
const lines = new Map((doc.가져온?.검산?.줄 ?? []).map((x) => [x.행, x]));
|
|
const rows = (src?.글줄 ?? []).map((g) => {
|
|
const hit = g.행 != null ? lines.get(g.행) : undefined;
|
|
return [
|
|
String(g.행 ?? ""),
|
|
g.글 ?? "",
|
|
g.값 == null ? "" : formatMoney(g.값),
|
|
hit?.셈 == null ? "" : formatMoney(hit.셈),
|
|
hit ? createBadge(hit.같음 ? "=" : (hit.까닭 ?? "≠"), hit.같음 ? "success" : "danger") : "",
|
|
];
|
|
});
|
|
openModal({
|
|
title: `${tx("SourceTitle")} — ${[src?.파일, src?.시트].filter(Boolean).join(" · ")}`,
|
|
closeLabel: tx("Close"),
|
|
wide: true,
|
|
dialogClass: "m02-modal m02-hopyo__src",
|
|
mount: (body) => {
|
|
const heads = [tx("Row"), tx("Text"), tx("Excel"), tx("Now"), ""];
|
|
const t = createTable(heads.map((h, i) => ({ label: h, ...(i > 1 && i < 4 ? { align: "end" as const } : {}) })), rows, { density: "data" }); // prettier-ignore
|
|
t.dataset.importedSource = "";
|
|
body.append(t);
|
|
},
|
|
});
|
|
}
|
|
|
|
function checkBadge(doc: ImportedDoc): HTMLElement {
|
|
const chk = doc.가져온?.검산;
|
|
const bad = wrongLines(doc);
|
|
const b = !chk
|
|
? createBadge(tx("NoCheck"), "neutral")
|
|
: chk.같음
|
|
? createBadge(tx("Same"), "success")
|
|
: createBadge(tx("Diff").replace("{n}", String(bad.length || 1)), "danger");
|
|
b.dataset.importedCheck = chk ? String(chk.같음) : "";
|
|
b.title = [chk?.까닭, ...bad.map((x) => `${tx("Row")} ${x.행 ?? "?"}: ${x.까닭 ?? ""}`)]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
return b;
|
|
}
|
|
|
|
/** 못 이은 기계 → 내 기계(821-4) — 생성 수 · 이은 수 · 줄마다 꼬리표 · 예정 수 */
|
|
function myMachines(src: NonNullable<ImportedDoc["가져온"]>["원본"]): HTMLElement | null {
|
|
const got = src?.내기계 ?? [];
|
|
const plan = src?.내기계예정 ?? [];
|
|
if (!got.length && !plan.length) return null;
|
|
const made = got.filter((m) => m.새로).length;
|
|
const items: HTMLElement[] = [];
|
|
if (made) items.push(createBadge(tx("MyMade").replace("{n}", String(made)), "info"));
|
|
if (got.length > made) items.push(createBadge(tx("MyUsed").replace("{n}", String(got.length - made)), "neutral")); // prettier-ignore
|
|
for (const m of got) {
|
|
const b = createBadge(
|
|
`${tx("MyTag")} · ${[m.이름, m.규격].filter(Boolean).join(" ")}`,
|
|
"accent",
|
|
);
|
|
b.title = `${m.키} · ${m.새로 ? tx("MyNew") : tx("MyOld")}`;
|
|
b.dataset.importedMine = m.키;
|
|
items.push(b);
|
|
}
|
|
if (plan.length) {
|
|
const b = createBadge(tx("MyPlan").replace("{n}", String(plan.length)), "warning");
|
|
b.title = plan.map((m) => [m.이름, m.규격].filter(Boolean).join(" ")).join("\n");
|
|
items.push(b);
|
|
}
|
|
const bar = createToolbar({ items });
|
|
bar.dataset.importedMineBar = String(made);
|
|
return bar;
|
|
}
|
|
|
|
/** 머리 판 */
|
|
export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
|
|
const meta = s.doc.가져온 ?? {};
|
|
const src = meta.원본;
|
|
const here = isImported(s.doc);
|
|
const head: (HTMLElement | "divider")[] = [
|
|
createBadge(tx("Imported"), "accent"),
|
|
muted([src?.파일, src?.시트, src?.행?.join("~")].filter(Boolean).join(" · ")),
|
|
checkBadge(s.doc),
|
|
];
|
|
if (here) {
|
|
const ours = meta.단가기준 === "우리";
|
|
const flip = createButton({
|
|
label: ours ? tx("ToExcel") : tx("ToOurs"),
|
|
size: 28,
|
|
variant: "ghost",
|
|
disabled: s.readOnly || s.busy,
|
|
onClick: () => a.price(ours ? "엑셀" : "우리"),
|
|
});
|
|
flip.dataset.importedPrice = ours ? "엑셀" : "우리";
|
|
head.push("divider", muted(`${tx("Basis")} ${meta.단가기준 ?? "엑셀"}`), flip);
|
|
if (s.price) {
|
|
const g = tx("Gap").replace("{a}", won(s.price.전)).replace("{b}", won(s.price.후));
|
|
const t = muted(g.replace("{d}", gapText(s.price.전, s.price.후)));
|
|
t.dataset.importedGap = "";
|
|
head.push(t);
|
|
}
|
|
}
|
|
const srcBtn = createButton({ label: tx("Source"), size: 28, variant: "ghost", disabled: !src?.글줄?.length, onClick: () => showSource(s.doc) }); // prettier-ignore
|
|
srcBtn.dataset.importedSourceBtn = "";
|
|
head.push("divider", srcBtn);
|
|
const kids: HTMLElement[] = [createToolbar({ items: head })];
|
|
if (here && s.unlinked.length) {
|
|
const items = s.unlinked.map((u) => {
|
|
const label = `${u.이름}${u.까닭 ? ` (${u.까닭})` : ""} · ${tx("Link")}`; // 못 이은 까닭(814-1)
|
|
const b = createButton({ label, size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: () => a.link(u.자리, u.이름) }); // prettier-ignore
|
|
b.dataset.importedLink = u.자리;
|
|
return b;
|
|
});
|
|
const quotes = s.unlinked.filter((u) => u.견적).length; // 815-1 견적 = 그대로 쓰는 값 · 못 이음 수에서 뺌
|
|
const left = s.unlinked.length - quotes;
|
|
const tags = [
|
|
...(left ? [createBadge(tx("Unlinked").replace("{n}", String(left)), "warning")] : []),
|
|
...(quotes ? [createBadge(tx("Quote").replace("{n}", String(quotes)), "info")] : []),
|
|
];
|
|
kids.push(createToolbar({ items: [...tags, ...items] }));
|
|
}
|
|
const mine = myMachines(src);
|
|
if (here && mine) kids.push(mine);
|
|
if (here && s.qty?.자리) {
|
|
const open = createButton({ label: tx("QtyOpen"), size: 24, variant: "ghost", onClick: a.qty });
|
|
open.dataset.importedQty = String(s.qty.자리);
|
|
const tag = createBadge(tx("QtyLinks").replace("{n}", String(s.qty.자리)), "info");
|
|
kids.push(createToolbar({ items: [tag, ...tallyBadges(s.qty), open] }));
|
|
}
|
|
return el("div", { className: "m02-std-sheet__body", attrs: { "data-imported-panel": "" }, children: kids }); // prettier-ignore
|
|
}
|