/* ============================================================================= * M02_MasterTemplete_Hopyo_Wizard.ts * 일위대가 불러오기 = 한시 마법사(PLAN 718-1 · 요구서 내역12 4장 22) — 본 칸 한 화면 · 걸음 셋: * ① 결과 층(쓸 수 있는 층만 · 기본 개인) · 결과(일위대가 늘 · 단가산출 체크) · 엑셀 고르기 → 올림 * (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 서버 409 안내) · 작업 중 파일 이어서 * ② 보기 · 확인 · 만들기 = 엑셀 보기 창(`Hopyo_Sheet` — [모두 고정으로 들여오기] · 호표 [수정] → * 확인 창 만들기 · [구조물 조합으로] 그대로) · 만들 층 = 고른 층 · * 단가산출 체크면 단가산출 칸(`Hopyo_WizardSk` — 기초 · SK · 고름 대기 기초 고름 · 729-3) · * 안 이은 줄 띠(`Hopyo_Left` — 안 만든 호표의 남은 줄 · [고르기] = 줄마다 이을 것 · 729-3) * ③ [끝내기] = 남은 줄 다시 셈 → 있으면 막음(띠 · 남은 수 · 표) · 없으면 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 · * 만든 일위대가 · 단가산출 · 별칭 장부 남음 · 지문 기록 * 작업 중 = 서버에 보관(떠났다 와도 ① 목록에서 이어서) · 옛 HP(지문 없음)는 서버 목록에서 숨김. * ========================================================================== */ import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { createCheckbox } from "@ui/ui_template_checkbox"; import { createEmpty } from "@ui/ui_template_empty"; import { createPanelHead } from "@ui/ui_template_panel_head"; import { createSegment } from "@ui/ui_template_segment"; import { createText } from "@ui/ui_template_text"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { deleteHopyo, listHopyo, uploadHopyo, type HopyoFile, type HopyoScope, } from "./M02_MasterTemplete_Hopyo_Api"; import { createBanner } from "@ui/ui_template_banner"; import { t as L } from "@ui/ui_template_locale"; import { readHopyo } from "./M02_MasterTemplete_Hopyo_Api"; import { hasDraft, draftKey } from "./M02_MasterTemplete_Hopyo_Check"; import { countLeft, leftCount, openLeft, type LeftMap, type LeftScope, } from "./M02_MasterTemplete_Hopyo_Left"; import { sangeunStep } from "./M02_MasterTemplete_Hopyo_WizardSk"; import { accessFor, type ManageLayer } from "./M02_MasterTemplete_Layer"; const LAYERS: { layer: ManageLayer; text: string }[] = [ { layer: "personal", text: "개인" }, { layer: "company", text: "회사" }, { layer: "system", text: "마스터" }, ]; const STEPS = [ { value: "1", label: "① 층 · 엑셀" }, { value: "2", label: "② 보기 · 확인 · 만들기" }, { value: "3", label: "③ 끝내기" }, ]; type Got = HopyoFile & { 이어서?: boolean }; const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); const textOf = (layer: string): string => LAYERS.find((x) => x.layer === layer)?.text ?? layer; /** 쓸 수 있는 자기 층(개인 · 회사 · 마스터 차례) */ export async function writableLayers(): Promise { const user = await fetchSessionUser().catch(() => null); return LAYERS.map((x) => x.layer).filter((layer) => accessFor(layer, null, user).canWrite); } export function buildHopyoWizard(): HTMLElement { const steps = createSegment({ options: STEPS.map((s) => ({ ...s, disabled: true })), value: "1", }); steps.root.dataset.hopyoSteps = ""; const body = el("div", { className: "m02-hopyo" }); const root = el("div", { className: "m02-hopyo", attrs: { "data-hopyo-wizard": "" }, children: [ createText( "한시 — 다른 프로그램 일위대가 엑셀을 우리 일위대가로 옮김 · 끝내면 올린 파일 지움 · 별칭 장부는 남음", "hint", ), el("div", { className: "ui-toolbar", children: [steps.root] }), body, ], }); let layers: ManageLayer[] = []; let pick: ManageLayer = "personal"; let wantSangeun = false; // 결과 「단가산출」 체크 const scopeOf = (layer: ManageLayer): HopyoScope => ({ layer, owner: null }); async function first(): Promise { steps.set("1"); layers = await writableLayers(); if (!layers.includes(pick)) pick = layers[0] ?? "personal"; const input = el("input", { attrs: { type: "file", accept: ".xls,.xlsx", "data-hopyo-file": "" }, }) as HTMLInputElement; input.hidden = true; const upload = createButton({ label: "엑셀 고르기", variant: "filled", disabled: !layers.length, onClick: () => input.click(), }); upload.title = layers.length ? "설계내역서 엑셀(일위대가 시트)" : "쓸 수 있는 층 없음"; input.addEventListener("change", () => { const file = input.files?.[0]; input.value = ""; if (!file) return; upload.disabled = true; uploadHopyo(scopeOf(pick), file) .then((got: Got) => { if (got.이어서) showToast(`같은 엑셀 작업 중 — ${got.키} 이어서`, "info"); else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success"); second(pick, got); }) .catch((error) => showToast(why(error), "error")) .finally(() => (upload.disabled = !layers.length)); }); const seg = createSegment({ options: LAYERS.map((x) => ({ value: x.layer, label: x.text, disabled: !layers.includes(x.layer), })), value: pick, onChange: (v) => (pick = v as ManageLayer), }); seg.root.dataset.hopyoLayer = ""; const ilwi = createCheckbox("일위대가(고정 줄)", { checked: true, disabled: true }); const sangeun = createCheckbox(L("M02_WizSkCheck"), { checked: wantSangeun, onChange: (on) => (wantSangeun = on), }); sangeun.root.dataset.hopyoSangeun = ""; const busy = el("div", { className: "m02-hopyo" }); body.replaceChildren( createText("결과 층", "muted", 14), el("div", { className: "ui-toolbar", children: [seg.root] }), createText("결과", "muted", 14), el("div", { className: "ui-toolbar", children: [ilwi.root, sangeun.root] }), el("div", { className: "ui-toolbar", children: [upload, input] }), createPanelHead({ title: "작업 중", level: 4, divider: true }).root, busy, ); const rows = ( await Promise.all( layers.map((layer) => listHopyo(scopeOf(layer)) .then((list) => list.map((f) => ({ layer, f }))) .catch(() => []), ), ) ).flat(); busy.replaceChildren( ...(rows.length ? rows.map(({ layer, f }) => { const row = createButton({ label: `${f.파일} · 호표 ${f.호표수} · ${textOf(layer)} — 이어서`, variant: "ghost", onClick: () => second(layer, f), }); row.dataset.hopyoKey = f.키; return row; }) : [createEmpty({ text: "작업 중인 불러오기 없음", small: true })]), ); } function second(layer: ManageLayer, file: HopyoFile): void { steps.set("2"); const scope = scopeOf(layer); const view = createButton({ label: "엑셀 보기 · 확인 · 만들기", variant: "filled", onClick: () => void import("./M02_MasterTemplete_Hopyo_Sheet") .then((m) => m.openHopyoSheet({ scope, key: file.키, 파일: file.파일, target: scope, canWrite: true, onDeleted: () => void first(), }), ) .catch((error: unknown) => showToast(why(error), "error")), }); const finish = createButton({ label: "끝내기", variant: "ghost", onClick: () => void end() }); finish.dataset.hopyoFinish = ""; const at: LeftScope = { scope, target: scope, key: file.키 }; const band = el("div", { attrs: { "data-hopyo-left": "" } }); let nos: number[] = []; let map: LeftMap | null = null; const pick = createButton({ label: L("M02_LeftOpen"), variant: "ghost", size: 28, onClick: () => openLeft({ ...at, 파일: file.파일, nos, ...(map ? { map } : {}) }, paint), }); pick.dataset.hopyoLeftOpen = ""; /** 띠 — 남은 줄 있으면 [끝내기] 막음 · 까닭 · 남은 수 */ function paint(got: LeftMap | null): void { map = got; const n = got ? leftCount(got) : null; finish.disabled = !!n?.줄; finish.title = n?.줄 ? L("M02_LeftBlocked") : ""; band.dataset.left = n ? String(n.줄) : ""; band.replaceChildren( createBanner({ tone: !n ? "info" : n.줄 ? "warning" : "success", text: !n ? L("M02_LeftUnknown") : n.줄 ? `${L("M02_LeftBand").replace("{n}", String(n.줄)).replace("{h}", String(n.호표))} · ${L("M02_LeftBlocked")}` : L("M02_LeftZero"), actions: [pick], }), ); } paint(null); async function recount(): Promise { if (!nos.length) nos = (await readHopyo(scope, file.키)).호표.map((h) => h.번); const got = await countLeft(at, nos, (i, all) => band.replaceChildren(createText(`${L("M02_LeftCounting")} ${i + 1} / ${all}`, "muted", 14)), ); paint(got); return got; } const back = createButton({ label: "처음으로", variant: "link", onClick: () => void first() }); back.title = "작업은 그대로 보관 — 처음 화면 「작업 중」에서 이어서"; const wrong = file.어긋남 ? ` · 검산 어긋남 ${file.어긋남}` : ""; body.replaceChildren( createText(`${file.파일} · ${textOf(layer)} 층 · 호표 ${file.호표수}${wrong}`, "muted", 14), ...(wantSangeun ? [ createPanelHead({ title: L("M02_WizSkHead"), level: 4, divider: true }).root, sangeunStep(scope, file.키).root, ] : []), createPanelHead({ title: L("M02_WizIlwiHead"), level: 4, divider: true }).root, createText( "엑셀 보기 창에서 [모두 고정으로 들여오기] 또는 호표 [수정] → 확인하고 만들기 · [구조물 조합으로] · 만든 일위대가 · 단가산출은 고른 층에 들어감", "hint", ), band, el("div", { className: "ui-toolbar", children: [view, finish, back] }), ); async function end(): Promise { steps.set("3"); finish.disabled = true; let got: LeftMap; try { got = await recount(); } catch (error) { showToast(why(error), "error"); finish.disabled = false; return steps.set("2"); } if (leftCount(got).줄) { showToast(L("M02_LeftBlocked"), "warning"); openLeft({ ...at, 파일: file.파일, nos, map: got }, paint); return steps.set("2"); } const loose = [...got.values()].filter( (x) => !x.one.호표.만든?.확인 && hasDraft(draftKey(scope, file.키, x.one.호표.번)), ).length; const ok = await showConfirmDialog( `「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출과 별칭 장부는 남음 · 하나라도 만들었으면 같은 엑셀은 다시 올리지 못함` + (loose ? ` · ${L("M02_LeftLoose").replace("{n}", String(loose))}` : ""), "끝내기", ); if (!ok) return steps.set("2"); try { const got = (await deleteHopyo(scope, file.키)) as { 만든?: number }; showToast(`끝냄 — 만든 일위대가 · 단가산출 ${got.만든 ?? 0} · 올린 파일 지움`, "success"); } catch (error) { showToast(why(error), "error"); return steps.set("2"); } await first(); } } void first(); return root; }