- 새 페이지 다섯(View_Assigned · Temp · People · Company · CompanyDefaults) · 등록부 다섯 줄 교체 - 표 줄 40 · 세로선 없음 · 줄 끝 ⋯ · 한글 배지 · 빈 상태 · 사람 탭 가입 요청 수 배지 · 보관함 남은 날 막대(7 일 안 빨강) - 옛 구역 정리(Legacy · Company · AssignedPlans · TempUpload 다시 씀) · 시험 test_549 · test_546 stub 일반화 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
248 lines
8.5 KiB
TypeScript
248 lines
8.5 KiB
TypeScript
/* =============================================================================
|
|
* B01_Dashboard_UI_View_People.ts
|
|
* 세부 페이지 「사람」(549-1) — 탭 셋: 팀원 · 가입 요청(수 배지) · 초대.
|
|
* 회사 관리자 = 자기 회사 · 시스템 관리자 = 머리에서 고른 회사
|
|
* 팀원 줄 ⋯ = 수정 · 역할 변경 · 회사에서 빼기(빨강) — 확인은 그 모달이 맡음
|
|
* 가입 요청 줄 = [승인] [거절] · 초대 = 이메일로 안내 메일 · 소속 없는 가입자는 [팀원 추가]
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
|
|
import {
|
|
fetchAllUsers,
|
|
fetchCompanyJoinRequests,
|
|
fetchCompanyMembers,
|
|
inviteMember,
|
|
type DashboardUser,
|
|
type JoinRequest,
|
|
type Member,
|
|
} from "./B01_Dashboard_Api_Fetch";
|
|
import { formatDate, L } from "./B01_Dashboard_UI_Common";
|
|
import { onB01_JoinRequest_Process_Click } from "./B01_Dashboard_UI_Company";
|
|
import { canChangeRole, canDeleteUser } from "./B01_Dashboard_UI_Helper";
|
|
import {
|
|
openAddMemberModal,
|
|
openChangeRoleModal,
|
|
openDeleteUserModal,
|
|
openEditUserModal,
|
|
} from "./B01_Dashboard_UI_Modals";
|
|
import {
|
|
badge,
|
|
dataTable,
|
|
emptyState,
|
|
kitIcon,
|
|
pageFrame,
|
|
primaryButton,
|
|
rowMenu,
|
|
tabBar,
|
|
type Cell,
|
|
type Column,
|
|
} from "./B01_Dashboard_UI_PageKit";
|
|
import type { ViewCtx } from "./B01_Dashboard_UI_Views";
|
|
import { pendingCount, roleText, statusBadgeOf } from "./logic/B01_Dashboard_Logic_Pages";
|
|
|
|
type TabKey = "members" | "requests" | "invite";
|
|
/** 승인 · 거절 뒤 페이지가 다시 그려져도 보던 탭을 유지 */
|
|
let lastTab: TabKey = "members";
|
|
|
|
const MEMBER_COLUMNS: Column[] = [
|
|
{ label: "이름" },
|
|
{ label: "이메일" },
|
|
{ label: "직급 · 부서" },
|
|
{ label: "역할", width: "120px" },
|
|
{ label: "상태", width: "90px" },
|
|
{ label: "", width: "44px" },
|
|
];
|
|
const REQUEST_COLUMNS: Column[] = [
|
|
{ label: "요청자" },
|
|
{ label: "이메일" },
|
|
{ label: "요청일", width: "110px" },
|
|
{ label: "상태", width: "90px" },
|
|
{ label: "", width: "150px", align: "end" },
|
|
];
|
|
|
|
function person(name: string): HTMLElement {
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "b01-pg-person";
|
|
const avatar = document.createElement("span");
|
|
avatar.className = "b01-pg-avatar";
|
|
avatar.textContent = (name.trim() || "?").slice(0, 1);
|
|
const text = document.createElement("span");
|
|
text.className = "b01-pg-name__main";
|
|
text.textContent = name || "-";
|
|
wrap.append(avatar, text);
|
|
return wrap;
|
|
}
|
|
|
|
function memberRow(m: Member, ctx: ViewCtx): Cell[] {
|
|
const me = ctx.me;
|
|
const status = statusBadgeOf(m.status);
|
|
const items = [{ label: L("Common_Btn_Edit"), onClick: () => openEditUserModal(me, m) }];
|
|
const rows = [...items];
|
|
if (canChangeRole(me, m)) {
|
|
rows.push({ label: L("B01_Dashboard_ChangeRole"), onClick: () => openChangeRoleModal(m) });
|
|
}
|
|
const system = ctx.role === "SYSTEM_ADMIN";
|
|
const removable = canDeleteUser(me, m) && (!system || m.id !== me.id);
|
|
return [
|
|
person(m.name),
|
|
m.email,
|
|
[m.position, m.department].filter(Boolean).join(" · ") || "-",
|
|
badge(roleText(m.role), m.role === "ADMIN" || m.role === "SYSTEM_ADMIN" ? "accent" : "neutral"),
|
|
badge(status.text, status.tone),
|
|
rowMenu([
|
|
...rows,
|
|
...(removable
|
|
? [
|
|
{
|
|
label: L(system ? "B01_Dashboard_DeleteUser" : "B01_Dashboard_RemoveMember"),
|
|
danger: true,
|
|
ownConfirm: true,
|
|
onClick: () => openDeleteUserModal(m),
|
|
},
|
|
]
|
|
: []),
|
|
]),
|
|
];
|
|
}
|
|
|
|
function requestRow(r: JoinRequest, system: boolean): Cell[] {
|
|
const status = statusBadgeOf(r.status);
|
|
const waiting = !r.status || r.status.toUpperCase() === "PENDING";
|
|
const actions = document.createElement("span");
|
|
actions.className = "b01-pg-inline";
|
|
if (waiting) {
|
|
actions.append(
|
|
createButton({
|
|
label: L("B01_Dashboard_Approve"),
|
|
size: "sm",
|
|
onClick: () => void onB01_JoinRequest_Process_Click(r.id, "APPROVE", system),
|
|
}),
|
|
createButton({
|
|
label: L("B01_Dashboard_Reject"),
|
|
size: "sm",
|
|
variant: "danger",
|
|
onClick: () => void onB01_JoinRequest_Process_Click(r.id, "REJECT", system),
|
|
}),
|
|
);
|
|
}
|
|
return [
|
|
person(r.user_name ?? r.user_email),
|
|
r.user_email,
|
|
formatDate(r.requested_at),
|
|
badge(status.text, status.tone),
|
|
actions,
|
|
];
|
|
}
|
|
|
|
/** 초대 탭 — 아직 가입 전인 사람에게 안내 메일 · 이미 가입한 사람은 [팀원 추가] */
|
|
function invitePanel(companyId: number | null | undefined, onAdd: () => void): HTMLElement {
|
|
const box = document.createElement("div");
|
|
box.className = "b01-pg-card b01-pg-invite";
|
|
const email = createInputField({
|
|
label: "초대할 이메일",
|
|
type: "email",
|
|
placeholder: "name@company.com",
|
|
});
|
|
const send = createButton({
|
|
label: "초대 메일 보내기",
|
|
iconStart: kitIcon("mail"),
|
|
onClick: async () => {
|
|
const value = email.input.value.trim();
|
|
if (!value.includes("@")) {
|
|
email.setError("메일을 보낼 이메일 주소를 입력");
|
|
return;
|
|
}
|
|
email.setError();
|
|
try {
|
|
await inviteMember(value, null, companyId);
|
|
email.input.value = "";
|
|
showToast("안내 메일을 보냈음", "success");
|
|
} catch (error) {
|
|
showToast(
|
|
error instanceof Error ? error.message : L("B01_Dashboard_RequestFailed"),
|
|
"error",
|
|
);
|
|
}
|
|
},
|
|
});
|
|
const form = document.createElement("div");
|
|
form.className = "b01-pg-invite__form";
|
|
form.append(email.root, send);
|
|
const note = document.createElement("p");
|
|
note.className = "b01-pg-note";
|
|
note.textContent = "아직 가입하지 않은 사람에게 가입 안내 메일만 보냄 · 계정은 본인이 만듦";
|
|
const already = document.createElement("p");
|
|
already.className = "b01-pg-note";
|
|
already.append("이미 가입했고 소속이 없는 사람은 ");
|
|
const link = document.createElement("button");
|
|
link.type = "button";
|
|
link.className = "b01-pg-link";
|
|
link.textContent = "팀원 추가";
|
|
link.addEventListener("click", onAdd);
|
|
already.append(link, " 에서 바로 등록");
|
|
box.append(form, note, already);
|
|
return box;
|
|
}
|
|
|
|
async function loadPeople(ctx: ViewCtx): Promise<{ members: Member[]; requests: JoinRequest[] }> {
|
|
const system = ctx.role === "SYSTEM_ADMIN";
|
|
const [members, requests] = await Promise.all([
|
|
(system ? fetchAllUsers(ctx.companyId) : fetchCompanyMembers()).catch(() => [] as Member[]),
|
|
fetchCompanyJoinRequests(system ? ctx.companyId : undefined).catch(() => []),
|
|
]);
|
|
return { members: members as DashboardUser[], requests };
|
|
}
|
|
|
|
export async function mountPeople(host: HTMLElement, ctx: ViewCtx): Promise<void> {
|
|
const system = ctx.role === "SYSTEM_ADMIN";
|
|
const companyId = system ? ctx.companyId : undefined;
|
|
const page = pageFrame(host, "사람", "회사 팀원 · 가입 요청 · 초대를 한곳에서 관리");
|
|
const addMember = (): void => openAddMemberModal(undefined, companyId);
|
|
page.setAction(primaryButton("팀원 추가", addMember));
|
|
|
|
const { members, requests } = await loadPeople(ctx);
|
|
if (!host.isConnected) return;
|
|
|
|
const content = document.createElement("div");
|
|
const draw = (key: TabKey): void => {
|
|
if (key === "members") {
|
|
content.replaceChildren(
|
|
members.length
|
|
? dataTable(
|
|
MEMBER_COLUMNS,
|
|
members.map((m) => memberRow(m, ctx)),
|
|
)
|
|
: emptyState({
|
|
icon: "users",
|
|
text: "등록된 팀원이 없음",
|
|
action: primaryButton("팀원 추가", addMember),
|
|
onGuide: () => ctx.go("guide"),
|
|
}),
|
|
);
|
|
} else if (key === "requests") {
|
|
content.replaceChildren(
|
|
requests.length
|
|
? dataTable(
|
|
REQUEST_COLUMNS,
|
|
requests.map((r) => requestRow(r, system)),
|
|
)
|
|
: emptyState({ icon: "users", text: "대기 중인 가입 요청이 없음" }),
|
|
);
|
|
} else {
|
|
content.replaceChildren(invitePanel(companyId, addMember));
|
|
}
|
|
};
|
|
const tabs = tabBar(
|
|
[
|
|
{ key: "members", label: "팀원" },
|
|
{ key: "requests", label: "가입 요청" },
|
|
{ key: "invite", label: "초대" },
|
|
],
|
|
lastTab,
|
|
(key) => draw((lastTab = key as TabKey)),
|
|
);
|
|
tabs.setBadge("requests", pendingCount(requests));
|
|
page.body.append(tabs.root, content);
|
|
draw(lastTab);
|
|
}
|