- 개요 = 숫자 카드 넷 + CPU · 메모리 · 디스크 게이지(70 % 주황 · 90 % 빨강) + 7 일 SVG 그래프 - 회사 = 목록(찾기 · ⋯) → 상세(정보 · 사람 · 프로젝트 탭) · 사용자 = 회사 · 역할 · 찾기 거르기 · ⋯ - 설계 기본값 = 설정 카드를 묶음 제목 + 줄로 · 시스템 로그 = 동작 · 기간 · 찾기 거르기 + 이전 기록 더 보기 - 내 정보 = Linear 꼴 설정 줄(기본정보 · 비밀번호 · 테마 · 내 설계 기본값) - 옛 구역(리소스 패널 · 옛 내 정보 폼) 지움 · 지킴 시험 test_549 추가 · test_546 stub 맞춤 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
163 lines
5.6 KiB
TypeScript
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;
|
|
}
|