// Florio Analytics — Notifications tab (ported from js/pages/notifications.js). // Shows every notification variant (from the `notification_analytics` collection) // as a styled card sorted by success rate, plus KPI tiles and a day-late filter. // Rebuilt in the new design kit. All queries / metrics / interactions preserved. (function () { const NS = window.HyperChartsDesignSystem_e51a95; const { Badge } = NS; const I = window.HyperIcons; const { useState, useEffect } = React; // formatNumber from the data layer (lazy — FlorioAPI may not be ready at def time). function fmt(n) { return (window.FlorioAPI && window.FlorioAPI.formatNumber) ? window.FlorioAPI.formatNumber(n) : String(n ?? 0); } // ── range-independent fetch cache (the page is NOT date-range based) ── const _cache = {}; async function cached(key, fn, fallback) { if (key in _cache) return _cache[key]; try { const v = await fn(); _cache[key] = v; return v; } catch (e) { console.error('[Notifications] ' + key + ' failed:', e); return fallback; } } const SORTS = [ { key: 'rate', label: 'Success rate' }, { key: 'sent', label: 'Most sent' }, { key: 'success', label: 'Most watered' }, ]; const RANGES = [ { key: 'all', label: 'All' }, { key: 'day0', label: 'Day 0' }, { key: 'day1-3', label: 'Day 1-3' }, { key: 'day4+', label: 'Day 4+' }, ]; function rangeLabel(n) { if (n.minDaysLate === 0 && n.maxDaysLate === 0) return 'Day 0'; if (n.minDaysLate === 1 && n.maxDaysLate === 3) return 'Day 1-3'; if (n.minDaysLate >= 4) return 'Day 4+'; return 'Day ' + n.minDaysLate + '-' + n.maxDaysLate; } function rangeBadgeColor(n) { if (n.minDaysLate >= 4) return '#FE7E07'; // accent if (n.minDaysLate >= 1) return '#2ACF56'; // green return 'var(--text-muted)'; // muted } // iOS-style Florio app icon (carried over from the old page). function AppIcon() { return ( ); } // One notification variant: iOS-style push bubble + delivery stats card. function NotifCard({ n }) { const hasSent = n.totalSent > 0; const rate = hasSent ? (n.successRate * 100).toFixed(1) : '--'; const barWidth = hasSent ? Math.round(n.successRate * 100) : 0; const barColor = n.successRate >= 0.3 ? '#2ACF56' : '#FE7E07'; const badgeColor = rangeBadgeColor(n); return (
{/* push bubble */}
Florio now
{n.title}
{n.body}
{/* stats card */}
{[ { v: fmt(n.totalSent), l: 'sent', c: 'var(--text-primary)' }, { v: fmt(n.totalSuccess), l: 'watered', c: '#2ACF56' }, { v: rate + '%', l: 'rate', c: '#FE7E07' }, ].map((s, i) => (
{s.v} {s.l}
))}
{rangeLabel(n)} {n.id}
); } function Notifications({ range }) { const P = window.PA; const [notifs, setNotifs] = useState(null); const [sort, setSort] = useState('rate'); const [dayRange, setDayRange] = useState('all'); useEffect(() => { let alive = true; (async () => { await window.FlorioReady; const { db, collection, getDocs } = window.FlorioDB; // Data source: `notification_analytics` collection (global per-variant stats). const list = await cached('notifs', async () => { const snap = await getDocs(collection(db, 'notification_analytics')); const out = []; snap.forEach((doc) => { const data = doc.data(); const totalSent = data.totalSent ?? 0; const totalSuccess = data.totalSuccess ?? 0; out.push({ id: doc.id, title: data.title ?? doc.id, body: data.body ?? '', emotion: data.emotion ?? '', category: data.category ?? '', minDaysLate: data.minDaysLate ?? 0, maxDaysLate: data.maxDaysLate ?? 0, totalSent, totalSuccess, lastSentDate: data.lastSentDate ?? '', successRate: totalSent > 0 ? totalSuccess / totalSent : 0, // guard /0 }); }); return out; }, []); if (!alive) return; setNotifs(list); })(); return () => { alive = false; }; }, []); // Loading spinner while data null. if (!notifs) { return
; } // ── KPI stats (all variants) ── const totalVariants = notifs.length; const totalSent = notifs.reduce((s, n) => s + n.totalSent, 0); const totalSuccess = notifs.reduce((s, n) => s + n.totalSuccess, 0); const avgRate = totalSent > 0 ? ((totalSuccess / totalSent) * 100).toFixed(1) : '0.0'; // guard /0 // ── filter by day-late range ── let filtered; if (dayRange === 'day0') filtered = notifs.filter((n) => n.minDaysLate === 0 && n.maxDaysLate === 0); else if (dayRange === 'day1-3') filtered = notifs.filter((n) => n.minDaysLate === 1 && n.maxDaysLate === 3); else if (dayRange === 'day4+') filtered = notifs.filter((n) => n.minDaysLate >= 4); else filtered = notifs.slice(); // ── sort ── if (sort === 'rate') filtered.sort((a, b) => b.successRate - a.successRate); else if (sort === 'sent') filtered.sort((a, b) => b.totalSent - a.totalSent); else if (sort === 'success') filtered.sort((a, b) => b.totalSuccess - a.totalSuccess); const Toggle = ({ options, value, onChange }) => (
{options.map((o) => { const active = o.key === value; return ( ); })}
); return (
{/* KPI row — 4 stat tiles */}
{/* Controls + variant grid */}
} > {filtered.length === 0 ? (
{I.bell({ size: 26 })}
No notification data yet
Notification analytics will appear once the hourly function starts running.
) : (
{filtered.map((n) => )}
)}
); } (window.FlorioTabs = window.FlorioTabs || {}).notifications = Notifications; })();