feat(531-3): D04 확정 화면 · B03 [예정노선에서 가져오기] · 예정노선 바뀜 띠
- D04 화면 — 연도 표(연장 · 시점 · 종점 · 누가 · 확정 · 지정 업체) · 업체 = 등록 회사 찾기 또는 이름 + 메일 · 캐시 d04-assignees · [저장] PUT · [확정] 연도 고름 → finalize(잠금판) · [내보내기] zip - PUT plan/assignees 에 years — 업체를 다 뺀 연도도 맞춤 - B03 노선 칸 옆 단추 · 머리 띠(새 파일에서 끼움 · 기존 동작 그대로) — 연결 = existing 길 · 옛 결과 지울 때 확인 창 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import { fetchUploadOverview } from "./B03_FileInput_Api_Fetch";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { createInputGuide } from "./B03_FileInput_UI_Guide";
|
||||
import { createTempPicker } from "./B03_FileInput_UI_TempPicker";
|
||||
import { mountPlanImport } from "./B03_FileInput_UI_PlanImport";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import {
|
||||
fetchWorkflowState,
|
||||
@@ -627,6 +628,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
const routePanel = document.createElement("div");
|
||||
routePanel.className = "b03-file__control-panel b03-file__cards-container-panel";
|
||||
routePanel.append(routeGroup);
|
||||
mountPlanImport(routePanel, uploadControlPanel, () => void applyUploadOverview());
|
||||
|
||||
const terrainPanel = document.createElement("div");
|
||||
terrainPanel.className = "b03-file__control-panel b03-file__cards-container-panel";
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/* =============================================================================
|
||||
* 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 {
|
||||
createButton,
|
||||
el,
|
||||
hideLoadingOverlay,
|
||||
showConfirmDialog,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
|
||||
interface AssignedRoute {
|
||||
assignee_id: number;
|
||||
project_name: string;
|
||||
year: number;
|
||||
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__overview-pending 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(
|
||||
el("span", { text: `${t("D04_Update_Banner")} (${source.plan_name} ${source.year}) — ` }),
|
||||
createButton({
|
||||
label: t("D04_Update_Reimport"),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
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(
|
||||
el("div", {
|
||||
className: "b03-file__plan-pick",
|
||||
children: [
|
||||
el("span", { text: `${route.project_name} · ${route.year}${linked}` }),
|
||||
createButton({
|
||||
label: t("D04_Import_Btn"),
|
||||
variant: "filled",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
close();
|
||||
void done(route.assignee_id);
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
buttonHost.append(
|
||||
createButton({
|
||||
label: t("D04_Import_Btn"),
|
||||
variant: "ghost",
|
||||
onClick: () => void openPicker(),
|
||||
}),
|
||||
);
|
||||
void refreshBanner();
|
||||
}
|
||||
Reference in New Issue
Block a user