Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1
This commit is contained in:
@@ -36,6 +36,14 @@ const FLOWS: readonly GroupFlow[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/** 종류별 단계 묶음(대시보드 표 단계 막대 · 528-2) — ROAD · 모르는 종류 = 없음(B 막대 그대로) */
|
||||
export function groupFlowOf(
|
||||
kind: string | null | undefined,
|
||||
): { routes: readonly RoutePath[]; steps: string[]; icons: readonly string[] } | null {
|
||||
const flow = { PLAN: FLOWS[0], STABILITY: FLOWS[1] }[kind ?? ""];
|
||||
return flow ? { ...flow, steps: flow.labels.map((key) => t(key)) } : null;
|
||||
}
|
||||
|
||||
/** 단계 틀을 세움 — mainContent · leftPanel 이 없으면 빈 단계 안내 · [?] 줄만 */
|
||||
export function mountGroupStep(
|
||||
root: HTMLElement,
|
||||
|
||||
@@ -39,6 +39,8 @@ export interface ProjectItem {
|
||||
id: string;
|
||||
company_id?: number | null;
|
||||
name: string;
|
||||
/** 종류(요구 345) — ROAD 임도 · PLAN 예정노선 · STABILITY 안정성평가 · EROSION 사방 */
|
||||
project_kind?: string | null;
|
||||
region?: string | null;
|
||||
road_type?: string | null;
|
||||
project_year?: number | null;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/* =============================================================================
|
||||
* B01_Dashboard_UI_AssignedPlans.ts
|
||||
* 대시보드 「받은 예정노선」 구역(요구서 345 2-2 · PLAN 528-2) — 우리 회사에 지정된 예정노선 연도.
|
||||
* 열 = 예정노선 · 연도 · 연결 상태(연결됨 · 미연결 · 업데이트 있음 배지) · [보기] [연결]
|
||||
* [보기] = 그 예정노선 첫 단계(D02 · 공유 회사 = GET 만) · [연결] = 자리만(동작은 531-3)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL, ROUTES } from "@config/config_frontend";
|
||||
import { createButton, createTag } from "@ui/ui_template_elements";
|
||||
import { table, text } from "@ui/ui_template_general_blocks";
|
||||
import { goToWorkflowStage } from "../A00_Common/b_workflow_nav";
|
||||
import { DASHBOARD_VISIBLE_ROWS, L } from "./B01_Dashboard_UI_Common";
|
||||
|
||||
export interface AssignedPlan {
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
year: number;
|
||||
linked_project_id: string | null;
|
||||
update_available: boolean;
|
||||
}
|
||||
|
||||
/** 받은 예정노선 — 실패 · 없음 = 빈 목록(구역을 안 그림) */
|
||||
export async function fetchAssignedPlans(): Promise<AssignedPlan[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/dashboard/plan-routes/assigned`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!response.ok) return [];
|
||||
return ((await response.json()) as { routes?: AssignedPlan[] }).routes ?? [];
|
||||
}
|
||||
|
||||
function linkState(plan: AssignedPlan): HTMLElement {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "b01-dashboard__actions";
|
||||
cell.append(
|
||||
createTag(
|
||||
L(plan.linked_project_id ? "DE_Assigned_Linked" : "DE_Assigned_Unlinked"),
|
||||
plan.linked_project_id ? "success" : "neutral",
|
||||
),
|
||||
);
|
||||
if (plan.update_available) cell.append(createTag(L("DE_Assigned_Update"), "warning"));
|
||||
return cell;
|
||||
}
|
||||
|
||||
export function assignedPlanTable(plans: AssignedPlan[]): HTMLElement {
|
||||
return table(
|
||||
[
|
||||
L("DE_New_Plan"),
|
||||
L("DE_Assigned_Year"),
|
||||
L("DE_Assigned_State"),
|
||||
L("B01_Dashboard_Table_Action"),
|
||||
],
|
||||
plans.map((plan) => {
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "b01-dashboard__actions";
|
||||
const link = createButton({ label: L("DE_Assigned_Link"), variant: "ghost" });
|
||||
link.disabled = true;
|
||||
link.title = L("DE_New_Soon");
|
||||
actions.append(
|
||||
createButton({
|
||||
label: L("DE_Assigned_View"),
|
||||
variant: "ghost",
|
||||
onClick: () => void goToWorkflowStage(plan.project_id, ROUTES.D02_PLAN_BASE),
|
||||
}),
|
||||
link,
|
||||
);
|
||||
return [text(plan.project_name), text(plan.year), linkState(plan), actions];
|
||||
}),
|
||||
DASHBOARD_VISIBLE_ROWS,
|
||||
);
|
||||
}
|
||||
@@ -45,6 +45,11 @@ import { openAddMemberModal, openCreateCompanyModal } from "./B01_Dashboard_UI_M
|
||||
import { buildProfileForm, buildSecurityForm } from "./B01_Dashboard_UI_Profile";
|
||||
import { openCompanyPicker } from "./B01_Dashboard_UI_CompanyPick";
|
||||
import { openNewProjectPicker } from "./B01_Dashboard_UI_NewProject";
|
||||
import {
|
||||
assignedPlanTable,
|
||||
fetchAssignedPlans,
|
||||
type AssignedPlan,
|
||||
} from "./B01_Dashboard_UI_AssignedPlans";
|
||||
import { projectTable } from "./B01_Dashboard_UI_Projects";
|
||||
import { buildResourcePanel } from "./B01_Dashboard_UI_Resources";
|
||||
import { buildTempUploadSection } from "./B01_Dashboard_UI_TempUpload";
|
||||
@@ -65,6 +70,8 @@ export interface DashboardState {
|
||||
allJoinRequests: JoinRequest[];
|
||||
auditLogs: AuditLog[];
|
||||
resources: ResourceData | null;
|
||||
/** 우리 회사에 지정된 예정노선(528-2) — 회사관리자 · 사용자만 */
|
||||
assignedPlans: AssignedPlan[];
|
||||
/** 시스템 관리자가 고른 회사(29-1) — 프로젝트 · 사용자 · 가입 요청 · 회사 관리가 이 회사 것 */
|
||||
adminCompanyId: number | null;
|
||||
}
|
||||
@@ -96,6 +103,7 @@ export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
||||
allJoinRequests: [],
|
||||
auditLogs: [],
|
||||
resources: null,
|
||||
assignedPlans: [],
|
||||
adminCompanyId: null,
|
||||
};
|
||||
await loadRoleData(state);
|
||||
@@ -134,12 +142,14 @@ async function loadRoleData(state: DashboardState): Promise<void> {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const [projects, company] = await Promise.all([
|
||||
const [projects, company, assigned] = await Promise.all([
|
||||
fetchUserProjects().catch(() => []),
|
||||
fetchUserCompany().catch(() => null),
|
||||
fetchAssignedPlans().catch(() => []),
|
||||
]);
|
||||
state.userProjects = projects;
|
||||
state.company = company;
|
||||
state.assignedPlans = assigned;
|
||||
if (state.user.role === "ADMIN") {
|
||||
const [members, requests, companyProjects] = await Promise.all([
|
||||
fetchCompanyMembers().catch(() => []),
|
||||
@@ -219,6 +229,7 @@ const SECTION_ICONS: [Parameters<typeof L>[0], string][] = [
|
||||
["B01_Dashboard_Resources", "📊"],
|
||||
["B01_Dashboard_SystemSettings", "⚙️"],
|
||||
["B01_Dashboard_Projects", "🗂️"],
|
||||
["DE_Assigned_Title", "📬"],
|
||||
["B01_Temp_Section", "📦"],
|
||||
["B01_Dashboard_Users", "👥"],
|
||||
["B01_Dashboard_Members", "👥"],
|
||||
@@ -246,6 +257,12 @@ function mountSectionShortcuts(grid: HTMLElement): void {
|
||||
setDrawerSteps(bar);
|
||||
}
|
||||
|
||||
/** 받은 예정노선 구역 — 받은 것이 있을 때만 */
|
||||
function assignedPlanSection(state: DashboardState): HTMLElement[] {
|
||||
if (!state.assignedPlans.length) return [];
|
||||
return [section(L("DE_Assigned_Title"), assignedPlanTable(state.assignedPlans), true)];
|
||||
}
|
||||
|
||||
function buildPage(state: DashboardState): HTMLElement {
|
||||
// 제목·여백은 공용 템플릿을 따른다 (2026-09-06 사용자 지시) — B02 등 다른 화면과 같은 모양.
|
||||
const page = document.createElement("div");
|
||||
@@ -297,6 +314,7 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => void openNewProjectPicker() }),
|
||||
]),
|
||||
...assignedPlanSection(state),
|
||||
buildTempUploadSection(),
|
||||
section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => openAddMemberModal() }),
|
||||
@@ -310,6 +328,7 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => void openNewProjectPicker() }),
|
||||
]),
|
||||
...assignedPlanSection(state),
|
||||
// 임시 보관함 — 프로젝트를 만들기 전에 자료를 올려 두는 곳이라 프로젝트 목록
|
||||
// 바로 아래에 둔다(2026-08-08 사용자 지시).
|
||||
buildTempUploadSection(),
|
||||
|
||||
@@ -3,15 +3,27 @@ import { table, text } from "@ui/ui_template_general_blocks";
|
||||
import { createStepBar } from "@ui/ui_template_workflow_layout";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
||||
import { groupFlowOf } from "../A00_Common/group_workflow";
|
||||
import type { DashboardUser, ProjectItem } from "./B01_Dashboard_Api_Fetch";
|
||||
import { canDeleteProject, canEditProject } from "./B01_Dashboard_UI_Helper";
|
||||
import { openDeleteProjectModal, openEditProjectModal } from "./B01_Dashboard_UI_Modals";
|
||||
import { DASHBOARD_VISIBLE_ROWS, formatDate, L } from "./B01_Dashboard_UI_Common";
|
||||
|
||||
/** 종류 열 글(528-2) — 칸이 없는 옛 응답 = 임도 */
|
||||
const KIND_LABEL = {
|
||||
ROAD: "DE_New_Road",
|
||||
PLAN: "DE_New_Plan",
|
||||
STABILITY: "DE_New_Stability",
|
||||
EROSION: "DE_New_Erosion",
|
||||
} as const;
|
||||
const kindText = (project: ProjectItem): string =>
|
||||
L(KIND_LABEL[(project.project_kind ?? "ROAD") as keyof typeof KIND_LABEL] ?? "DE_New_Road");
|
||||
|
||||
export function projectTable(projects: ProjectItem[], currentUser: DashboardUser): HTMLElement {
|
||||
return table(
|
||||
[
|
||||
L("B01_Dashboard_Table_Project"),
|
||||
L("DE_Kind_Column"),
|
||||
L("B01_Dashboard_Table_Region"),
|
||||
// 진행도(%) 열은 없앴다 (2026-09-06 사용자 지시) — 워크플로 배지가 같은 것을 보여 준다.
|
||||
L("B01_Dashboard_Table_Workflow"),
|
||||
@@ -43,6 +55,7 @@ export function projectTable(projects: ProjectItem[], currentUser: DashboardUser
|
||||
|
||||
return [
|
||||
text(project.name),
|
||||
text(kindText(project)),
|
||||
text(project.region),
|
||||
workflow(project),
|
||||
text(formatDate(project.updated_at)),
|
||||
@@ -53,16 +66,19 @@ export function projectTable(projects: ProjectItem[], currentUser: DashboardUser
|
||||
);
|
||||
}
|
||||
|
||||
/** 단계 막대 — 종류별 단계(ROAD = B03~B09 · PLAN = D02~D04 · STABILITY = E02~E05) · 누름 = 그 단계 화면 */
|
||||
export function workflow(project: ProjectItem): HTMLElement {
|
||||
const group = groupFlowOf(project.project_kind);
|
||||
const routes = group?.routes ?? WORKFLOW_STEP_ROUTES;
|
||||
return createStepBar(
|
||||
workflowSteps(),
|
||||
group?.steps ?? workflowSteps(),
|
||||
project.workflow_state?.current_stage ?? project.workflow_stage,
|
||||
{
|
||||
stages: project.workflow_state?.stages,
|
||||
currentStage: project.workflow_state?.current_stage,
|
||||
routes: WORKFLOW_STEP_ROUTES,
|
||||
routes,
|
||||
compact: true,
|
||||
onStepClick: (stepIndex) => goToWorkflowStage(project.id, WORKFLOW_STEP_ROUTES[stepIndex]),
|
||||
onStepClick: (stepIndex) => goToWorkflowStage(project.id, routes[stepIndex]),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,8 +46,7 @@ export function renderE01StabRegister(root: HTMLElement): void {
|
||||
label: t("DE_Reg_Submit"),
|
||||
variant: "filled",
|
||||
onClick: () => {
|
||||
source.setError();
|
||||
if (!source.select.value) return void source.setError(t("B02_Proj_Error_Required"));
|
||||
// 목록이 비면 단추가 꺼져 있음 — 고른 값은 늘 있음
|
||||
void createGroupProject(
|
||||
"/e01/project",
|
||||
{ project_name: name.input.value.trim() || null, source_project_id: source.select.value },
|
||||
|
||||
+1
-1
@@ -34,6 +34,6 @@
|
||||
"@kninnug/constrainautor": ["config/node_modules/@kninnug/constrainautor"],
|
||||
}
|
||||
},
|
||||
"include": ["A00_Common", "A0*", "B0*", "B1*", "M0*", "D0*", "E0*", "Z0*", "ui_template", "config/**/*", "common_util"],
|
||||
"include": ["A00_Common", "A0*", "B0*", "B1*", "M0*", "D0*/**/*", "E0*/**/*", "Z0*", "ui_template", "config/**/*", "common_util"],
|
||||
"exclude": ["0_old", "node_modules", "venv", "dist"]
|
||||
}
|
||||
|
||||
@@ -62,4 +62,15 @@ export const ui_locales_de = {
|
||||
],
|
||||
DE_Reg_Submit: ["등록", "Register"],
|
||||
DE_Reg_Success: ["프로젝트를 만들었습니다.", "Project created."],
|
||||
|
||||
/* --- 대시보드 표 · 받은 예정노선(528-2) --- */
|
||||
DE_Kind_Column: ["종류", "Kind"],
|
||||
DE_Assigned_Title: ["받은 예정노선", "Assigned Planned Routes"],
|
||||
DE_Assigned_Year: ["연도", "Year"],
|
||||
DE_Assigned_State: ["연결 상태", "Link"],
|
||||
DE_Assigned_Linked: ["연결됨", "Linked"],
|
||||
DE_Assigned_Unlinked: ["미연결", "Not linked"],
|
||||
DE_Assigned_Update: ["업데이트 있음", "Update available"],
|
||||
DE_Assigned_View: ["보기", "View"],
|
||||
DE_Assigned_Link: ["연결", "Link"],
|
||||
} as const satisfies Record<string, LocaleEntry>;
|
||||
|
||||
Reference in New Issue
Block a user