229 lines
7.9 KiB
TypeScript
229 lines
7.9 KiB
TypeScript
/* =============================================================================
|
||
* B01_Dashboard_UI_View_SysCompanies.ts
|
||
* 시스템 관리 › 회사(549-2) — 회사 목록(찾기 · 줄 끝 ⋯) → 회사 상세(정보 · 사람 · 프로젝트 탭) · [+ 회사].
|
||
* 상세 = 주소 `&company=<id>`(뒤로 가기 됨) · 수정 · 로고 · 새 회사 = 지금 창(Modals) 그대로.
|
||
* ========================================================================== */
|
||
|
||
import { createButton } from "@ui/ui_template_elements";
|
||
import { currentQuery } from "../A00_Common/router";
|
||
import {
|
||
fetchAllCompanies,
|
||
fetchAllUsers,
|
||
fetchCompanyProjects,
|
||
setCompanyLogo,
|
||
type CompanyInfo,
|
||
type ProjectItem,
|
||
} from "./B01_Dashboard_Api_Fetch";
|
||
import { openAssetPicker } from "./B01_Dashboard_UI_AssetPicker";
|
||
import { formatDate } from "./B01_Dashboard_UI_Common";
|
||
import { filterCompanies } from "./B01_Dashboard_UI_CompanyPick";
|
||
import { openCreateCompanyModal, openEditCompanyModal } from "./B01_Dashboard_UI_Modals";
|
||
import { viewHref } from "./B01_Dashboard_UI_Nav";
|
||
import {
|
||
badge,
|
||
dataTable,
|
||
emptyState,
|
||
nameCell,
|
||
pageFrame,
|
||
primaryButton,
|
||
rowMenu,
|
||
settingCard,
|
||
settingRow,
|
||
tabBar,
|
||
valueText,
|
||
} from "./B01_Dashboard_UI_PageKit";
|
||
import { el, loadPage, searchBox, toolbar } from "./B01_Dashboard_UI_View_Parts";
|
||
import type { ViewCtx } from "./B01_Dashboard_UI_Views";
|
||
import { roleText, statusBadgeOf } from "./logic/B01_Dashboard_Logic_Pages";
|
||
|
||
export const KIND_KO: Record<string, string> = {
|
||
ROAD: "임도설계",
|
||
PLAN: "예정노선",
|
||
STABILITY: "안정성평가",
|
||
EROSION: "사방설계",
|
||
};
|
||
|
||
const statusTag = (s?: string | null): HTMLElement =>
|
||
badge(s || "-", s === "활동중" ? "success" : "neutral");
|
||
|
||
const logoLabel = (c: CompanyInfo): string => (c.logo_asset_id ? "로고 변경…" : "로고 지정…");
|
||
|
||
function pickLogo(ctx: ViewCtx, company: CompanyInfo, after?: () => void): void {
|
||
void openAssetPicker("LOGO", company.id, ctx.me, company.logo_asset_id ?? null, async (id) => {
|
||
await setCompanyLogo(id, company.id);
|
||
company.logo_asset_id = id;
|
||
after?.();
|
||
});
|
||
}
|
||
|
||
/** 이름 칸 — 이름은 상세로 가는 링크(뒤로 가기 · 새 탭 됨) + 아래 작은 주소 */
|
||
function linkedName(c: CompanyInfo): HTMLElement {
|
||
const link = el("a", "b01-pg-name__main", c.name);
|
||
link.href = viewHref("sys-companies", { company: String(c.id) });
|
||
const cell = nameCell(c.name, c.business_address);
|
||
cell.firstElementChild!.replaceWith(link);
|
||
return cell;
|
||
}
|
||
|
||
const LIST_COLUMNS = [
|
||
{ label: "회사" },
|
||
{ label: "사업자등록번호" },
|
||
{ label: "대표자" },
|
||
{ label: "상태" },
|
||
{ label: "사용자", align: "end" as const },
|
||
{ label: "프로젝트", align: "end" as const },
|
||
{ label: "" },
|
||
];
|
||
|
||
function companyList(stage: HTMLElement, ctx: ViewCtx, companies: CompanyInfo[]): void {
|
||
const page = pageFrame(
|
||
stage,
|
||
"회사",
|
||
"등록된 회사를 보고 정보 · 로고를 고칩니다. 이름을 누르면 상세입니다.",
|
||
);
|
||
page.setAction(primaryButton("회사", () => openCreateCompanyModal()));
|
||
const body = el("div");
|
||
const draw = (query: string): void => {
|
||
const shown = filterCompanies(companies, query);
|
||
if (!shown.length) {
|
||
body.replaceChildren(
|
||
companies.length
|
||
? emptyState({ icon: "search", text: "찾는 회사가 없습니다." })
|
||
: emptyState({
|
||
icon: "building",
|
||
text: "등록된 회사가 없습니다.",
|
||
action: primaryButton("회사", () => openCreateCompanyModal()),
|
||
}),
|
||
);
|
||
return;
|
||
}
|
||
body.replaceChildren(
|
||
dataTable(
|
||
LIST_COLUMNS,
|
||
shown.map((c) => [
|
||
linkedName(c),
|
||
c.business_registration_number ?? "-",
|
||
c.business_owner ?? "-",
|
||
statusTag(c.business_status),
|
||
String(c.user_count ?? 0),
|
||
String(c.project_count ?? 0),
|
||
rowMenu([
|
||
{
|
||
label: "상세 보기",
|
||
onClick: () => ctx.go("sys-companies", { company: String(c.id) }),
|
||
},
|
||
{ label: "정보 수정", onClick: () => openEditCompanyModal(c) },
|
||
{ label: logoLabel(c), onClick: () => pickLogo(ctx, c) },
|
||
]),
|
||
]),
|
||
),
|
||
);
|
||
};
|
||
draw("");
|
||
page.body.append(toolbar(searchBox("회사 이름 · 사업자번호 찾기", draw)), body);
|
||
}
|
||
|
||
const projectTable = (projects: ProjectItem[]): HTMLElement =>
|
||
projects.length
|
||
? dataTable(
|
||
[
|
||
{ label: "프로젝트" },
|
||
{ label: "종류" },
|
||
{ label: "지역" },
|
||
{ label: "단계" },
|
||
{ label: "담당" },
|
||
{ label: "수정일" },
|
||
],
|
||
projects.map((p) => [
|
||
p.name,
|
||
KIND_KO[p.project_kind ?? "ROAD"] ?? "-",
|
||
p.region ?? "-",
|
||
`${p.workflow_stage} 단계`,
|
||
p.owner_name ?? "-",
|
||
formatDate(p.updated_at),
|
||
]),
|
||
)
|
||
: emptyState({ icon: "folder", text: "이 회사의 프로젝트가 없습니다." });
|
||
|
||
async function companyDetail(
|
||
stage: HTMLElement,
|
||
ctx: ViewCtx,
|
||
company: CompanyInfo,
|
||
): Promise<void> {
|
||
const [users, projects] = await Promise.all([
|
||
fetchAllUsers(company.id).catch(() => []),
|
||
fetchCompanyProjects(company.id).catch(() => []),
|
||
]);
|
||
const page = pageFrame(stage, company.name, "회사 상세");
|
||
page.setAction(
|
||
createButton({ label: "정보 수정", onClick: () => openEditCompanyModal(company) }),
|
||
);
|
||
const back = el("button", "b01-sys-back", "‹ 회사 목록");
|
||
back.type = "button";
|
||
back.addEventListener("click", () => ctx.go("sys-companies"));
|
||
const logoBtn = createButton({
|
||
label: logoLabel(company),
|
||
variant: "ghost",
|
||
onClick: () => pickLogo(ctx, company, () => (logoBtn.textContent = logoLabel(company))),
|
||
});
|
||
const info = settingCard([
|
||
settingRow("회사 이름", "", valueText(company.name)),
|
||
settingRow("사업자등록번호", "", valueText(company.business_registration_number)),
|
||
settingRow("대표자", "", valueText(company.business_owner)),
|
||
settingRow("주소", "", valueText(company.business_address)),
|
||
settingRow("상태", "", statusTag(company.business_status)),
|
||
settingRow("로고", "도면 표제란에 들어갑니다.", logoBtn),
|
||
]);
|
||
const people = users.length
|
||
? dataTable(
|
||
[
|
||
{ label: "이름" },
|
||
{ label: "직급" },
|
||
{ label: "부서" },
|
||
{ label: "역할" },
|
||
{ label: "상태" },
|
||
{ label: "" },
|
||
],
|
||
users.map((u) => {
|
||
const st = statusBadgeOf(u.status);
|
||
return [
|
||
nameCell(u.name, u.email),
|
||
u.position ?? "-",
|
||
u.department ?? "-",
|
||
roleText(u.role),
|
||
badge(st.text, st.tone),
|
||
rowMenu([
|
||
{
|
||
label: "사용자 페이지에서 보기",
|
||
onClick: () => ctx.go("sys-users", { company: String(company.id), q: u.email }),
|
||
},
|
||
]),
|
||
];
|
||
}),
|
||
)
|
||
: emptyState({ icon: "users", text: "이 회사에 소속된 사용자가 없습니다." });
|
||
const panes: Record<string, HTMLElement> = { info, people, projects: projectTable(projects) };
|
||
const pane = el("div");
|
||
pane.append(info);
|
||
const tabs = tabBar(
|
||
[
|
||
{ key: "info", label: "정보" },
|
||
{ key: "people", label: `사람 ${users.length}` },
|
||
{ key: "projects", label: `프로젝트 ${projects.length}` },
|
||
],
|
||
"info",
|
||
(key) => pane.replaceChildren(panes[key]),
|
||
);
|
||
page.body.append(back, tabs.root, pane);
|
||
}
|
||
|
||
export function mountSysCompanies(host: HTMLElement, ctx: ViewCtx): void {
|
||
const id = Number(currentQuery().get("company"));
|
||
loadPage(host, async (stage) => {
|
||
const companies = await fetchAllCompanies();
|
||
const company = id ? companies.find((c) => c.id === id) : undefined;
|
||
if (company) await companyDetail(stage, ctx, company);
|
||
else companyList(stage, ctx, companies);
|
||
});
|
||
}
|