297 lines
9.3 KiB
JavaScript
297 lines
9.3 KiB
JavaScript
import {
|
|
CheckCircle2,
|
|
Clock3,
|
|
MoreHorizontal,
|
|
Plus,
|
|
Search,
|
|
UserPlus,
|
|
Upload,
|
|
X,
|
|
XCircle,
|
|
} from "lucide-react";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { listOrgMembers } from "../api";
|
|
import AddEmployeeForm from "./AddEmployeeForm";
|
|
import ImportUsersPanel from "./ImportUsersPanel";
|
|
|
|
const STATUS_TABS = [
|
|
{ key: "all", label: "All users" },
|
|
{ key: "active", label: "Active" },
|
|
{ key: "invited", label: "Invited" },
|
|
{ key: "suspended", label: "Suspended" },
|
|
];
|
|
|
|
function initials(firstName, lastName) {
|
|
return `${firstName?.[0] || ""}${lastName?.[0] || ""}`.toUpperCase();
|
|
}
|
|
|
|
function roleLabel(member) {
|
|
if (member.is_domain_owner || member.org_role === "owner") return "Owner";
|
|
if (member.org_role === "admin") return "User Admin";
|
|
return "User";
|
|
}
|
|
|
|
function sectionLabel(member) {
|
|
return member.section_name || "—";
|
|
}
|
|
|
|
function StatusBadge({ status }) {
|
|
if (status === "active") {
|
|
return (
|
|
<span className="people-status people-status-active">
|
|
<CheckCircle2 size={13} /> Active
|
|
</span>
|
|
);
|
|
}
|
|
if (status === "invited") {
|
|
return (
|
|
<span className="people-status people-status-invited">
|
|
<Clock3 size={13} /> Invited
|
|
</span>
|
|
);
|
|
}
|
|
return (
|
|
<span className="people-status people-status-suspended">
|
|
<XCircle size={13} /> Suspended
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function AddUserPanel({ companyDomain, onClose, onCreated }) {
|
|
return (
|
|
<div className="people-drawer-scrim" onClick={onClose}>
|
|
<aside
|
|
className="people-drawer"
|
|
role="dialog"
|
|
aria-labelledby="add-user-title"
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<header className="people-drawer-head">
|
|
<div className="people-drawer-title">
|
|
<span className="people-drawer-title-icon" aria-hidden="true">
|
|
<UserPlus size={18} />
|
|
</span>
|
|
<h2 id="add-user-title">Add user</h2>
|
|
</div>
|
|
<button type="button" className="people-drawer-close" aria-label="Close" onClick={onClose}>
|
|
<X size={18} />
|
|
</button>
|
|
</header>
|
|
|
|
<AddEmployeeForm
|
|
companyDomain={companyDomain}
|
|
onCreated={(member) => {
|
|
onCreated?.(member);
|
|
onClose();
|
|
}}
|
|
onCancel={onClose}
|
|
/>
|
|
</aside>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function CompanyPeople({ account }) {
|
|
const token = sessionStorage.getItem("agmail_access_token");
|
|
const companyDomain = account.company_domain;
|
|
const [members, setMembers] = useState([]);
|
|
const [counts, setCounts] = useState({ all: 0, active: 0, invited: 0, suspended: 0 });
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
const [statusTab, setStatusTab] = useState("all");
|
|
const [search, setSearch] = useState("");
|
|
const [roleFilter, setRoleFilter] = useState("all");
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
const [importOpen, setImportOpen] = useState(false);
|
|
const [notice, setNotice] = useState("");
|
|
|
|
const loadMembers = useCallback(async () => {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
const data = await listOrgMembers(token);
|
|
setMembers(data.items);
|
|
setCounts(data.counts);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [token]);
|
|
|
|
useEffect(() => {
|
|
loadMembers();
|
|
}, [loadMembers]);
|
|
|
|
useEffect(() => {
|
|
if (sessionStorage.getItem("agmail_open_people_import") === "1") {
|
|
sessionStorage.removeItem("agmail_open_people_import");
|
|
setImportOpen(true);
|
|
}
|
|
}, []);
|
|
|
|
const filteredMembers = useMemo(() => {
|
|
const query = search.trim().toLowerCase();
|
|
return members.filter((member) => {
|
|
if (statusTab !== "all" && member.member_status !== statusTab) {
|
|
return false;
|
|
}
|
|
if (roleFilter === "admin" && member.org_role !== "admin" && !member.is_domain_owner) {
|
|
return false;
|
|
}
|
|
if (roleFilter === "user" && (member.org_role !== "user" || member.is_domain_owner)) {
|
|
return false;
|
|
}
|
|
if (!query) return true;
|
|
const haystack = `${member.first_name} ${member.last_name} ${member.email}`.toLowerCase();
|
|
return haystack.includes(query);
|
|
});
|
|
}, [members, roleFilter, search, statusTab]);
|
|
|
|
return (
|
|
<section className="company-people">
|
|
<header className="company-people-head">
|
|
<div>
|
|
<h1>People</h1>
|
|
<p>Manage your team members and their email accounts.</p>
|
|
</div>
|
|
<div className="company-people-actions">
|
|
<button
|
|
type="button"
|
|
className="button button-outline people-import-btn"
|
|
onClick={() => setImportOpen(true)}
|
|
>
|
|
<Upload size={16} /> Import CSV
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="button button-purple"
|
|
onClick={() => setDrawerOpen(true)}
|
|
>
|
|
<Plus size={16} /> Add user
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="people-tabs" role="tablist" aria-label="Filter by status">
|
|
{STATUS_TABS.map((tab) => (
|
|
<button
|
|
key={tab.key}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={statusTab === tab.key}
|
|
className={statusTab === tab.key ? "active" : ""}
|
|
onClick={() => setStatusTab(tab.key)}
|
|
>
|
|
{tab.label} <em>{counts[tab.key] ?? 0}</em>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="people-filters">
|
|
<label className="people-search">
|
|
<Search size={16} />
|
|
<input
|
|
value={search}
|
|
onChange={(event) => setSearch(event.target.value)}
|
|
placeholder="Search people…"
|
|
/>
|
|
</label>
|
|
<select value={roleFilter} onChange={(event) => setRoleFilter(event.target.value)}>
|
|
<option value="all">All roles</option>
|
|
<option value="admin">Admins</option>
|
|
<option value="user">Users</option>
|
|
</select>
|
|
</div>
|
|
|
|
{notice && <p className="people-notice" role="status">{notice}</p>}
|
|
{error && <p className="form-error" role="alert">{error}</p>}
|
|
|
|
<div className="people-table-wrap">
|
|
<table className="people-table">
|
|
<thead>
|
|
<tr>
|
|
<th aria-label="Select" />
|
|
<th>Name</th>
|
|
<th>Email</th>
|
|
<th>Status</th>
|
|
<th>Role</th>
|
|
<th>Section</th>
|
|
<th>Storage</th>
|
|
<th>Last active</th>
|
|
<th aria-label="Actions" />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{loading ? (
|
|
<tr>
|
|
<td colSpan={9} className="people-table-empty">Loading people…</td>
|
|
</tr>
|
|
) : filteredMembers.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={9} className="people-table-empty">
|
|
{members.length === 0
|
|
? "No team members yet. Add your first user to get started."
|
|
: "No people match your filters."}
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
filteredMembers.map((member) => (
|
|
<tr key={member.id}>
|
|
<td><input type="checkbox" aria-label={`Select ${member.first_name}`} /></td>
|
|
<td>
|
|
<div className="people-name-cell">
|
|
<span className="people-avatar">{initials(member.first_name, member.last_name)}</span>
|
|
<span>{member.first_name} {member.last_name}</span>
|
|
</div>
|
|
</td>
|
|
<td>{member.email}</td>
|
|
<td><StatusBadge status={member.member_status} /></td>
|
|
<td>{roleLabel(member)}</td>
|
|
<td>{sectionLabel(member)}</td>
|
|
<td>{member.member_status === "invited" ? "—" : "—"}</td>
|
|
<td>{member.member_status === "invited" ? "—" : "—"}</td>
|
|
<td>
|
|
<button type="button" className="people-row-menu" aria-label="More actions">
|
|
<MoreHorizontal size={16} />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{importOpen && (
|
|
<ImportUsersPanel
|
|
companyDomain={companyDomain}
|
|
onClose={() => setImportOpen(false)}
|
|
onImported={(result) => {
|
|
loadMembers();
|
|
setNotice(`Imported ${result.created_count} users${result.error_count ? ` (${result.error_count} failed)` : ""}.`);
|
|
window.setTimeout(() => setNotice(""), 6000);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{drawerOpen && (
|
|
<AddUserPanel
|
|
companyDomain={companyDomain}
|
|
onClose={() => setDrawerOpen(false)}
|
|
onCreated={(member) => {
|
|
loadMembers();
|
|
if (member.invitation_sent) {
|
|
setNotice(`Invitation email sent to ${member.invitation_email}.`);
|
|
} else if (member.member_status === "invited") {
|
|
setNotice("User created. Invitation email was not sent.");
|
|
} else {
|
|
setNotice("User created.");
|
|
}
|
|
window.setTimeout(() => setNotice(""), 5000);
|
|
}}
|
|
/>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|