import { useCallback, useEffect, useMemo, useState } from 'react';
import GridLayout from 'react-grid-layout';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { AnalyticsApi, LayoutApi, ExportApi, SyncApi } from '../api/client.js';
import SyncSplash from '../components/SyncSplash.jsx';
import {
HealthWidget, StatCard, TopSendersWidget, VolumeWidget,
CategoryBreakdownWidget, AttachmentsWidget, StorageWidget
} from '../components/widgets.jsx';
import { Skeleton } from '../components/ui/skeleton.jsx';
// Default grid geometry; overridden by the user's saved layout.
const DEFAULT_LAYOUT = [
{ i: 'inbox-health', x: 0, y: 0, w: 3, h: 5 },
{ i: 'total-emails', x: 3, y: 0, w: 2, h: 2 },
{ i: 'unread-emails', x: 5, y: 0, w: 2, h: 2 },
{ i: 'storage', x: 7, y: 0, w: 2, h: 2 },
{ i: 'top-senders', x: 3, y: 2, w: 3, h: 5 },
{ i: 'volume', x: 6, y: 2, w: 6, h: 4 },
{ i: 'category-breakdown', x: 0, y: 5, w: 6, h: 5 },
{ i: 'attachments', x: 6, y: 6, w: 4, h: 4 },
];
const ALL_WIDGETS = DEFAULT_LAYOUT.map((l) => l.i);
function WidgetSkeleton() {
return (
);
}
export default function Dashboard() {
const [data, setData] = useState(null);
const [layout, setLayout] = useState(DEFAULT_LAYOUT);
const [hidden, setHidden] = useState([]);
const [syncing, setSyncing] = useState(false);
const loadData = useCallback(() => {
AnalyticsApi.dashboard().then(setData).catch(() => {});
}, []);
useEffect(() => {
LayoutApi.get().then((saved) => {
if (saved?.length) {
const savedLayout = saved.map((w) => ({ i: w.widgetKey, x: w.x, y: w.y, w: w.w, h: w.h }));
const savedKeys = new Set(savedLayout.map((l) => l.i));
// Append any newer default widgets the saved layout doesn't know about yet.
const merged = [...savedLayout, ...DEFAULT_LAYOUT.filter((d) => !savedKeys.has(d.i))];
setLayout(merged);
setHidden(saved.filter((w) => !w.visible).map((w) => w.widgetKey));
}
}).catch(() => {});
// Decide whether to show the loading splash: if a sync is running, watch it;
// if the inbox has never been synced, kick one off automatically.
SyncApi.status().then((s) => {
if (s.isRunning) {
setSyncing(true);
} else if (!s.lastSuccessfulSyncUtc) {
SyncApi.incremental().then(() => setSyncing(true)).catch(loadData);
} else {
loadData();
}
}).catch(loadData);
}, [loadData]);
// Show the splash when a sync is triggered from the header "Sync now" button.
useEffect(() => {
const onStart = () => setSyncing(true);
window.addEventListener('inboxintel:sync-started', onStart);
return () => window.removeEventListener('inboxintel:sync-started', onStart);
}, []);
const onSyncDone = useCallback(() => {
setSyncing(false);
loadData();
}, [loadData]);
const persist = (nextLayout, nextHidden) => {
const dto = nextLayout.map((l, idx) => ({
widgetKey: l.i, x: l.x, y: l.y, w: l.w, h: l.h,
visible: !nextHidden.includes(l.i), sortOrder: idx, settingsJson: null
}));
LayoutApi.save(dto).catch(() => {});
};
const onLayoutChange = (l) => { setLayout(l); persist(l, hidden); };
const toggle = (key) => {
const next = hidden.includes(key) ? hidden.filter((h) => h !== key) : [...hidden, key];
setHidden(next); persist(layout, next);
};
const visibleLayout = useMemo(() => layout.filter((l) => !hidden.includes(l.i)), [layout, hidden]);
const render = (key) => {
// category-breakdown self-fetches, so it renders regardless of dashboard load state.
if (key === 'category-breakdown') return ;
// While the dashboard payload loads, show a skeleton placeholder per widget.
if (!data) return ;
switch (key) {
case 'inbox-health': return ;
case 'total-emails': return ;
case 'unread-emails': return ;
case 'storage': return ;
case 'top-senders': return ;
case 'volume': return ;
case 'attachments': return ;
default: return null;
}
};
return (
{syncing &&
}
{visibleLayout.map((l) => (
{render(l.i)}
))}
);
}