Files
Aislo/B03_FileInput/B03_FileInput_UI_PlanImport.ts
T
eomsangdonandClaude Opus 5.5 f558f5d2ba feat(563-2): D04 표 = 조각 줄(연도 · 선) · 조각마다 업체 · B03 가져오기 · B01 받은 예정노선 = 조각 이름
- D04 업체 추가 · 저장에 조각 id · 옛 연도 지정 업체 = 그 연도 첫 줄
- B03 [예정노선에서 가져오기] 줄 · B01 받은 예정노선 연도 칸 · [연결] 제목에 「본선 0+0~12+10」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:39:12 +09:00

191 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B03_FileInput_UI_PlanImport.ts
* 예정노선에서 가져오기(요구서 345 3-5 · PLAN 531-3) — 본보기 `B03_FileInput_UI_TempPicker.ts`.
* 노선 칸 옆 [예정노선에서 가져오기] = 우리 회사에 지정된 확정 조각(옛 = 연도) 고르기 →
* POST /api/dashboard/plan-routes/{assignee}/link {mode: existing, project_id: 이 프로젝트}.
* 머리 띠 = 이어 받은 예정노선이 바뀌면 「예정노선이 바뀜 — [다시 가져오기]」(같은 길 · 자동 반영 없음).
* 옛 결과가 지워지면 서버가 409 confirm 으로 묻고 → 확인 창 → confirm_replace 로 다시.
* ========================================================================== */
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
import { createBanner } from "@ui/ui_template_banner";
import {
createButton,
el,
hideLoadingOverlay,
showConfirmDialog,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import { t } from "@ui/ui_template_locale";
import { createListRow } from "@ui/ui_template_list_row";
import { openModal } from "@ui/ui_template_modal";
interface AssignedRoute {
assignee_id: number;
project_name: string;
year: number;
/** 조각 이름 「본선 0+0~12+10」 — 옛 연도 지정 = null */
piece_label: string | null;
linked_project_id: string | null;
linked_project_name: string | null;
update_available: boolean;
}
interface PlanSource {
linked: boolean;
assignee_id?: number;
plan_name?: string;
year?: number;
update_available?: boolean;
}
async function getJson<T>(path: string): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return (await response.json()) as T;
}
/** 연결 — 지울 결과가 있으면 한 번 묻고 다시 · 성공 = 참 */
async function linkRoute(
assigneeId: number,
projectId: string,
lasFree: boolean,
): Promise<boolean> {
const send = (confirmReplace: boolean): Promise<Response> =>
fetch(`${API_BASE_URL}/dashboard/plan-routes/${assigneeId}/link`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
mode: "existing",
project_id: projectId,
confirm_replace: confirmReplace,
las_free: lasFree,
}),
});
showLoadingOverlay();
try {
let response = await send(false);
let data = (await response.json().catch(() => ({}))) as {
confirm?: string;
message?: string;
detail?: unknown;
};
if (response.status === 409 && data.confirm === "replace_outputs") {
hideLoadingOverlay();
const agreed = await showConfirmDialog(data.message ?? "", t("D04_Import_Btn"));
showLoadingOverlay();
if (!agreed) return false;
response = await send(true);
data = (await response.json().catch(() => ({}))) as typeof data;
}
if (!response.ok) {
throw new Error(typeof data.detail === "string" ? data.detail : `HTTP ${response.status}`);
}
showToast(t("D04_Import_Done"), "success");
return true;
} catch (error) {
showToast(error instanceof Error ? error.message : String(error), "error");
return false;
} finally {
hideLoadingOverlay();
}
}
/** 노선 칸에 단추 · 본문 위에 바뀜 띠를 끼움 — `onImported` = 카드 현황 다시 읽기.
* 프로젝트 · 「LAS 없이 설계」는 B03 화면과 같은 기억값(localStorage)을 읽음. */
export function mountPlanImport(
buttonHost: HTMLElement,
bannerHost: HTMLElement,
onImported: () => void,
): void {
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
if (!projectId) return;
const lasFree = (): boolean => localStorage.getItem(`b03_las_free_${projectId}`) === "1";
const banner = el("div", { className: "b03-file__plan-update" });
banner.hidden = true;
bannerHost.prepend(banner);
const done = async (assigneeId: number): Promise<void> => {
if (!(await linkRoute(assigneeId, projectId, lasFree()))) return;
onImported();
void refreshBanner();
};
async function refreshBanner(): Promise<void> {
const source = await getJson<PlanSource>(`/projects/${projectId}/plan-source`).catch(
() => null,
);
banner.hidden = !source?.update_available;
if (!source?.update_available || source.assignee_id === undefined) return;
const assigneeId = source.assignee_id;
banner.replaceChildren(
createBanner({
tone: "info",
text: `${t("D04_Update_Banner")} (${source.plan_name} ${source.year})`,
actions: [
createButton({
label: t("D04_Update_Reimport"),
variant: "ghost",
size: 24,
onClick: () => void done(assigneeId),
}),
],
}),
);
}
const openPicker = async (): Promise<void> => {
let routes: AssignedRoute[];
try {
routes = (await getJson<{ routes: AssignedRoute[] }>("/dashboard/plan-routes/assigned"))
.routes;
} catch (error) {
showToast(error instanceof Error ? error.message : String(error), "error");
return;
}
const { close } = openModal({
title: t("D04_Import_Title"),
closeLabel: "×",
mount: (body) => {
if (!routes.length) {
body.append(el("p", { text: t("D04_Import_Empty") }));
return;
}
for (const route of routes) {
const linked = route.linked_project_name
? ` · ${t("D04_Linked")}: ${route.linked_project_name}`
: "";
body.append(
createListRow({
label: [route.project_name, route.year, route.piece_label]
.filter(Boolean)
.join(" · ")
.concat(linked),
trailing: createButton({
label: t("D04_Import_Btn"),
variant: "filled",
size: 24,
onClick: () => {
close();
void done(route.assignee_id);
},
}),
}),
);
}
},
});
};
buttonHost.append(
createButton({
label: t("D04_Import_Btn"),
variant: "ghost",
onClick: () => void openPicker(),
}),
);
void refreshBanner();
}