Release v0.1.2
This commit is contained in:
@@ -1,22 +1,149 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Card from "../../components/Card";
|
||||
import DismissibleAlert from "../../components/DismissibleAlert";
|
||||
import LoadingFrame from "../../components/LoadingFrame";
|
||||
import MetricCard from "../../components/MetricCard";
|
||||
import StatusBadge from "../../components/StatusBadge";
|
||||
import { apiFetch, isApiError } from "../../api/client";
|
||||
import { usePlatformModules } from "../../platform/ModuleContext";
|
||||
import type { ApiSettings } from "../../types";
|
||||
|
||||
type DashboardCampaign = {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
external_id?: string | null;
|
||||
status?: string | null;
|
||||
updated_at?: string | null;
|
||||
updatedAt?: string | null;
|
||||
};
|
||||
|
||||
type CampaignListResponse = DashboardCampaign[] | { campaigns?: DashboardCampaign[]; items?: DashboardCampaign[]; results?: DashboardCampaign[] };
|
||||
|
||||
export default function DashboardPage({ settings }: { settings: ApiSettings }) {
|
||||
const modules = usePlatformModules();
|
||||
const campaignsInstalled = modules.some((module) => module.id === "campaigns");
|
||||
const [campaigns, setCampaigns] = useState<DashboardCampaign[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!campaignsInstalled) {
|
||||
setCampaigns([]);
|
||||
setError("");
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
apiFetch<CampaignListResponse>(settings, "/api/v1/campaigns")
|
||||
.then((response) => {
|
||||
if (!cancelled) setCampaigns(campaignsFromResponse(response));
|
||||
})
|
||||
.catch((reason: unknown) => {
|
||||
if (cancelled) return;
|
||||
if (isApiError(reason, 403, 404)) {
|
||||
setCampaigns([]);
|
||||
setError("");
|
||||
return;
|
||||
}
|
||||
setError(reason instanceof Error ? reason.message : String(reason));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [campaignsInstalled, settings]);
|
||||
|
||||
const statusCounts = useMemo(() => countByStatus(campaigns), [campaigns]);
|
||||
const recentCampaigns = useMemo(
|
||||
() => [...campaigns].sort((left, right) => timestamp(right) - timestamp(left)).slice(0, 5),
|
||||
[campaigns],
|
||||
);
|
||||
const activeCampaigns = (statusCounts.active ?? 0) + (statusCounts.draft ?? 0);
|
||||
const completedCampaigns = (statusCounts.completed ?? 0) + (statusCounts.sent ?? 0);
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="content-pad">
|
||||
<div className="page-heading">
|
||||
<h1>Dashboard</h1>
|
||||
<div className="content-pad workspace-data-page">
|
||||
<div className="page-heading split workspace-heading">
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>Tenant overview across installed modules and accessible campaigns.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <DismissibleAlert tone="warning" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||
|
||||
<div className="metric-grid">
|
||||
<MetricCard label="Campaigns" value="0" detail="Connect the API to load data" />
|
||||
<MetricCard label="Queued" value="0" tone="info" />
|
||||
<MetricCard label="Needs review" value="0" tone="warning" />
|
||||
<MetricCard label="Failed" value="0" tone="danger" />
|
||||
<MetricCard label="Installed modules" value={modules.length} tone="info" detail={modules.length ? moduleLabels(modules).join(", ") : "Core only"} />
|
||||
<MetricCard label="Campaigns" value={campaignsInstalled ? campaigns.length : "—"} tone="neutral" detail={campaignsInstalled ? "Accessible to you" : "Campaign module not installed"} />
|
||||
<MetricCard label="Active drafts" value={campaignsInstalled ? activeCampaigns : "—"} tone="warning" detail="Draft or active" />
|
||||
<MetricCard label="Completed" value={campaignsInstalled ? completedCampaigns : "—"} tone="good" detail="Completed or sent" />
|
||||
</div>
|
||||
|
||||
<div className="dashboard-grid">
|
||||
<Card title="Recommended next action"><p className="muted">Create or open a campaign to continue.</p></Card>
|
||||
<Card title="System status"><p className="muted">API health and queue metrics will appear here.</p></Card>
|
||||
<Card title="Recent campaigns" collapsible>
|
||||
<LoadingFrame loading={loading} label="Loading campaigns...">
|
||||
{!campaignsInstalled && <p className="muted">Install the Campaign module to show campaign activity here.</p>}
|
||||
{campaignsInstalled && recentCampaigns.length === 0 && <p className="muted">No accessible campaigns found.</p>}
|
||||
{recentCampaigns.length > 0 && (
|
||||
<dl className="detail-list">
|
||||
{recentCampaigns.map((campaign) => (
|
||||
<div key={campaign.id}>
|
||||
<dt><StatusBadge status={campaign.status || "draft"} /></dt>
|
||||
<dd>
|
||||
<strong>{campaign.name || campaign.external_id || campaign.id}</strong>
|
||||
<span className="muted"> · {formatDashboardDate(campaign.updated_at ?? campaign.updatedAt)}</span>
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
</LoadingFrame>
|
||||
</Card>
|
||||
|
||||
<Card title="Installed modules" collapsible>
|
||||
{modules.length === 0 ? <p className="muted">No feature modules are active.</p> : (
|
||||
<dl className="detail-list">
|
||||
{modules.map((module) => (
|
||||
<div key={module.id}>
|
||||
<dt>{module.id}</dt>
|
||||
<dd><strong>{module.label}</strong><span className="muted"> · v{module.version}</span></dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function campaignsFromResponse(response: CampaignListResponse): DashboardCampaign[] {
|
||||
if (Array.isArray(response)) return response;
|
||||
return response.campaigns ?? response.items ?? response.results ?? [];
|
||||
}
|
||||
|
||||
function countByStatus(campaigns: DashboardCampaign[]): Record<string, number> {
|
||||
return campaigns.reduce<Record<string, number>>((counts, campaign) => {
|
||||
const status = String(campaign.status || "draft").toLowerCase();
|
||||
counts[status] = (counts[status] ?? 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function timestamp(campaign: DashboardCampaign): number {
|
||||
const value = campaign.updated_at ?? campaign.updatedAt ?? "";
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function formatDashboardDate(value?: string | null): string {
|
||||
if (!value) return "not updated";
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) return value;
|
||||
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(parsed);
|
||||
}
|
||||
|
||||
function moduleLabels(modules: Array<{ label: string }>): string[] {
|
||||
return modules.map((module) => module.label).slice(0, 4);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user