- 상세 화면(view=project) — 머리 · [⋯] · [작업 열기 — 지금 단계 ›] · 단계 진행(누름 = 그 단계) · 노선 지도(배경 타일 + 선 · 시점 · 종점) · 핵심 숫자 · 기본 정보 · 연결된 것 · 산출물 · 최근 변경(있는 칸만) - 목록 · 상세 숫자 · 썸네일 = 547-1 카드 요약(project_summaries) 한 벌 - 색 = 셸 변수(--b01-*) 이름에 맞춤 - 시험 test_548_project_overview — 칸반 나눔 · 산출물 · 내 것 · 단계 수 짝 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
178 lines
6.2 KiB
TypeScript
178 lines
6.2 KiB
TypeScript
/* =============================================================================
|
|
* B01_Dashboard_UI_ProjectList_Views.ts
|
|
* 프로젝트 목록 보기 셋(PLAN 548-1) — 카드 · 표(줄 끝 ⋯) · 칸반(칸 = 서버 `board` · 완료 칸 접힘).
|
|
* ========================================================================== */
|
|
|
|
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
|
import { el } from "@ui/ui_template_elements_base";
|
|
import type { DashboardUser } from "./B01_Dashboard_Api_Fetch";
|
|
import type { OverviewProject } from "./B01_Dashboard_Api_Projects";
|
|
import { canDeleteProject, canEditProject } from "./B01_Dashboard_UI_Helper";
|
|
import { openDeleteProjectModal, openEditProjectModal } from "./B01_Dashboard_UI_Modals";
|
|
import {
|
|
ago,
|
|
BOARD_COLUMNS,
|
|
boardLabel,
|
|
kindBadge,
|
|
progressBar,
|
|
stageDots,
|
|
stageText,
|
|
thumbSvg,
|
|
} from "./B01_Dashboard_UI_ProjectList_Parts";
|
|
|
|
type Open = (project: OverviewProject) => void;
|
|
|
|
/** 누름 = 상세 · Enter 도 같음 */
|
|
function clickable<T extends HTMLElement>(node: T, onOpen: () => void): T {
|
|
node.tabIndex = 0;
|
|
node.addEventListener("click", onOpen);
|
|
node.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter") onOpen();
|
|
});
|
|
return node;
|
|
}
|
|
|
|
function stalledMark(project: OverviewProject): HTMLElement[] {
|
|
return project.stalled
|
|
? [
|
|
el("span", {
|
|
className: "b01-proj-stalled",
|
|
text: `${project.idle_days} 일 그대로`,
|
|
attrs: { title: "14 일 넘게 고친 것이 없음" },
|
|
}),
|
|
]
|
|
: [];
|
|
}
|
|
|
|
function card(project: OverviewProject, onOpen: Open, compact = false): HTMLElement {
|
|
const box = el("div", {
|
|
className: `b01-proj-card b01-proj-card--${(project.project_kind ?? "ROAD").toLowerCase()}`,
|
|
attrs: { "data-project": project.id },
|
|
});
|
|
const info = el("div", {
|
|
className: "b01-proj-card__body",
|
|
children: [
|
|
el("b", { className: "b01-proj-card__name", text: project.name }),
|
|
el("div", {
|
|
className: "b01-proj-card__line",
|
|
children: [
|
|
kindBadge(project.project_kind),
|
|
el("span", { className: "b01-proj-mini", text: stageText(project) }),
|
|
],
|
|
}),
|
|
...(compact ? [progressBar(project)] : [stageDots(project)]),
|
|
el("div", {
|
|
className: "b01-proj-card__meta",
|
|
children: [
|
|
el("span", { text: project.manager_name ?? "-" }),
|
|
...stalledMark(project),
|
|
el("span", { text: ago(project) }),
|
|
],
|
|
}),
|
|
],
|
|
});
|
|
box.append(thumbSvg(project.thumb), info);
|
|
return clickable(box, () => onOpen(project));
|
|
}
|
|
|
|
export function cardGrid(projects: OverviewProject[], onOpen: Open): HTMLElement {
|
|
return el("div", {
|
|
className: "b01-proj-cards",
|
|
children: projects.map((p) => card(p, onOpen)),
|
|
});
|
|
}
|
|
|
|
/** 줄 끝 ⋯ — 지금 있는 동작만(수정 · 삭제) */
|
|
export function rowMenuItems(project: OverviewProject, me: DashboardUser): MapContextMenuItem[] {
|
|
const items: MapContextMenuItem[] = [];
|
|
if (canEditProject(me, project)) {
|
|
items.push(["수정", () => void openEditProjectModal(me, project)]);
|
|
}
|
|
if (canDeleteProject(me)) {
|
|
items.push("-", ["삭제", () => openDeleteProjectModal(me, project)]);
|
|
}
|
|
return items;
|
|
}
|
|
|
|
export function projectRows(
|
|
projects: OverviewProject[],
|
|
me: DashboardUser,
|
|
onOpen: Open,
|
|
): HTMLElement {
|
|
const wrap = el("div", { className: "b01-proj-table" });
|
|
const menu = createMapContextMenu("b01-proj-menu");
|
|
wrap.append(menu.element);
|
|
const head = ["이름", "종류", "지역", "진행", "상태", "담당", "수정일", ""];
|
|
const table = el("table");
|
|
const thead = el("tr", { children: head.map((h) => el("th", { text: h })) });
|
|
table.append(thead);
|
|
for (const project of projects) {
|
|
const more = el("button", {
|
|
className: "b01-proj-more",
|
|
text: "⋯",
|
|
attrs: { type: "button", "aria-label": "더 보기" },
|
|
});
|
|
const items = rowMenuItems(project, me);
|
|
if (!items.length) more.hidden = true;
|
|
more.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
const box = wrap.getBoundingClientRect();
|
|
const at = more.getBoundingClientRect();
|
|
menu.open(at.right - box.left - 120, at.bottom - box.top, items);
|
|
});
|
|
const row = el("tr", {
|
|
className: "b01-proj-row",
|
|
attrs: { "data-project": project.id },
|
|
children: [
|
|
el("td", { children: [el("b", { text: project.name }), ...stalledMark(project)] }),
|
|
el("td", { children: [kindBadge(project.project_kind)] }),
|
|
el("td", { text: project.region ?? "-" }),
|
|
el("td", {
|
|
className: "b01-proj-prog",
|
|
children: [progressBar(project), el("span", { text: stageText(project) })],
|
|
}),
|
|
el("td", { text: boardLabel(project.board) }),
|
|
el("td", { text: project.manager_name ?? "-" }),
|
|
el("td", { text: (project.updated_at ?? "").slice(0, 10) || "-" }),
|
|
el("td", { children: [more] }),
|
|
],
|
|
});
|
|
table.append(clickable(row, () => onOpen(project)));
|
|
}
|
|
wrap.append(table);
|
|
return wrap;
|
|
}
|
|
|
|
/** 칸반 — 칸 차례 고정 · 완료 칸은 접혀 시작(머리 누름 = 펼침) */
|
|
export function kanbanBoard(projects: OverviewProject[], onOpen: Open): HTMLElement {
|
|
const board = el("div", { className: "b01-proj-kanban" });
|
|
for (const column of BOARD_COLUMNS) {
|
|
const items = projects.filter((p) => p.board === column.key);
|
|
const col = el("section", {
|
|
className: "b01-proj-col",
|
|
attrs: { "data-board": column.key },
|
|
});
|
|
const title = el("h4", {
|
|
children: [
|
|
el("span", { text: column.label }),
|
|
el("span", { className: "b01-proj-mini", text: String(items.length) }),
|
|
],
|
|
});
|
|
const list = el("div", {
|
|
className: "b01-proj-col__list",
|
|
children: items.map((p) => card(p, onOpen, true)),
|
|
});
|
|
if (column.key === "done") {
|
|
col.classList.add("is-folded");
|
|
title.append(el("span", { className: "b01-proj-fold", text: "펼치기" }));
|
|
title.addEventListener("click", () => {
|
|
const folded = col.classList.toggle("is-folded");
|
|
title.lastElementChild!.textContent = folded ? "펼치기" : "접기";
|
|
});
|
|
}
|
|
col.append(title, list);
|
|
board.append(col);
|
|
}
|
|
return board;
|
|
}
|