/* ============================================================================= * 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 { 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); }