Files
Aislo/B01_Dashboard/B01_Dashboard_UI_View_SysUsers.ts
T
eomsangdonandClaude Sonnet 5.5 70786cef9e feat(B01): 시스템 관리 · 내 정보 세부 페이지 여섯 — 개요 · 회사 · 사용자 · 설계 기본값 · 로그 · 내 정보 (PLAN 549-2)
- 개요 = 숫자 카드 넷 + CPU · 메모리 · 디스크 게이지(70 % 주황 · 90 % 빨강) + 7 일 SVG 그래프
- 회사 = 목록(찾기 · ⋯) → 상세(정보 · 사람 · 프로젝트 탭) · 사용자 = 회사 · 역할 · 찾기 거르기 · ⋯
- 설계 기본값 = 설정 카드를 묶음 제목 + 줄로 · 시스템 로그 = 동작 · 기간 · 찾기 거르기 + 이전 기록 더 보기
- 내 정보 = Linear 꼴 설정 줄(기본정보 · 비밀번호 · 테마 · 내 설계 기본값)
- 옛 구역(리소스 패널 · 옛 내 정보 폼) 지움 · 지킴 시험 test_549 추가 · test_546 stub 맞춤

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 18:19:47 +09:00

163 lines
5.6 KiB
TypeScript

/* =============================================================================
* B01_Dashboard_UI_View_SysUsers.ts
* 시스템 관리 › 사용자(549-2) — 전체 사용자 표 · 거르기(회사 · 역할 · 찾기) · [+ 사용자] · 줄 끝 ⋯(수정 · 역할 변경 · 삭제).
* 회사 거르기 = 서버가 그 회사 사람만 줌(전체는 최근 200 명) · 역할 · 찾기 = 받은 줄 안에서.
* 주소 `&company=<id>&q=<글>` 로 미리 거른 채 열 수 있음(회사 상세 › 사람).
* ========================================================================== */
import { currentQuery } from "../A00_Common/router";
import {
fetchAllCompanies,
fetchAllUsers,
type CompanyInfo,
type DashboardUser,
} from "./B01_Dashboard_Api_Fetch";
import { canChangeRole, canDeleteUser } from "./B01_Dashboard_UI_Helper";
import {
openAddMemberModal,
openChangeRoleModal,
openDeleteUserModal,
openEditUserModal,
} from "./B01_Dashboard_UI_Modals";
import {
badge,
dataTable,
emptyState,
nameCell,
pageFrame,
primaryButton,
rowMenu,
type RowMenuItem,
} from "./B01_Dashboard_UI_PageKit";
import { el, loadPage, searchBox, selectBox, toolbar } from "./B01_Dashboard_UI_View_Parts";
import type { ViewCtx } from "./B01_Dashboard_UI_Views";
import { roleText, statusBadgeOf } from "./logic/B01_Dashboard_Logic_Pages";
const SERVER_LIMIT = 200;
/** 역할 · 찾기(이름 · 이메일 · 회사 · 부서 · 직급) 거르기 — 순서는 받은 그대로 */
export function filterUsers(users: DashboardUser[], role: string, query: string): DashboardUser[] {
const q = query.trim().toLowerCase();
return users.filter(
(u) =>
(!role || u.role === role) &&
(!q ||
[u.name, u.email, u.company_name, u.department, u.position]
.filter(Boolean)
.join(" ")
.toLowerCase()
.includes(q)),
);
}
const COLUMNS = [
{ label: "이름" },
{ label: "회사" },
{ label: "직급" },
{ label: "부서" },
{ label: "전화" },
{ label: "역할" },
{ label: "상태" },
{ label: "" },
];
function menuOf(ctx: ViewCtx, u: DashboardUser): RowMenuItem[] {
const items: RowMenuItem[] = [
{ label: "정보 수정", onClick: () => openEditUserModal(ctx.me, u) },
];
if (canChangeRole(ctx.me, u))
items.push({ label: "역할 변경", onClick: () => openChangeRoleModal(u) });
// 회사에서 빼기 · 계정 삭제 — 처리 방식 고르는 창이 확인을 겸함 · 자기 자신은 안 보임
if (canDeleteUser(ctx.me, u) && u.id !== ctx.me.id)
items.push({ label: "삭제…", onClick: () => openDeleteUserModal(u) });
return items;
}
export function mountSysUsers(host: HTMLElement, ctx: ViewCtx): void {
const start = currentQuery();
const state = {
company: start.get("company") ?? "",
role: "",
q: (start.get("q") ?? "").toLowerCase(),
};
loadPage(host, async (stage) => {
const companies: CompanyInfo[] = await fetchAllCompanies().catch(() => []);
let users: DashboardUser[] = [];
const body = el("div");
const count = el("span", "b01-sys-count");
const draw = (): void => {
const shown = filterUsers(users, state.role, state.q);
const capped = users.length >= SERVER_LIMIT && !state.company;
count.textContent = `${shown.length} / ${users.length} 명${capped ? " · 최근 200 명까지 — 회사로 거르면 더 보임" : ""}`;
body.replaceChildren(
shown.length
? dataTable(
COLUMNS,
shown.map((u) => {
const st = statusBadgeOf(u.status);
return [
nameCell(u.name, u.email),
u.company_name ?? "-",
u.position ?? "-",
u.department ?? "-",
u.phone ?? "-",
badge(roleText(u.role), u.role === "USER" ? "neutral" : "accent"),
badge(statusLabel(u.status, st.text), st.tone),
rowMenu(menuOf(ctx, u)),
];
}),
)
: emptyState({
icon: "user",
text: users.length ? "조건에 맞는 사용자가 없습니다." : "사용자가 없습니다.",
}),
);
};
const load = async (): Promise<void> => {
users = await fetchAllUsers(state.company ? Number(state.company) : null);
draw();
};
await load();
const company = selectBox(
"회사",
[["", "모든 회사"], ...companies.map((c): [string, string] => [String(c.id), c.name])],
(v) => {
state.company = v;
void load();
},
);
company.value = state.company;
const role = selectBox(
"역할",
[
["", "모든 역할"],
["SYSTEM_ADMIN", "시스템 관리자"],
["ADMIN", "회사 관리자"],
["USER", "일반 사용자"],
],
(v) => {
state.role = v;
draw();
},
);
const search = searchBox("이름 · 이메일 · 부서 찾기", (v) => {
state.q = v;
draw();
});
search.value = state.q;
const page = pageFrame(stage, "사용자", "모든 회사의 사용자를 보고 정보 · 역할을 고칩니다.");
page.setAction(
primaryButton("사용자", () =>
openAddMemberModal(undefined, state.company ? Number(state.company) : ctx.companyId),
),
);
page.body.append(toolbar(search, company, role, count), body);
});
}
/** 계정 상태 글 — 공용 배지 글에 없는 값(회사 없음)만 덧붙임 */
function statusLabel(status: string, fallback: string): string {
return status === "NO_COMPANY" ? "회사 없음" : fallback;
}