/* ============================================================================= * 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(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; }