Files
Aislo/B09_Estimation/B09_Estimation_UI_Tabs_Base_Quote.ts
T

101 lines
4.0 KiB
TypeScript

/* =============================================================================
* B09_Estimation_UI_Tabs_Base_Quote.ts
* 자재단가대비표 「견적서」 칸 작은 모달(PLAN 166-4) — 자재 줄 하나의 견적서 여러 파일 ·
* 목록 · 올리기(여러 개) · 내려받기 · 지우기. 파일은 올리는 즉시 서버에 둠([저장]과 따로).
*
* 길 = `B09_Estimation_Router_Quotes.py` · 바뀐 목록은 onChange(이름들)로 표 칸 「n 파일」에 반영
* ========================================================================== */
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
import { openModal } from "@ui/ui_template_modal";
import { createButton, el, showToast } from "@ui/ui_template_elements";
interface QuoteFile {
name: string;
size: number;
at: string;
}
const url = (key: string, name?: string): string => {
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
const q = new URLSearchParams({ key, ...(name ? { name } : {}) });
return `${API_BASE_URL}/projects/${projectId}/estimation/quotes${name ? "/file" : ""}?${q}`;
};
async function call(input: string, init: RequestInit = {}): Promise<Response> {
const response = await fetch(input, { credentials: "include", ...init });
if (!response.ok) {
const body = (await response.json().catch(() => null)) as { detail?: unknown } | null;
throw new Error(typeof body?.detail === "string" ? body.detail : `HTTP ${response.status}`);
}
return response;
}
async function download(key: string, name: string): Promise<void> {
const blob = await (await call(url(key, name))).blob();
const a = el("a", { attrs: { href: URL.createObjectURL(blob), download: name } });
a.click();
URL.revokeObjectURL(a.href);
}
/** 견적서 모달 — label = 「품명 규격」 */
export function openQuotes(key: string, label: string, onChange: (names: string[]) => void): void {
openModal({
title: `견적서 — ${label}`,
closeLabel: "닫기",
mount: (body) => {
const list = el("ul", { className: "b09-quotes" });
const show = (files: QuoteFile[]): void => {
onChange(files.map((f) => f.name));
list.replaceChildren(
...(files.length
? files.map((f) =>
el("li", {
children: [
createButton({
label: `${f.name} (${Math.ceil(f.size / 1024).toLocaleString()} KB)`,
variant: "ghost",
size: "sm",
onClick: () => void download(key, f.name).catch(fail),
}),
createButton({
label: "지우기",
variant: "danger",
size: "sm",
onClick: () =>
void call(url(key, f.name).replace("/file?", "?"), { method: "DELETE" })
.then((r) => r.json())
.then(show, fail),
}),
],
}),
)
: [el("li", { text: "올린 견적서 없음" })]),
);
};
const fail = (error: unknown): void =>
showToast(`하지 못했습니다 — ${String(error)}`, "error");
const input = el("input", { attrs: { type: "file", multiple: "" } });
input.addEventListener("change", () => {
const form = new FormData();
for (const file of Array.from(input.files ?? [])) form.append("files", file);
input.value = "";
void call(url(key), { method: "POST", body: form })
.then((r) => r.json())
.then(show, fail);
});
const pick = createButton({
label: "견적서 올리기(여러 개)",
variant: "filled",
size: "sm",
onClick: () => input.click(),
});
input.hidden = true;
body.append(pick, input, list);
void call(url(key))
.then((r) => r.json())
.then(show, fail);
},
});
}