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>
);
}

View file

@ -8,7 +8,7 @@ const STEPS = [
{ id: 3, title: "Configure email DNS", hint: "Set up MX, SPF, DKIM" },
{ id: 4, title: "Company details", hint: "Tell us about your organization" },
{ id: 5, title: "Add employees", hint: "Create and invite your team" },
{ id: 6, title: "Finish setup", hint: "Start using AGMail" },
{ id: 6, title: "Finish setup", hint: "Start using aGentic Email" },
];
const ORG_SIZES = [
@ -98,6 +98,18 @@ export function isOrgSetupComplete(accountId) {
return localStorage.getItem(storageKey(accountId, "complete")) === "1";
}
export function getOrgDetails(accountId) {
try {
const raw = localStorage.getItem(storageKey(accountId, "details"));
if (raw) {
return JSON.parse(raw);
}
} catch {
/* ignore */
}
return null;
}
function RequiredLabel({ children }) {
return (
<span className="org-setup-label-text">
@ -339,7 +351,7 @@ export default function CompanyOverviewSetup({ account, companyDomain, onComplet
<div className="org-setup-form-shell">
<h2>Finish setup</h2>
<p className="org-setup-lead">
Domain <strong>{companyDomainName}</strong> is connected. You&apos;re ready to use AGMail.
Domain <strong>{companyDomainName}</strong> is connected. You&apos;re ready to use aGentic Email.
</p>
<ul className="org-setup-summary">
<li><Check size={14} /> Domain added and verified</li>

View file

@ -34,6 +34,7 @@ import CompanyAliases from "./CompanyAliases";
import CompanyGroups from "./CompanyGroups";
import CompanyPeople from "./CompanyPeople";
import CompanySecurity from "./CompanySecurity";
import CompanyOverview from "./CompanyOverview";
import CompanyOverviewSetup, { isOrgSetupComplete } from "./CompanyOverviewSetup";
import ChangePasswordModal from "./ChangePasswordModal";
import ComposeModal from "./ComposeModal";
@ -278,17 +279,10 @@ export default function DashboardPage() {
</div>
{!isOrgOnboarding && (
<div className="dashboard-compose-wrap">
<button className="dashboard-compose" type="button" onClick={() => setComposeOpen(true)}>
<span className="dashboard-compose-main">
<Mail size={16} strokeWidth={1.75} />
<span>Compose</span>
</span>
</button>
<button className="dashboard-compose-menu" type="button" aria-label="Compose options">
<ChevronDown size={14} strokeWidth={1.75} />
</button>
</div>
<button className="dashboard-compose" type="button" onClick={() => setComposeOpen(true)}>
<Mail size={16} strokeWidth={1.75} />
<span>Compose</span>
</button>
)}
<div className="dashboard-sidebar-scroll">
@ -446,7 +440,7 @@ export default function DashboardPage() {
onOpenSettings={() => selectMailItem("Settings")}
/>
)}
{companyActive === "Overview" && isDomainOwner && (
{companyActive === "Overview" && isDomainOwner && !isOrgSetupComplete(account.id) && (
<CompanyOverviewSetup
account={account}
companyDomain={companyDomain}
@ -456,6 +450,13 @@ export default function DashboardPage() {
}}
/>
)}
{companyActive === "Overview" && isDomainOwner && isOrgSetupComplete(account.id) && (
<CompanyOverview
account={account}
companyDomain={companyDomain}
onNavigate={selectCompanyItem}
/>
)}
{companyActive === "Domains" && isDomainOwner && (
<CompanyDomains
account={account}

View file

@ -126,9 +126,6 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.dashboard-sidebar { height:100vh; padding:29px 20px 19px; position:sticky; top:0; display:flex; flex-direction:column; border-right:1px solid #e6e2de; background:#fffefd; box-shadow:4px 0 28px #0808210c; z-index:12; }
.dashboard-brand-row { padding:0 9px; display:flex; align-items:center; justify-content:space-between; }
.dashboard-brand-row>button { display:none; }
.dashboard-compose { min-height:45px; margin:35px 3px 29px; display:flex; align-items:center; justify-content:center; gap:9px; border:0; border-radius:11px; background:var(--ink); color:white; font-weight:700; box-shadow:0 6px 18px #08082128; transition:transform .18s ease,box-shadow .18s ease,background .18s ease; }
.dashboard-compose:hover { transform:scale(1.02); box-shadow:0 10px 26px #08082136; background:#12102a; }
.dashboard-compose:active { transform:scale(0.99); box-shadow:0 4px 12px #08082122; }
.dashboard-nav-group { margin-bottom:24px; }
.dashboard-nav-group h2 { margin:0 10px 8px; display:flex; align-items:center; gap:8px; color:#97929a; font-size:10px; letter-spacing:1.2px; text-transform:uppercase; }
.dashboard-nav-group nav { display:grid; gap:3px; }
@ -234,6 +231,58 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.employee-added-list span { color:#6f6c7b; }
.employee-added-list em { color:#7357F6; font-size:11px; font-weight:700; font-style:normal; text-transform:uppercase; letter-spacing:.3px; }
.org-setup-step-actions { margin-top:8px; }
.company-overview { padding:8px 0 24px; display:grid; gap:24px; }
.company-overview-head { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; }
.company-overview-title { display:flex; align-items:flex-start; gap:16px; }
.company-overview-logo { width:52px; height:52px; display:grid; place-items:center; border-radius:50%; background:#ffe0d8; color:#bd4e3b; font-size:22px; font-weight:700; flex-shrink:0; }
.company-overview-name-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.company-overview-head h1 { margin:0; font:700 clamp(28px,3vw,36px)/1.15 "DM Sans"; letter-spacing:-.6px; color:#1a1628; }
.company-overview-head p { margin:6px 0 0; color:#77758A; font-size:14px; }
.company-overview-badge { padding:4px 10px; border-radius:999px; background:#f3f1f7; color:#6f6c7b; font-size:11px; font-weight:700; }
.company-overview-subscription { display:inline-flex; align-items:center; gap:8px; padding:10px 14px; border:1px solid #E1DFE7; border-radius:10px; background:#fff; color:#4a4652; font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; }
.company-overview-stats { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:16px; }
.company-overview-stat { padding:18px 18px 16px; display:grid; grid-template-columns:auto 1fr; gap:14px; align-items:start; border:1px solid rgba(20,20,30,0.06); border-radius:16px; background:#fff; box-shadow:0 1px 2px rgba(20,18,30,0.03); }
.company-overview-stat-icon { width:38px; height:38px; display:grid; place-items:center; border-radius:12px; }
.company-overview-stat.tone-coral .company-overview-stat-icon { background:#ffe8e2; color:#c85e49; }
.company-overview-stat.tone-violet .company-overview-stat-icon { background:#f1ecff; color:#6f56d8; }
.company-overview-stat.tone-green .company-overview-stat-icon { background:#e8f7ef; color:#2f8a57; }
.company-overview-stat.tone-amber .company-overview-stat-icon { background:#fff4df; color:#b7892d; }
.company-overview-stat p { margin:0; color:#8d8792; font-size:12px; font-weight:600; }
.company-overview-stat strong { display:block; margin-top:4px; font-size:30px; line-height:1; color:#1a1628; letter-spacing:-.5px; }
.company-overview-stat small { display:block; margin-top:6px; color:#9b96a3; font-size:11px; }
.company-overview-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:16px; }
.company-overview-panel { padding:20px; border:1px solid rgba(20,20,30,0.06); border-radius:16px; background:#fff; box-shadow:0 1px 2px rgba(20,18,30,0.03); }
.company-overview-panel-head h2 { margin:0 0 16px; font-size:15px; font-weight:700; color:#1a1628; }
.company-overview-domain-row { width:100%; margin-bottom:14px; padding:12px 14px; display:flex; align-items:center; justify-content:space-between; border:1px solid #eee9e5; border-radius:12px; background:#faf8f6; cursor:pointer; }
.company-overview-domain-row strong { display:block; font-size:14px; color:#1a1628; }
.company-overview-domain-row em { display:inline-block; margin-top:4px; padding:2px 8px; border-radius:999px; background:#e8f7ef; color:#2f8a57; font-size:10px; font-style:normal; font-weight:700; }
.company-overview-health-list { margin:0; padding:0; list-style:none; display:grid; gap:10px; }
.company-overview-health-list li { display:flex; align-items:center; gap:10px; font-size:13px; color:#4a4652; }
.company-overview-health-icon { width:24px; height:24px; display:grid; place-items:center; border-radius:50%; background:#f2f0f5; color:#b4afba; }
.company-overview-health-icon.ok { background:#e8f7ef; color:#2f8a57; }
.company-overview-health-icon.warn { background:#fff4df; color:#b7892d; }
.company-overview-health-badge { margin-left:auto; padding:3px 8px; border-radius:999px; background:#fff4df; color:#b7892d; font-size:10px; font-style:normal; font-weight:700; }
.company-overview-panel-footer { margin-top:16px; padding-top:14px; border-top:1px solid #f0ebe7; display:flex; align-items:center; justify-content:space-between; gap:12px; color:#9b96a3; font-size:12px; }
.company-overview-panel-footer button { display:inline-flex; align-items:center; gap:6px; border:0; background:transparent; color:#4d74c4; font-size:12px; font-weight:600; cursor:pointer; }
.company-overview-activity { margin:0; padding:0; list-style:none; display:grid; gap:14px; }
.company-overview-activity li { display:grid; grid-template-columns:auto 1fr; gap:12px; align-items:start; }
.company-overview-activity-icon { width:30px; height:30px; display:grid; place-items:center; border-radius:50%; background:#f3f1f7; color:#6f6c7b; }
.company-overview-activity p { margin:0; font-size:13px; color:#3d3948; line-height:1.45; }
.company-overview-activity time { display:block; margin-top:4px; color:#a8a4ae; font-size:11px; }
.company-overview-shortcuts { margin:0; padding:0; list-style:none; display:grid; gap:8px; }
.company-overview-shortcuts button { width:100%; padding:12px 14px; display:flex; align-items:center; gap:10px; border:1px solid #eee9e5; border-radius:12px; background:#fff; color:#4a4652; font-size:13px; font-weight:600; text-align:left; cursor:pointer; }
.company-overview-shortcuts button:disabled { opacity:.55; cursor:not-allowed; }
.company-overview-empty { margin:0; color:#9b96a3; font-size:13px; }
@media (max-width: 1100px) {
.company-overview-stats { grid-template-columns:repeat(2,minmax(0,1fr)); }
.company-overview-grid { grid-template-columns:1fr; }
}
@media (max-width: 720px) {
.company-overview-head { flex-direction:column; }
.company-overview-stats { grid-template-columns:1fr; }
}
.dashboard-sidebar-manage { margin-top:var(--space-4); }
@media(max-width:900px) {
.org-setup-head h1 { font-size:28px; }
@ -677,12 +726,9 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.dashboard-sidebar .brand-mark { background:#fff; }
.dashboard-sidebar .robot-eyes i { background:#1c1730; }
.dashboard-sidebar .robot-smile { border-color:#1c1730; }
.dashboard-compose-wrap { flex-shrink:0; margin:var(--space-4) var(--space-1) 0; display:flex; border-radius:var(--radius-md); overflow:hidden; box-shadow:var(--shadow-accent); }
.dashboard-compose { flex:1; min-height:40px; padding:0 var(--space-3); display:flex; align-items:center; border:0; color:#fff; background:linear-gradient(180deg,#ff806d 0%,#ff5c55 100%); font-size:var(--text-meta); font-weight:700; transition:filter var(--motion-fast) var(--ease-standard),transform var(--motion-fast) var(--ease-standard); }
.dashboard-compose-main { display:flex; align-items:center; gap:var(--space-2); }
.dashboard-compose-menu { width:38px; min-height:40px; display:grid; place-items:center; border:0; border-left:1px solid rgba(255,255,255,0.22); background:linear-gradient(180deg,#ff7860 0%,#ff554e 100%); color:#fff; }
.dashboard-compose-wrap:hover .dashboard-compose,.dashboard-compose-wrap:hover .dashboard-compose-menu { filter:brightness(1.03); }
.dashboard-compose-wrap:active .dashboard-compose { transform:scale(0.99); }
.dashboard-compose { flex-shrink:0; width:calc(100% - 8px); margin:var(--space-4) var(--space-1) 0; min-height:40px; padding:0 var(--space-4); display:flex; align-items:center; justify-content:center; gap:var(--space-2); border:1px solid rgba(255,255,255,0.14); border-radius:var(--radius-md); color:#fff; background:linear-gradient(180deg,#ff806d 0%,#ff5c55 100%); font-size:var(--text-meta); font-weight:700; box-shadow:0 4px 14px rgba(255,88,72,0.24); cursor:pointer; transition:filter var(--motion-fast) var(--ease-standard),transform var(--motion-fast) var(--ease-standard),box-shadow var(--motion-fast) var(--ease-standard); }
.dashboard-compose:hover { filter:brightness(1.04); box-shadow:0 6px 18px rgba(255,88,72,0.32); }
.dashboard-compose:active { transform:scale(0.99); box-shadow:0 2px 8px rgba(255,88,72,0.2); }
.dashboard-sidebar-section { margin-top:var(--space-5); padding-top:var(--space-4); border-top:1px solid #ffffff0a; }
.dashboard-sidebar-section h2 { margin:0 0 var(--space-2); padding:0 var(--space-2); color:#8a84a0; font-size:10px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
.dashboard-sidebar-section nav { display:grid; gap:2px; }