diff --git a/frontend/src/components/CompanyOverview.jsx b/frontend/src/components/CompanyOverview.jsx new file mode 100644 index 0000000..55fba10 --- /dev/null +++ b/frontend/src/components/CompanyOverview.jsx @@ -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 ( +
+ +
+

{label}

+ {value} + {detail} +
+
+ ); +} + +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 ( +
+
+
+ +
+
+

{companyTitle(account, details, domainName)}

+ Business +
+

Manage your organisation, people and email settings.

+
+
+ +
+ +
+ + + + +
+ +
+
+
+

Domain health

+
+ {primaryDomain ? ( + <> + +
    + {dnsChecks.map((item) => ( +
  • + + + + {item.label} + {item.recommended && !item.verified && ( + Recommended + )} +
  • + ))} +
+
+ {lastChecked ? `Last checked ${formatRelativeTime(lastChecked)}` : "Not checked yet"} + +
+ + ) : ( +

No domains configured yet.

+ )} +
+ +
+
+

Recent activity

+
+ {activity.length === 0 ? ( +

No recent activity yet.

+ ) : ( +
    + {activity.map((item) => { + const Icon = item.icon; + return ( +
  • + +
    +

    {item.text}

    + +
    +
  • + ); + })} +
+ )} +
+ +
+
+

Admin shortcuts

+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+ ); +} diff --git a/frontend/src/components/CompanyOverviewSetup.jsx b/frontend/src/components/CompanyOverviewSetup.jsx index d3e9e50..b7a3c74 100644 --- a/frontend/src/components/CompanyOverviewSetup.jsx +++ b/frontend/src/components/CompanyOverviewSetup.jsx @@ -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 ( @@ -339,7 +351,7 @@ export default function CompanyOverviewSetup({ account, companyDomain, onComplet

Finish setup

- Domain {companyDomainName} is connected. You're ready to use AGMail. + Domain {companyDomainName} is connected. You're ready to use aGentic Email.

{!isOrgOnboarding && ( -
- - -
+ )}
@@ -446,7 +440,7 @@ export default function DashboardPage() { onOpenSettings={() => selectMailItem("Settings")} /> )} - {companyActive === "Overview" && isDomainOwner && ( + {companyActive === "Overview" && isDomainOwner && !isOrgSetupComplete(account.id) && ( )} + {companyActive === "Overview" && isDomainOwner && isOrgSetupComplete(account.id) && ( + + )} {companyActive === "Domains" && isDomainOwner && ( 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; }