// 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 */}
{/* 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;
})();