- D04 업체 추가 · 저장에 조각 id · 옛 연도 지정 업체 = 그 연도 첫 줄 - B03 [예정노선에서 가져오기] 줄 · B01 받은 예정노선 연도 칸 · [연결] 제목에 「본선 0+0~12+10」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
191 lines
6.4 KiB
TypeScript
191 lines
6.4 KiB
TypeScript
/* =============================================================================
|
||
* 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();
|
||
}
|