MotorSignal-App/website/components/founder-portal/founder-stats-page.tsx

121 lines
7.2 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { onAuthStateChanged } from 'firebase/auth';
import { useRouter, useSearchParams } from 'next/navigation';
import { firebaseAuth } from '../../lib/firebase-client';
import {
fetchClubRides,
fetchFounderClubContext,
sanitizePortalError,
type ClubRidesResult,
type FounderClubContext,
} from '../../lib/club-data';
import { FounderPortalShell } from './founder-portal-shell';
import { MetricCard } from './metric-card';
import { OverviewSkeleton } from './skeleton-state';
import { SafeErrorState } from './safe-error-state';
export function FounderStatsPage({ clubId }: { clubId: string }) {
const router = useRouter();
const searchParams = useSearchParams();
const demo = process.env.NODE_ENV === 'development' && searchParams?.get('preview') === 'demo';
const [context, setContext] = useState<FounderClubContext | null>(null);
const [rides, setRides] = useState<ClubRidesResult | null>(null);
const [loading, setLoading] = useState(true);
const [access, setAccess] = useState<'sign-in' | 'restricted' | null>(null);
const [error, setError] = useState<string | null>(null);
const [retry, setRetry] = useState(0);
useEffect(() => {
let generation = 0;
let disposed = false;
const unsubscribe = onAuthStateChanged(firebaseAuth, async (user) => {
const current = ++generation;
setContext(null); setRides(null); setLoading(true); setAccess(null); setError(null);
if (!demo && (!user || user.isAnonymous)) {
setAccess('sign-in'); setLoading(false); return;
}
try {
const resolved = await fetchFounderClubContext(clubId, user?.uid, { allowDemoPreview: demo });
if (disposed || current !== generation) return;
if (!resolved.permissions.canManageSettings && !resolved.isDemoFixture) {
setAccess('restricted'); return;
}
const clubRides = await fetchClubRides(resolved.club.id, user?.uid, { allowDemoPreview: demo });
if (disposed || current !== generation) return;
setContext(resolved);
setRides(clubRides);
} catch (err) {
if (!disposed && current === generation) setError(sanitizePortalError(err, 'Club stats could not be loaded.'));
} finally {
if (!disposed && current === generation) setLoading(false);
}
});
return () => { disposed = true; generation++; unsubscribe(); };
}, [clubId, demo, retry]);
if (loading) return <div className="min-h-screen bg-brand-bg p-6"><OverviewSkeleton /></div>;
if (access) return <SafeErrorState title={access === 'sign-in' ? 'Authentication Required' : 'Management Access Restricted'}
message={access === 'sign-in' ? 'Sign in to view club stats.' : 'Only club Owners and Admins can view club stats.'}
actionLabel={access === 'sign-in' ? 'Sign In' : 'Return to Clubs'}
onAction={() => router.push(access === 'sign-in' ? '/login' : '/clubs')} />;
if (error || !context || !rides) return <SafeErrorState title="Club stats unavailable"
message={error || undefined} onRetry={() => setRetry((value) => value + 1)}
actionLabel="Return to Clubs" onAction={() => router.push('/clubs')} />;
const completed = rides.pastRides.filter((ride) => ride.status === 'completed');
const canceled = rides.pastRides.filter((ride) => ride.status === 'canceled');
const confirmedSeats = rides.upcomingRides.reduce((sum, ride) => sum + ride.confirmedCount, 0);
const ridePath = `/clubs/${context.club.slug || context.club.id}/manage/rides${demo ? '?preview=demo' : ''}`;
return <FounderPortalShell club={context.club} permissions={context.permissions} activeNav="stats">
<div className="space-y-7">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<p className="font-mono text-xs uppercase tracking-wider text-brand-cyan">Club overview</p>
<h1 className="font-headline text-3xl font-bold text-on-surface">Stats</h1>
<p className="mt-2 text-sm text-on-surface-variant">Current roster and the rides available in club management.</p>
</div>
<Link href={ridePath} className="rounded-xl border border-outline-variant px-4 py-2 text-sm font-semibold text-brand-cyan hover:bg-surface-container">Manage rides</Link>
</div>
<section aria-label="Club statistics" className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<MetricCard label="Active members" value={context.club.memberCount} subtextLeft="Club roster count" />
<MetricCard label="Scheduled or active" value={rides.upcomingRides.length} subtextLeft="Loaded club rides" disclaimer="Shows up to 30 rides; this is not an all-time total." />
<MetricCard label="Completed" value={completed.length} subtextLeft="Recently loaded rides" disclaimer="Shows up to 30 recent completed or canceled rides." />
<MetricCard label="Confirmed ride seats" value={confirmedSeats} subtextLeft="Across loaded scheduled and active rides" disclaimer="A rider on two rides counts twice. Shows up to 30 rides." />
</section>
<div className="grid gap-5 lg:grid-cols-2">
<section className="rounded-2xl border border-brand-border/60 bg-surface-container-low p-5" aria-labelledby="ride-status-title">
<h2 id="ride-status-title" className="font-headline text-lg font-semibold text-on-surface">Ride status</h2>
<p className="mt-1 text-xs text-on-surface-variant">From the club rides currently loaded in management.</p>
<dl className="mt-5 space-y-3 text-sm">
{[
['Scheduled', rides.upcomingRides.filter((ride) => ride.status === 'scheduled').length],
['Assembling', rides.upcomingRides.filter((ride) => ride.status === 'assembling').length],
['Underway', rides.upcomingRides.filter((ride) => ride.status === 'underway').length],
['Completed', completed.length],
['Canceled', canceled.length],
['Unclassified', [...rides.upcomingRides, ...rides.pastRides].filter((ride) => !ride.status).length],
].map(([label, count]) => <div key={label} className="flex justify-between border-b border-brand-border/40 pb-2">
<dt className="text-on-surface-variant">{label}</dt><dd className="font-mono font-semibold text-on-surface">{count}</dd>
</div>)}
</dl>
</section>
<section className="rounded-2xl border border-brand-border/60 bg-surface-container-low p-5" aria-labelledby="recent-rides-title">
<h2 id="recent-rides-title" className="font-headline text-lg font-semibold text-on-surface">Recent completed rides</h2>
{completed.length === 0 ? <p className="mt-5 text-sm text-on-surface-variant">No completed rides in the recent club ride list.</p>
: <ul className="mt-4 space-y-3">{completed.slice(0, 5).map((ride) => <li key={ride.id} className="flex justify-between gap-4 border-b border-brand-border/40 pb-3 text-sm">
<span className="min-w-0 truncate text-on-surface">{ride.title}</span>
<span className="shrink-0 font-mono text-on-surface-variant">{ride.confirmedCount} seats</span>
</li>)}</ul>}
</section>
</div>
</div>
</FounderPortalShell>;
}