feat(563-2): D04 표 = 조각 줄(연도 · 선) · 조각마다 업체 · B03 가져오기 · B01 받은 예정노선 = 조각 이름
- D04 업체 추가 · 저장에 조각 id · 옛 연도 지정 업체 = 그 연도 첫 줄 - B03 [예정노선에서 가져오기] 줄 · B01 받은 예정노선 연도 칸 · [연결] 제목에 「본선 0+0~12+10」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* B01_Dashboard_UI_AssignedPlans.ts
|
||||
* 「받은 예정노선」 자료 읽기(요구서 345 2-2 · PLAN 528-2) — 우리 회사에 지정된 예정노선 연도.
|
||||
* 「받은 예정노선」 자료 읽기(요구서 345 2-2 · PLAN 528-2 · 563-2) — 우리 회사에 지정된 예정노선 조각(옛 = 연도).
|
||||
* 화면 = `B01_Dashboard_UI_View_Assigned.ts` · [연결] 모달 = `B01_Dashboard_UI_PlanLink`
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface AssignedPlan {
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
year: number;
|
||||
/** 조각 이름 「본선 0+0~12+10」 — 옛 연도 지정 = null */
|
||||
piece_label: string | null;
|
||||
linked_project_id: string | null;
|
||||
/** 연도 확정 시각 — 서버 UTC */
|
||||
confirmed_at: string | null;
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { ViewCtx } from "./B01_Dashboard_UI_Views";
|
||||
|
||||
const COLUMNS: Column[] = [
|
||||
{ label: L("DE_New_Plan") },
|
||||
{ label: L("DE_Assigned_Year"), width: "90px" },
|
||||
{ label: L("DE_Assigned_Year"), width: "220px" },
|
||||
{ label: L("D04_Col_Confirmed"), width: "150px" },
|
||||
{ label: L("DE_Assigned_State"), width: "220px" },
|
||||
{ label: "", width: "44px" },
|
||||
@@ -46,7 +46,7 @@ function planRow(plan: AssignedPlan): Cell[] {
|
||||
const view = (): void => void goToWorkflowStage(plan.project_id, ROUTES.D02_PLAN_BASE);
|
||||
return [
|
||||
nameCell(plan.project_name),
|
||||
String(plan.year),
|
||||
plan.piece_label ? `${plan.year} · ${plan.piece_label}` : String(plan.year),
|
||||
kstText(plan.confirmed_at),
|
||||
stateCell(plan),
|
||||
rowMenu([
|
||||
@@ -54,7 +54,10 @@ function planRow(plan: AssignedPlan): Cell[] {
|
||||
{
|
||||
label: L("DE_Assigned_Link"),
|
||||
onClick: () =>
|
||||
void openPlanLinkModal(plan.assignee_id, `${plan.project_name} ${plan.year}`),
|
||||
void openPlanLinkModal(
|
||||
plan.assignee_id,
|
||||
[plan.project_name, plan.year, plan.piece_label].filter(Boolean).join(" "),
|
||||
),
|
||||
},
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* 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 로 다시.
|
||||
@@ -25,6 +25,8 @@ 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;
|
||||
@@ -157,7 +159,10 @@ export function mountPlanImport(
|
||||
: "";
|
||||
body.append(
|
||||
createListRow({
|
||||
label: `${route.project_name} · ${route.year}${linked}`,
|
||||
label: [route.project_name, route.year, route.piece_label]
|
||||
.filter(Boolean)
|
||||
.join(" · ")
|
||||
.concat(linked),
|
||||
trailing: createButton({
|
||||
label: t("D04_Import_Btn"),
|
||||
variant: "filled",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* =============================================================================
|
||||
* D04_PlanConfirm_UI_Page.ts
|
||||
* D04 확정 · 업체 · 내보내기(요구서 345 3-4 · PLAN 531-3) — B05~B09 틀(group_workflow).
|
||||
* 본문 = 연도 표(연장 · 시점 · 종점 · 측점 · 확정 · 지정 업체) · 연도마다 업체 여럿
|
||||
* D04 확정 · 업체 · 내보내기(요구서 345 3-4 · 요구 391 · PLAN 531-3 · 563-2) — B05~B09 틀(group_workflow).
|
||||
* 본문 = 조각 표(연도 · 선 · 연장 · 시점 · 종점 · 측점 · 확정 · 지정 업체) · 조각마다 업체 여럿
|
||||
* (조각 없는 옛 프로젝트 = 연도 한 줄 · 옛 연도 지정 업체는 그 연도 첫 줄에 보임)
|
||||
* (등록 회사 찾기 = 가입 화면 회사 검색 · 아니면 업체명 + 메일 수기).
|
||||
* 좌측 = 확정할 연도 고르기 + [내보내기] [확정] [저장].
|
||||
* 조작 = 캐시 `d04-assignees` 즉시 · [저장] = PUT …/plan/assignees(연도 전부 맞춤) → 캐시 비움 ·
|
||||
* 조작 = 캐시 `d04-assignees` 즉시 · [저장] = PUT …/plan/assignees(연도 전부 맞춤 · 조각 id) → 캐시 비움 ·
|
||||
* [확정] = (캐시 있으면 저장 먼저) POST …/plan/finalize → 잠금판 · [내보내기] = GET …/plan/export zip.
|
||||
* 산림청이 아니면(지정 회사 = 보기만) 업체 고침 · 연도 고르기 · [확정] · [저장] 없음 · [내보내기]만.
|
||||
* ========================================================================== */
|
||||
@@ -51,8 +52,15 @@ interface YearRow {
|
||||
end?: EndPoint;
|
||||
}
|
||||
|
||||
/** 표 한 줄 — 조각(piece_id) · 옛 연도(piece_id null) */
|
||||
interface PieceRow extends YearRow {
|
||||
piece_id: string | null;
|
||||
line_name: string | null;
|
||||
}
|
||||
|
||||
interface Assignee {
|
||||
year: number;
|
||||
piece_id?: string | null;
|
||||
company_id: number | null;
|
||||
company_name: string | null;
|
||||
email: string | null;
|
||||
@@ -63,6 +71,7 @@ interface Assignee {
|
||||
interface Summary {
|
||||
name: string;
|
||||
years: YearRow[];
|
||||
pieces: PieceRow[];
|
||||
assignees: Assignee[];
|
||||
}
|
||||
|
||||
@@ -87,9 +96,10 @@ const STATION_INTERVAL_M = 20;
|
||||
const station = (m: number | undefined): string =>
|
||||
m === undefined || m === null ? "—" : stationLabel(m, STATION_INTERVAL_M, 1);
|
||||
const where = (p?: EndPoint): string => (p ? `${p.lat.toFixed(6)}, ${p.lon.toFixed(6)}` : "—");
|
||||
/** 같은 업체 — 등록 회사 = id · 수기 = 메일(서버 `_key` 와 같음) */
|
||||
/** 같은 업체 — 같은 조각(옛 = 연도)에서 등록 회사 = id · 수기 = 메일(서버 `_key` 와 같음) */
|
||||
const sameFirm = (a: Assignee, b: Assignee): boolean =>
|
||||
a.year === b.year &&
|
||||
(a.piece_id ?? null) === (b.piece_id ?? null) &&
|
||||
(a.company_id !== null
|
||||
? a.company_id === b.company_id
|
||||
: b.company_id === null && (a.email ?? "").toLowerCase() === (b.email ?? "").toLowerCase());
|
||||
@@ -105,7 +115,7 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise<void> {
|
||||
const panel = el("div", { className: "d04-confirm__panel", children: [pickCard, actions] });
|
||||
mountGroupStep(root, ROUTES.D04_PLAN_CONFIRM, { mainContent: main, leftPanel: panel });
|
||||
|
||||
let summary: Summary = { name: "", years: [], assignees: [] };
|
||||
let summary: Summary = { name: "", years: [], pieces: [], assignees: [] };
|
||||
let editable = false; // 산림청만 고침
|
||||
const draft = (): Assignee[] | null =>
|
||||
editable ? readState<Assignee[]>("d04-assignees", projectId) : null;
|
||||
@@ -123,8 +133,9 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise<void> {
|
||||
async function save(): Promise<void> {
|
||||
const body = {
|
||||
years: summary.years.map((row) => row.year),
|
||||
assignees: current().map(({ year, company_id, company_name, email }) => ({
|
||||
assignees: current().map(({ year, piece_id, company_id, company_name, email }) => ({
|
||||
year,
|
||||
piece_id: piece_id ?? null,
|
||||
company_id,
|
||||
company_name,
|
||||
email,
|
||||
@@ -146,8 +157,8 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function addForm(year: number): HTMLElement {
|
||||
const listId = `d04-companies-${year}`;
|
||||
function addForm(row: PieceRow): HTMLElement {
|
||||
const listId = `d04-companies-${row.piece_id ?? row.year}`;
|
||||
const options = el("datalist", { attrs: { id: listId } });
|
||||
const found = new Map<string, number>();
|
||||
const firm = createInputField({ placeholder: t("D04_Company"), size: 28 }).input;
|
||||
@@ -182,7 +193,13 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise<void> {
|
||||
showToast(t("D04_NeedEmail"), "warning");
|
||||
return;
|
||||
}
|
||||
const next: Assignee = { year, company_id: companyId, company_name: name, email };
|
||||
const next: Assignee = {
|
||||
year: row.year,
|
||||
piece_id: row.piece_id,
|
||||
company_id: companyId,
|
||||
company_name: name,
|
||||
email,
|
||||
};
|
||||
const list = current();
|
||||
if (list.some((item) => sameFirm(item, next))) return;
|
||||
edit([...list, next]);
|
||||
@@ -227,21 +244,30 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise<void> {
|
||||
}));
|
||||
const { root: tableRoot, tbody } = tableShell(columns);
|
||||
tableRoot.querySelector("table")?.classList.add("d04-confirm__table");
|
||||
for (const row of summary.years) {
|
||||
const firms = list.filter((item) => item.year === row.year).map(firmChip);
|
||||
const firstOfYear = new Set(
|
||||
summary.pieces.filter((row, i, all) => all.findIndex((o) => o.year === row.year) === i),
|
||||
);
|
||||
// 조각 지정 = 그 조각 줄 · 옛 연도 지정(piece_id 없음) = 그 연도 첫 줄
|
||||
const belongs = (item: Assignee, row: PieceRow): boolean =>
|
||||
item.piece_id
|
||||
? item.piece_id === row.piece_id
|
||||
: item.year === row.year && firstOfYear.has(row);
|
||||
for (const row of summary.pieces) {
|
||||
const firms = list.filter((item) => belongs(item, row)).map(firmChip);
|
||||
const chain = row.saved
|
||||
? `${station(row.start_chainage_m)} ~ ${station(row.end_chainage_m)}`
|
||||
: t("D04_NotSaved");
|
||||
const tr = tableRow(columns, [
|
||||
String(row.year),
|
||||
row.line_name ? `${row.year} · ${row.line_name}` : String(row.year),
|
||||
fmt(row.length_m),
|
||||
where(row.start),
|
||||
where(row.end),
|
||||
chain,
|
||||
kstText(row.confirmed_at),
|
||||
el("div", { children: editable ? [...firms, addForm(row.year)] : firms }),
|
||||
el("div", { children: editable ? [...firms, addForm(row)] : firms }),
|
||||
]);
|
||||
tr.dataset.year = String(row.year);
|
||||
if (row.piece_id) tr.dataset.piece = row.piece_id;
|
||||
tbody.append(tr);
|
||||
}
|
||||
const notes = draft() ? [createBanner({ tone: "warning", text: t("D04_Dirty") })] : [];
|
||||
|
||||
Reference in New Issue
Block a user