agentic email invite html

This commit is contained in:
debu-2 2026-10-04 21:27:09 +05:30
parent 65bb73f30b
commit 04a033e099
4 changed files with 403 additions and 23 deletions

View file

@ -0,0 +1,321 @@
import {
ArrowRight,
CheckCircle2,
ChevronRight,
CreditCard,
Database,
FileUp,
Globe,
RefreshCw,
Shield,
UserPlus,
Users,
UsersRound,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
listMailGroups,
listMyDomains,
listOrgMembers,
verifyMyDomainDns,
} from "../api";
import { getOrgDetails } from "./CompanyOverviewSetup";
const HEALTH_ITEMS = [
{ purpose: "ownership", label: "Domain verified" },
{ purpose: "mx", label: "MX records" },
{ purpose: "spf", label: "SPF" },
{ purpose: "dkim", label: "DKIM" },
{ purpose: "dmarc", label: "DMARC" },
];
function formatRelativeTime(dateValue) {
const date = new Date(dateValue);
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 3600) return `${Math.max(1, Math.floor(seconds / 60))}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
const days = Math.floor(seconds / 86400);
if (days < 7) return `${days}d ago`;
return `${Math.floor(days / 7)}w ago`;
}
function companyTitle(account, details, domainName) {
if (details?.companyName?.trim()) return details.companyName.trim();
if (domainName) {
const base = domainName.split(".")[0];
return base.charAt(0).toUpperCase() + base.slice(1);
}
return `${account.first_name} ${account.last_name}`.trim();
}
function StatCard({ icon: Icon, tone, label, value, detail }) {
return (
<article className={`company-overview-stat tone-${tone}`}>
<span className="company-overview-stat-icon" aria-hidden="true">
<Icon size={18} />
</span>
<div>
<p>{label}</p>
<strong>{value}</strong>
<small>{detail}</small>
</div>
</article>
);
}
export default function CompanyOverview({ account, companyDomain, onNavigate }) {
const token = sessionStorage.getItem("agmail_access_token");
const domainName = companyDomain?.domain_name || account.company_domain || "";
const details = getOrgDetails(account.id);
const [loading, setLoading] = useState(true);
const [members, setMembers] = useState({ items: [], counts: {} });
const [groups, setGroups] = useState({ items: [], counts: {} });
const [domains, setDomains] = useState([]);
const [dnsChecks, setDnsChecks] = useState([]);
const [lastChecked, setLastChecked] = useState(null);
const [dnsLoading, setDnsLoading] = useState(false);
const loadOverview = useCallback(async () => {
setLoading(true);
try {
const [memberData, groupData, domainData] = await Promise.all([
listOrgMembers(token),
listMailGroups(token),
listMyDomains(token),
]);
setMembers(memberData);
setGroups(groupData);
setDomains(domainData);
} finally {
setLoading(false);
}
}, [token]);
const loadDnsHealth = useCallback(async () => {
const primary = domains.find((domain) => domain.domain_name === domainName) || domains[0];
if (!primary) {
setDnsChecks([]);
return;
}
if (primary.status === "active") {
setDnsChecks(HEALTH_ITEMS.map((item) => ({
...item,
verified: item.purpose !== "dmarc",
recommended: item.purpose === "dmarc",
})));
setLastChecked(new Date());
return;
}
setDnsLoading(true);
try {
const result = await verifyMyDomainDns(primary.id, token);
const checkMap = Object.fromEntries(result.checks.map((check) => [check.purpose, check]));
setDnsChecks(HEALTH_ITEMS.map((item) => ({
...item,
verified: checkMap[item.purpose]?.verified ?? false,
recommended: item.purpose === "dmarc" && !checkMap[item.purpose]?.verified,
})));
setLastChecked(new Date());
} catch {
setDnsChecks(HEALTH_ITEMS.map((item) => ({
...item,
verified: false,
recommended: item.purpose === "dmarc",
})));
} finally {
setDnsLoading(false);
}
}, [domainName, domains, token]);
useEffect(() => {
loadOverview();
}, [loadOverview]);
useEffect(() => {
if (!loading) {
loadDnsHealth();
}
}, [loadDnsHealth, loading]);
const activity = useMemo(() => {
const events = [
...members.items.map((member) => ({
id: `member-${member.id}`,
icon: Users,
text: `${member.first_name} ${member.last_name} joined the organization`,
time: member.created_at,
})),
...groups.items.map((group) => ({
id: `group-${group.id}`,
icon: UsersRound,
text: `${group.name} group created`,
time: group.created_at,
})),
];
return events
.sort((left, right) => new Date(right.time) - new Date(left.time))
.slice(0, 5);
}, [groups.items, members.items]);
const primaryDomain = domains.find((domain) => domain.domain_name === domainName) || domains[0];
const secondaryCount = Math.max(0, domains.length - 1);
const distributionCount = groups.items.filter((group) => group.group_type === "distribution").length;
const dynamicCount = groups.items.filter((group) => group.group_type === "dynamic").length;
return (
<section className="company-overview">
<header className="company-overview-head">
<div className="company-overview-title">
<span className="company-overview-logo" aria-hidden="true">
{companyTitle(account, details, domainName).charAt(0).toUpperCase()}
</span>
<div>
<div className="company-overview-name-row">
<h1>{companyTitle(account, details, domainName)}</h1>
<span className="company-overview-badge">Business</span>
</div>
<p>Manage your organisation, people and email settings.</p>
</div>
</div>
<button type="button" className="company-overview-subscription" onClick={() => onNavigate?.("Billing")}>
Manage subscription <ArrowRight size={15} />
</button>
</header>
<div className="company-overview-stats">
<StatCard
icon={Users}
tone="coral"
label="Total users"
value={loading ? "—" : members.counts.all ?? members.items.length}
detail={loading ? "Loading…" : `${members.counts.active ?? 0} active • ${members.counts.invited ?? 0} pending`}
/>
<StatCard
icon={UsersRound}
tone="violet"
label="Groups"
value={loading ? "—" : groups.items.length}
detail={loading ? "Loading…" : `${distributionCount} distribution • ${dynamicCount} dynamic`}
/>
<StatCard
icon={Globe}
tone="green"
label="Domains"
value={loading ? "—" : domains.length}
detail={loading ? "Loading…" : `1 primary • ${secondaryCount} secondary`}
/>
<StatCard
icon={Database}
tone="amber"
label="Total storage"
value="—"
detail="of 100 GB"
/>
</div>
<div className="company-overview-grid">
<section className="company-overview-panel">
<header className="company-overview-panel-head">
<h2>Domain health</h2>
</header>
{primaryDomain ? (
<>
<button
type="button"
className="company-overview-domain-row"
onClick={() => onNavigate?.("Domains")}
>
<span>
<strong>{primaryDomain.domain_name}</strong>
<em>Primary</em>
</span>
<ChevronRight size={16} />
</button>
<ul className="company-overview-health-list">
{dnsChecks.map((item) => (
<li key={item.purpose}>
<span className={`company-overview-health-icon${item.verified ? " ok" : item.recommended ? " warn" : ""}`}>
<CheckCircle2 size={15} />
</span>
<span>{item.label}</span>
{item.recommended && !item.verified && (
<em className="company-overview-health-badge">Recommended</em>
)}
</li>
))}
</ul>
<footer className="company-overview-panel-footer">
<span>{lastChecked ? `Last checked ${formatRelativeTime(lastChecked)}` : "Not checked yet"}</span>
<button type="button" onClick={loadDnsHealth} disabled={dnsLoading}>
<RefreshCw size={14} />
{dnsLoading ? "Checking…" : "Recheck"}
</button>
</footer>
</>
) : (
<p className="company-overview-empty">No domains configured yet.</p>
)}
</section>
<section className="company-overview-panel">
<header className="company-overview-panel-head">
<h2>Recent activity</h2>
</header>
{activity.length === 0 ? (
<p className="company-overview-empty">No recent activity yet.</p>
) : (
<ul className="company-overview-activity">
{activity.map((item) => {
const Icon = item.icon;
return (
<li key={item.id}>
<span className="company-overview-activity-icon" aria-hidden="true">
<Icon size={15} />
</span>
<div>
<p>{item.text}</p>
<time>{formatRelativeTime(item.time)}</time>
</div>
</li>
);
})}
</ul>
)}
</section>
<section className="company-overview-panel">
<header className="company-overview-panel-head">
<h2>Admin shortcuts</h2>
</header>
<ul className="company-overview-shortcuts">
<li>
<button type="button" onClick={() => onNavigate?.("People")}>
<UserPlus size={16} /> Add user
</button>
</li>
<li>
<button type="button" disabled title="Coming soon">
<FileUp size={16} /> Invite users via CSV
</button>
</li>
<li>
<button type="button" onClick={() => onNavigate?.("Groups")}>
<UsersRound size={16} /> Create group
</button>
</li>
<li>
<button type="button" onClick={() => onNavigate?.("Security")}>
<Shield size={16} /> Security settings
</button>
</li>
<li>
<button type="button" onClick={() => onNavigate?.("Billing")}>
<CreditCard size={16} /> Manage billing
</button>
</li>
</ul>
</section>
</div>
</section>
);
}