259 lines
9.2 KiB
TypeScript
259 lines
9.2 KiB
TypeScript
/* =============================================================================
|
||
* B01_Dashboard_UI_Home.ts
|
||
* 대시보드 홈(PLAN 547-2 · 요구 369~374) — 진행 중 프로젝트 수에 따라 꼴이 저절로 바뀜
|
||
* 1 = 큰 카드 · 2~3 = 중간 카드 · 4~8 = 한 줄 목록 + 팀원별 · 단계 분포 · 9 넘음 = 요약 · 그래프
|
||
* 오른쪽 = 처리할 일 · 보관함 · 회사(회사 관리자) · 예정노선(산림청) · 처음이면 세 걸음
|
||
* 꼴 고정(자동 · 작게 · 크게) = localStorage 기억
|
||
* ========================================================================== */
|
||
|
||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||
import { fetchHome, type HomeData, type ProjectSummary } from "./B01_Dashboard_Api_Home";
|
||
import { openNewProjectPicker } from "./B01_Dashboard_UI_NewProject";
|
||
import { bigCard, midCard, rowCard, type Go } from "./B01_Dashboard_UI_Home_Cards";
|
||
import { manyView, memberPane, pane, stagePane } from "./B01_Dashboard_UI_Home_Many";
|
||
import type { ViewCtx } from "./B01_Dashboard_UI_Views";
|
||
import { homeLayout, type HomeFixed, type HomeLayout } from "./logic/B01_Dashboard_Home_Layout";
|
||
import "./B01_Dashboard_UI_Style_Home.css";
|
||
|
||
const FIXED_KEY = "aislo.b01.homeLayout";
|
||
const LAYOUT_TEXT: Record<HomeLayout, string> = {
|
||
empty: "처음 시작",
|
||
one: "진행 중 1 개 → 큰 카드",
|
||
few: "진행 중 2~3 개 → 중간 카드",
|
||
list: "진행 중 4~8 개 → 한 줄 목록",
|
||
many: "진행 중 9 개 넘음 → 요약 · 그래프",
|
||
};
|
||
|
||
function readFixed(): HomeFixed {
|
||
const saved = localStorage.getItem(FIXED_KEY);
|
||
return saved === "small" || saved === "large" ? saved : "auto";
|
||
}
|
||
|
||
function fixedSwitch(current: HomeFixed, onPick: (next: HomeFixed) => void): HTMLElement {
|
||
const seg = el("div", {
|
||
className: "b01-home__seg",
|
||
attrs: { role: "group", "aria-label": "꼴" },
|
||
});
|
||
for (const [value, label] of [
|
||
["auto", "자동"],
|
||
["small", "작게"],
|
||
["large", "크게"],
|
||
] as const) {
|
||
const b = el("button", {
|
||
className: value === current ? "on" : "",
|
||
text: label,
|
||
attrs: { type: "button", "aria-pressed": String(value === current) },
|
||
});
|
||
b.addEventListener("click", () => onPick(value));
|
||
seg.append(b);
|
||
}
|
||
return seg;
|
||
}
|
||
|
||
function todoRow(dot: string, text: string, action: string, onClick: () => void): HTMLElement {
|
||
return el("div", {
|
||
className: "b01-home__todo",
|
||
children: [
|
||
el("span", { className: `b01-home__dot b01-home__dot--${dot}` }),
|
||
el("span", { text }),
|
||
createButton({ label: action, variant: "ghost", size: "sm", onClick }),
|
||
],
|
||
});
|
||
}
|
||
|
||
/** 처리할 일 — 없으면 「처리할 일 없음」 */
|
||
function todoPane(data: HomeData, go: Go): HTMLElement {
|
||
const t = data.todos;
|
||
const rows: HTMLElement[] = [];
|
||
for (const p of t.new_plans)
|
||
rows.push(
|
||
todoRow("plan", `새 예정노선 — ${p.project_name} ${p.year}`, "보기", () => go("assigned")),
|
||
);
|
||
for (const p of t.updated_plans)
|
||
rows.push(
|
||
todoRow("warn", `업데이트 — ${p.project_name} ${p.year}`, "보기", () => go("assigned")),
|
||
);
|
||
if (t.join_requests)
|
||
rows.push(todoRow("alert", `가입 요청 ${t.join_requests} 건`, "처리", () => go("people")));
|
||
for (const b of t.expiring_temp)
|
||
rows.push(
|
||
todoRow("warn", `보관 파일 ${b.name} · ${b.days_left} 일 남음`, "열기", () => go("temp")),
|
||
);
|
||
if (!rows.length) rows.push(el("div", { className: "b01-home__muted", text: "처리할 일 없음" }));
|
||
return pane("처리할 일", ...rows);
|
||
}
|
||
|
||
function sidePanes(data: HomeData, go: Go): HTMLElement[] {
|
||
const panes = [todoPane(data, go)];
|
||
panes.push(
|
||
pane(
|
||
"임시 보관함",
|
||
el("div", { className: "b01-home__muted", text: `보관 묶음 ${data.todos.temp_count} 개` }),
|
||
createButton({ label: "보관함 ›", variant: "ghost", size: "sm", onClick: () => go("temp") }),
|
||
),
|
||
);
|
||
if (data.company) {
|
||
const c = data.company;
|
||
panes.push(
|
||
pane(
|
||
c.name ?? "회사",
|
||
el("div", {
|
||
className: "b01-home__muted",
|
||
text: `팀원 ${c.member_count ?? 0} 명 · 진행 중 ${data.projects.length} 개`,
|
||
}),
|
||
createButton({
|
||
label: "사람 ›",
|
||
variant: "ghost",
|
||
size: "sm",
|
||
onClick: () => go("people"),
|
||
}),
|
||
),
|
||
);
|
||
}
|
||
if (data.forest) {
|
||
const f = data.forest;
|
||
panes.push(
|
||
pane(
|
||
"예정노선",
|
||
el("div", {
|
||
className: "b01-home__muted",
|
||
text: `분석 전 ${f.plan_status.draft} · 분석됨 ${f.plan_status.analyzed} · 확정 ${f.plan_status.confirmed}`,
|
||
}),
|
||
el("div", {
|
||
className: f.assign_waiting ? "b01-home__stale" : "b01-home__muted",
|
||
text: `업체 지정 대기 ${f.assign_waiting}`,
|
||
}),
|
||
),
|
||
);
|
||
}
|
||
return panes;
|
||
}
|
||
|
||
/** 처음 — 세 걸음(내 정보 · 파일 · 첫 프로젝트) */
|
||
function firstSteps(ctx: ViewCtx, data: HomeData): HTMLElement {
|
||
const me = ctx.me;
|
||
const steps: [boolean, string, string, () => void][] = [
|
||
[
|
||
Boolean(me.position || me.department || me.phone),
|
||
"내 정보 채우기",
|
||
"내 정보 ›",
|
||
() => ctx.go("me"),
|
||
],
|
||
[data.todos.temp_count > 0, "측량 파일 올려 두기", "보관함 ›", () => ctx.go("temp")],
|
||
[false, "첫 프로젝트 만들기", "+ 새 프로젝트", () => void openNewProjectPicker()],
|
||
];
|
||
return pane(
|
||
"처음 오셨네요 — 세 걸음",
|
||
...steps.map(([done, text, action, onClick], i) =>
|
||
el("div", {
|
||
className: `b01-home__step${done ? " b01-home__step--done" : ""}`,
|
||
children: [
|
||
el("span", { className: "b01-home__step-no", text: done ? "✓" : String(i + 1) }),
|
||
el("span", { text }),
|
||
createButton({ label: action, variant: done ? "ghost" : "filled", size: "sm", onClick }),
|
||
],
|
||
}),
|
||
),
|
||
);
|
||
}
|
||
|
||
function mainArea(layout: HomeLayout, ctx: ViewCtx, data: HomeData): HTMLElement {
|
||
const cards: ProjectSummary[] = data.projects;
|
||
const go = ctx.go;
|
||
if (layout === "empty")
|
||
return data.project_total
|
||
? pane(
|
||
"진행 중인 프로젝트 없음",
|
||
el("div", { className: "b01-home__muted", text: "모두 끝났습니다." }),
|
||
)
|
||
: firstSteps(ctx, data);
|
||
if (layout === "one") return bigCard(cards[0], go);
|
||
if (layout === "few") {
|
||
const mids = el("div", {
|
||
className: "b01-home__mids",
|
||
children: cards.slice(0, 8).map((c, i) => midCard(c, go, i === 0)),
|
||
});
|
||
if (cards.length <= 8) return mids;
|
||
const all = createButton({ label: "모든 프로젝트 ›", onClick: () => go("projects") });
|
||
return el("div", { className: "b01-home__many", children: [mids, all] });
|
||
}
|
||
if (layout === "list")
|
||
return el("div", {
|
||
className: "b01-home__split b01-home__split--list",
|
||
children: [
|
||
el("div", {
|
||
children: [
|
||
el("div", {
|
||
className: "b01-home__muted",
|
||
text: `진행 중 ${cards.length} · 최근 작업 순`,
|
||
}),
|
||
el("div", { className: "b01-home__rows", children: cards.map((c) => rowCard(c, go)) }),
|
||
],
|
||
}),
|
||
el("div", { children: [memberPane(cards), stagePane(cards)] }),
|
||
],
|
||
});
|
||
return manyView(data, go);
|
||
}
|
||
|
||
export function mountHome(host: HTMLElement, ctx: ViewCtx): () => void {
|
||
let alive = true;
|
||
let data: HomeData | null = null;
|
||
const root = el("div", { className: "b01-home" });
|
||
host.append(root);
|
||
|
||
const draw = (): void => {
|
||
if (!data) return;
|
||
const fixed = readFixed();
|
||
const layout = homeLayout(data.projects.length, fixed);
|
||
root.dataset.layout = layout;
|
||
const head = el("div", {
|
||
className: "b01-home__head",
|
||
children: [
|
||
el("h2", { text: "홈" }),
|
||
el("span", {
|
||
className: "b01-home__muted",
|
||
text:
|
||
fixed === "auto" || layout === "empty"
|
||
? LAYOUT_TEXT[layout]
|
||
: `진행 중 ${data.projects.length} 개 · ${fixed === "small" ? "작게" : "크게"} 고정`,
|
||
}),
|
||
el("span", { className: "b01-home__push" }),
|
||
fixedSwitch(fixed, (next) => {
|
||
if (next === "auto") localStorage.removeItem(FIXED_KEY);
|
||
else localStorage.setItem(FIXED_KEY, next);
|
||
draw();
|
||
}),
|
||
createButton({ label: "+ 새 프로젝트", onClick: () => void openNewProjectPicker() }),
|
||
],
|
||
});
|
||
const body = el("div", {
|
||
className: "b01-home__body",
|
||
children: [
|
||
el("div", { className: "b01-home__main", children: [mainArea(layout, ctx, data)] }),
|
||
el("aside", { className: "b01-home__side", children: sidePanes(data, ctx.go) }),
|
||
],
|
||
});
|
||
root.replaceChildren(head, body);
|
||
};
|
||
|
||
root.append(el("div", { className: "b01-home__muted", text: "불러오는 중…" }));
|
||
fetchHome()
|
||
.then((loaded) => {
|
||
if (!alive) return;
|
||
data = loaded;
|
||
draw();
|
||
})
|
||
.catch((error: unknown) => {
|
||
if (!alive) return;
|
||
root.replaceChildren(
|
||
el("div", { className: "b01-home__muted", text: "홈을 불러오지 못했습니다." }),
|
||
);
|
||
showToast(error instanceof Error ? error.message : String(error), "error");
|
||
});
|
||
|
||
return () => {
|
||
alive = false;
|
||
};
|
||
}
|