@burdenoff/microfe-eventfullymanaged 2026.713.1 → 2026.719.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/eventfullymanaged/api/eventApi.d.ts +40 -0
- package/dist/eventfullymanaged/api/eventApi.d.ts.map +1 -1
- package/dist/eventfullymanaged/api/eventApi.js +147 -117
- package/dist/eventfullymanaged/api/eventApi.js.map +1 -1
- package/dist/eventfullymanaged/pages/events/SessionFormPage.d.ts.map +1 -1
- package/dist/eventfullymanaged/pages/events/SessionFormPage.js +152 -135
- package/dist/eventfullymanaged/pages/events/SessionFormPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/AnalyticsPage.d.ts.map +1 -1
- package/dist/eventfullymanaged/pages/modules/AnalyticsPage.js +154 -117
- package/dist/eventfullymanaged/pages/modules/AnalyticsPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/CommunicationPage.d.ts.map +1 -1
- package/dist/eventfullymanaged/pages/modules/CommunicationPage.js +228 -187
- package/dist/eventfullymanaged/pages/modules/CommunicationPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/modules/EventsPage.d.ts.map +1 -1
- package/dist/eventfullymanaged/pages/modules/EventsPage.js +233 -122
- package/dist/eventfullymanaged/pages/modules/EventsPage.js.map +1 -1
- package/dist/eventfullymanaged/pages/registration/RegisterAttendeePage.d.ts.map +1 -1
- package/dist/eventfullymanaged/pages/registration/RegisterAttendeePage.js +255 -254
- package/dist/eventfullymanaged/pages/registration/RegisterAttendeePage.js.map +1 -1
- package/dist/eventfullymanaged/types/index.d.ts.map +1 -1
- package/dist/services/createSandboxAssistantTransport.d.ts.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/dist/stories/ActionMenu.stories.d.ts +10 -0
- package/dist/stories/ActionMenu.stories.d.ts.map +1 -0
- package/dist/stories/Pagination.stories.d.ts +11 -0
- package/dist/stories/Pagination.stories.d.ts.map +1 -0
- package/dist/stories/SafeLink.stories.d.ts +11 -0
- package/dist/stories/SafeLink.stories.d.ts.map +1 -0
- package/package.json +12 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/modules/AnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, CalendarDays, ClipboardList, MapPin, QrCode, Sparkles, Ticket, Trophy, Users } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n EmphasisPanel,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\n\nimport {\n CHART_SERIES_COLORS,\n ChartLegend,\n EventAreaChart,\n EventBarChart,\n EventDonutChart,\n type AreaPoint,\n type BarDatum,\n type DonutSlice,\n} from '../../components/EventCharts';\nimport { Pagination, usePagination } from '../../components/Pagination';\nimport {\n useDashboardSummaryQuery,\n useRegistrationsQuery,\n useUpsertAnalyticsSnapshotMutation,\n} from '../../api/eventApi';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\n/**\n * Snapshot metrics are computed server-side at generation time and returned on\n * the `metrics` JSON field of an analytics snapshot. The shared\n * `useAnalyticsSnapshotsQuery` hook does not select `metrics`, so this page owns\n * a dedicated query (distinct operation name) that pulls the metric payload in\n * addition to the identity fields.\n */\nconst ANALYTICS_SNAPSHOTS_WITH_METRICS_QUERY = gql`\n query EventfullymanagedAnalyticsSnapshotsWithMetrics($limit: Int, $offset: Int) {\n analyticsSnapshots: eventfullyManagedAnalyticsSnapshots(limit: $limit, offset: $offset) {\n total\n items {\n id\n name\n title\n status\n metrics\n createdAt\n }\n }\n }\n`;\n\ntype SnapshotMetrics = {\n totalEvents?: number | null;\n activeEvents?: number | null;\n totalRegistrations?: number | null;\n totalAttendees?: number | null;\n totalVenues?: number | null;\n totalSponsors?: number | null;\n upcomingSessions?: number | null;\n generatedAt?: string | null;\n};\n\ntype SnapshotRow = {\n id: string;\n name?: string | null;\n title?: string | null;\n status?: string | null;\n createdAt?: string | null;\n metrics?: SnapshotMetrics | null;\n};\n\ntype SnapshotsResponse = {\n analyticsSnapshots: { total: number; items: SnapshotRow[] };\n};\n\nconst METRIC_FIELDS: { key: keyof SnapshotMetrics; label: string }[] = [\n { key: 'totalEvents', label: 'Events' },\n { key: 'activeEvents', label: 'Active' },\n { key: 'totalRegistrations', label: 'Registrations' },\n { key: 'totalAttendees', label: 'Attendees' },\n { key: 'totalVenues', label: 'Venues' },\n { key: 'totalSponsors', label: 'Sponsors' },\n { key: 'upcomingSessions', label: 'Upcoming Sessions' },\n];\n\nfunction statusVariant(status: string): 'success' | 'warning' | 'secondary' | 'info' {\n const value = status.toLowerCase();\n if (['active', 'published', 'completed'].includes(value)) return 'success';\n if (['inactive', 'cancelled', 'archived', 'expired'].includes(value)) return 'warning';\n if (['draft', 'created', 'pending', 'scheduled'].includes(value)) return 'info';\n return 'secondary';\n}\n\nfunction formatGeneratedAt(value?: string | null): string | null {\n if (!value) return null;\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? null : parsed.toLocaleString();\n}\n\nfunction shortDate(value?: string | null): string {\n if (!value) return '—';\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) return '—';\n return parsed.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\n/** Humanise a registration status token (e.g. `checked_in` → `Checked In`). */\nfunction humanizeStatus(status: string): string {\n return status\n .replace(/[_-]+/g, ' ')\n .split(' ')\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ');\n}\n\nfunction MetricTile({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: number }) {\n return (\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Icon className=\"h-4 w-4 text-accent-primary\" />\n <p className=\"text-xs font-medium uppercase tracking-wide\">{label}</p>\n </div>\n <p className=\"mt-2 text-2xl font-bold tabular-nums text-text-primary\">{value.toLocaleString()}</p>\n </GlassCard>\n );\n}\n\nexport default function AnalyticsPage() {\n const pagination = usePagination({ pageSize: 8 });\n const { data, loading, refetch } = useQuery<SnapshotsResponse, { limit: number; offset: number }>(\n ANALYTICS_SNAPSHOTS_WITH_METRICS_QUERY,\n { variables: { limit: pagination.limit, offset: pagination.offset } }\n );\n const summaryQuery = useDashboardSummaryQuery();\n const registrationsQuery = useRegistrationsQuery({ limit: 200, offset: 0 });\n const [upsertSnapshot, { loading: creating }] = useUpsertAnalyticsSnapshotMutation();\n const [snapshotName, setSnapshotName] = useState('overview');\n\n const snapshots = useMemo(() => data?.analyticsSnapshots.items ?? [], [data]);\n const total = data?.analyticsSnapshots.total ?? 0;\n\n const summary = summaryQuery.data?.dashboardSummary;\n const registrations = useMemo(() => registrationsQuery.data?.registrations.items ?? [], [registrationsQuery.data]);\n\n // Check-ins are derived from the loaded registration sample (registrations\n // carrying a `checkedInAt`) — the dashboard summary does not expose them.\n const checkInCount = useMemo(() => registrations.filter((row) => Boolean(row.checkedInAt)).length, [registrations]);\n\n // Registrations over time: the totalRegistrations captured on each snapshot,\n // ordered oldest → newest. Purely real, snapshot-derived — never synthesised.\n const registrationsSeries = useMemo<AreaPoint[]>(() => {\n return snapshots\n .map((snapshot) => {\n const at = snapshot.metrics?.generatedAt ?? snapshot.createdAt ?? null;\n const parsed = at ? new Date(at) : null;\n const value =\n typeof snapshot.metrics?.totalRegistrations === 'number' ? snapshot.metrics.totalRegistrations : null;\n return parsed && !Number.isNaN(parsed.getTime()) && value !== null\n ? { time: parsed.getTime(), label: shortDate(at), value }\n : null;\n })\n .filter((point): point is { time: number; label: string; value: number } => point !== null)\n .sort((a, b) => a.time - b.time)\n .map(({ label, value }) => ({ label, value }));\n }, [snapshots]);\n\n // Registrations by status donut, computed from the real registration list.\n const statusBreakdown = useMemo<DonutSlice[]>(() => {\n const counts = new Map<string, number>();\n for (const row of registrations) {\n const key = (row.status || 'unknown').toLowerCase();\n counts.set(key, (counts.get(key) ?? 0) + 1);\n }\n return Array.from(counts.entries())\n .sort((a, b) => b[1] - a[1])\n .map(([key, count], index) => ({\n name: humanizeStatus(key),\n value: count,\n color: CHART_SERIES_COLORS[index % CHART_SERIES_COLORS.length],\n }));\n }, [registrations]);\n\n const registrationTotal = registrations.length;\n\n // Entity-count bar chart from the real dashboard summary + derived check-ins.\n const entityCounts = useMemo<BarDatum[]>(() => {\n if (!summary) return [];\n return [\n { name: 'Events', value: summary.totalEvents },\n { name: 'Regs', value: summary.totalRegistrations },\n { name: 'Attendees', value: summary.totalAttendees },\n { name: 'Check-ins', value: checkInCount },\n { name: 'Venues', value: summary.totalVenues },\n { name: 'Sponsors', value: summary.totalSponsors },\n { name: 'Sessions', value: summary.upcomingSessions },\n ];\n }, [summary, checkInCount]);\n\n const onCreate = async () => {\n if (!snapshotName.trim()) return;\n try {\n const response = await upsertSnapshot({\n variables: {\n input: {\n name: snapshotName,\n title: snapshotName,\n status: 'created',\n metadata: {},\n },\n },\n });\n if (!mutationSucceeded(response, response.data?.upsertAnalyticsSnapshot?.id, 'Could not generate snapshot')) {\n return;\n }\n notify.success('Snapshot generated');\n await refetch();\n } catch (error) {\n notify.error('Could not generate snapshot', describeError(error));\n }\n };\n\n const summaryLoading = summaryQuery.loading && !summary;\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n Analytics gives you the live pulse of your event operations — registrations over time, how those registrations\n break down by status, and totals across every entity you manage. Generate a snapshot to freeze today's\n numbers so you can compare trends over time and report on outcomes.\n </PagePurpose>\n\n <EmphasisPanel>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>Analytics</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Real-time metrics from live event operations, plus point-in-time snapshots you control.\n </p>\n </div>\n <div className=\"flex w-full flex-col gap-2 sm:w-auto sm:flex-row\">\n <Input\n value={snapshotName}\n onChange={(e) => setSnapshotName(e.target.value)}\n placeholder=\"daily-summary\"\n aria-label=\"Snapshot name\"\n className=\"sm:w-48\"\n />\n <Button type=\"button\" disabled={creating} onClick={onCreate}>\n <Sparkles className=\"h-4 w-4\" />\n {creating ? 'Generating...' : 'Generate snapshot'}\n </Button>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_320px]\">\n <div className=\"space-y-6\">\n {/* Live metric tiles */}\n {summaryLoading ? (\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 7 }).map((_, index) => (\n <Skeleton key={index} className=\"h-[92px] rounded-card\" />\n ))}\n </div>\n ) : summary ? (\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-4\">\n <MetricTile icon={CalendarDays} label=\"Events\" value={summary.totalEvents} />\n <MetricTile icon={ClipboardList} label=\"Registrations\" value={summary.totalRegistrations} />\n <MetricTile icon={Users} label=\"Attendees\" value={summary.totalAttendees} />\n <MetricTile icon={QrCode} label=\"Check-ins\" value={checkInCount} />\n <MetricTile icon={MapPin} label=\"Venues\" value={summary.totalVenues} />\n <MetricTile icon={Trophy} label=\"Sponsors\" value={summary.totalSponsors} />\n <MetricTile icon={Ticket} label=\"Upcoming Sessions\" value={summary.upcomingSessions} />\n </div>\n ) : null}\n\n {/* Registrations over time + by status */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-base font-semibold text-text-primary\">Registrations over time</h2>\n <span className=\"text-xs text-text-muted\">By snapshot</span>\n </div>\n <div className=\"mt-4\">\n <EventAreaChart data={registrationsSeries} emptyLabel=\"Generate a few snapshots to chart the trend\" />\n </div>\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h2 className=\"text-base font-semibold text-text-primary\">Registrations by status</h2>\n <div className=\"mt-4 flex flex-col items-center gap-4 sm:flex-row\">\n <div className=\"w-full sm:w-1/2\">\n <EventDonutChart\n data={statusBreakdown}\n centerValue={registrationTotal.toLocaleString()}\n centerLabel=\"Registrations\"\n emptyLabel=\"No registrations yet\"\n />\n </div>\n <div className=\"w-full sm:w-1/2\">\n <ChartLegend\n items={statusBreakdown.map((slice) => ({\n label: slice.name,\n color: slice.color ?? 'var(--color-chart-1)',\n value: slice.value.toLocaleString(),\n }))}\n />\n </div>\n </div>\n </GlassCard>\n </div>\n\n {/* Entity totals */}\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h2 className=\"text-base font-semibold text-text-primary\">Portfolio totals</h2>\n <p className=\"mt-0.5 text-xs text-text-muted\">Live counts across every entity you manage.</p>\n <div className=\"mt-4\">\n <EventBarChart data={entityCounts} emptyLabel=\"No operational data yet\" />\n </div>\n </GlassCard>\n </div>\n\n {/* Saved snapshots rail */}\n <aside className=\"space-y-4\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Saved snapshots {total > 0 ? <span className=\"text-text-muted\">({total})</span> : null}\n </h2>\n <BarChart3 className=\"h-4 w-4 text-text-muted\" />\n </div>\n\n {loading && snapshots.length === 0 ? (\n <div className=\"space-y-3\">\n {Array.from({ length: 3 }).map((_, index) => (\n <Skeleton key={index} className=\"h-24 rounded-card\" />\n ))}\n </div>\n ) : snapshots.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No snapshots yet\"\n description=\"Generate your first snapshot to freeze the current metrics.\"\n className=\"py-8\"\n />\n </GlassCard>\n ) : (\n <div className=\"space-y-3\">\n {snapshots.map((snapshot) => {\n const generatedAt =\n formatGeneratedAt(snapshot.metrics?.generatedAt) ?? formatGeneratedAt(snapshot.createdAt);\n const captured = METRIC_FIELDS.map((field) => {\n const raw = snapshot.metrics?.[field.key];\n return { label: field.label, value: typeof raw === 'number' ? raw : null };\n }).filter((entry) => entry.value !== null);\n return (\n <GlassCard key={snapshot.id} treatment=\"plain\" className=\"border-border-seam p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">\n {snapshot.title ?? snapshot.name ?? `Snapshot ${snapshot.id}`}\n </h3>\n {generatedAt ? <p className=\"mt-0.5 text-xs text-text-muted\">{generatedAt}</p> : null}\n </div>\n {snapshot.status ? (\n <Badge variant={statusVariant(snapshot.status)} className=\"shrink-0 uppercase\">\n {snapshot.status}\n </Badge>\n ) : null}\n </div>\n {captured.length > 0 ? (\n <dl className=\"mt-3 space-y-1.5\">\n {captured.map((entry) => (\n <div key={entry.label} className=\"flex items-center justify-between text-xs\">\n <dt className=\"text-text-secondary\">{entry.label}</dt>\n <dd className=\"font-semibold tabular-nums text-text-primary\">\n {(entry.value ?? 0).toLocaleString()}\n </dd>\n </div>\n ))}\n </dl>\n ) : (\n <p className=\"mt-3 text-xs text-text-muted\">No metrics captured.</p>\n )}\n </GlassCard>\n );\n })}\n\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={snapshots.length}\n total={total}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"snapshots\"\n loading={loading}\n />\n </div>\n )}\n </aside>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/eventfullymanaged/pages/modules/AnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, CalendarDays, ClipboardList, MapPin, QrCode, Sparkles, Ticket, Trophy, Users } from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport {\n Badge,\n Button,\n EmphasisPanel,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useMemo, useState } from 'react';\n\nimport {\n CHART_SERIES_COLORS,\n ChartLegend,\n EventAreaChart,\n EventBarChart,\n EventDonutChart,\n type AreaPoint,\n type BarDatum,\n type DonutSlice,\n} from '../../components/EventCharts';\nimport { Pagination, usePagination } from '../../components/Pagination';\nimport {\n useCheckInThroughputQuery,\n useDashboardSummaryQuery,\n useEventsQuery,\n useRegistrationsQuery,\n useUpsertAnalyticsSnapshotMutation,\n} from '../../api/eventApi';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\n/**\n * Snapshot metrics are computed server-side at generation time and returned on\n * the `metrics` JSON field of an analytics snapshot. The shared\n * `useAnalyticsSnapshotsQuery` hook does not select `metrics`, so this page owns\n * a dedicated query (distinct operation name) that pulls the metric payload in\n * addition to the identity fields.\n */\nconst ANALYTICS_SNAPSHOTS_WITH_METRICS_QUERY = gql`\n query EventfullymanagedAnalyticsSnapshotsWithMetrics($limit: Int, $offset: Int) {\n analyticsSnapshots: eventfullyManagedAnalyticsSnapshots(limit: $limit, offset: $offset) {\n total\n items {\n id\n name\n title\n status\n metrics\n createdAt\n }\n }\n }\n`;\n\ntype SnapshotMetrics = {\n totalEvents?: number | null;\n activeEvents?: number | null;\n totalRegistrations?: number | null;\n totalAttendees?: number | null;\n totalVenues?: number | null;\n totalSponsors?: number | null;\n upcomingSessions?: number | null;\n generatedAt?: string | null;\n};\n\ntype SnapshotRow = {\n id: string;\n name?: string | null;\n title?: string | null;\n status?: string | null;\n createdAt?: string | null;\n metrics?: SnapshotMetrics | null;\n};\n\ntype SnapshotsResponse = {\n analyticsSnapshots: { total: number; items: SnapshotRow[] };\n};\n\nconst METRIC_FIELDS: { key: keyof SnapshotMetrics; label: string }[] = [\n { key: 'totalEvents', label: 'Events' },\n { key: 'activeEvents', label: 'Active' },\n { key: 'totalRegistrations', label: 'Registrations' },\n { key: 'totalAttendees', label: 'Attendees' },\n { key: 'totalVenues', label: 'Venues' },\n { key: 'totalSponsors', label: 'Sponsors' },\n { key: 'upcomingSessions', label: 'Upcoming Sessions' },\n];\n\nfunction statusVariant(status: string): 'success' | 'warning' | 'secondary' | 'info' {\n const value = status.toLowerCase();\n if (['active', 'published', 'completed'].includes(value)) return 'success';\n if (['inactive', 'cancelled', 'archived', 'expired'].includes(value)) return 'warning';\n if (['draft', 'created', 'pending', 'scheduled'].includes(value)) return 'info';\n return 'secondary';\n}\n\nfunction formatGeneratedAt(value?: string | null): string | null {\n if (!value) return null;\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? null : parsed.toLocaleString();\n}\n\nfunction shortDate(value?: string | null): string {\n if (!value) return '—';\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) return '—';\n return parsed.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\n/** Humanise a registration status token (e.g. `checked_in` → `Checked In`). */\nfunction humanizeStatus(status: string): string {\n return status\n .replace(/[_-]+/g, ' ')\n .split(' ')\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ');\n}\n\nfunction MetricTile({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: number }) {\n return (\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Icon className=\"h-4 w-4 text-accent-primary\" />\n <p className=\"text-xs font-medium uppercase tracking-wide\">{label}</p>\n </div>\n <p className=\"mt-2 text-2xl font-bold tabular-nums text-text-primary\">{value.toLocaleString()}</p>\n </GlassCard>\n );\n}\n\nexport default function AnalyticsPage() {\n const pagination = usePagination({ pageSize: 8 });\n const { data, loading, refetch } = useQuery<SnapshotsResponse, { limit: number; offset: number }>(\n ANALYTICS_SNAPSHOTS_WITH_METRICS_QUERY,\n { variables: { limit: pagination.limit, offset: pagination.offset } }\n );\n const summaryQuery = useDashboardSummaryQuery();\n const registrationsQuery = useRegistrationsQuery({ limit: 200, offset: 0 });\n const [upsertSnapshot, { loading: creating }] = useUpsertAnalyticsSnapshotMutation();\n const [snapshotName, setSnapshotName] = useState('overview');\n\n // BOFF-3999 — check-in throughput is scoped to one event; default to the\n // first event once the list loads.\n const eventsQuery = useEventsQuery({ limit: 50, offset: 0 });\n const events = useMemo(() => eventsQuery.data?.events.items ?? [], [eventsQuery.data]);\n // Derived (not effect-synced): an explicit user pick wins, otherwise fall\n // back to the first loaded event — avoids a setState-in-effect render.\n const [throughputEventIdOverride, setThroughputEventIdOverride] = useState('');\n const throughputEventId = throughputEventIdOverride || events[0]?.id || '';\n const throughputQuery = useCheckInThroughputQuery({ eventId: throughputEventId });\n const throughputSeries = useMemo<BarDatum[]>(() => {\n return (throughputQuery.data?.checkInThroughput ?? []).map((point) => ({\n name: new Date(point.hour).toLocaleTimeString(undefined, { hour: 'numeric' }),\n value: point.count,\n }));\n }, [throughputQuery.data]);\n\n const snapshots = useMemo(() => data?.analyticsSnapshots.items ?? [], [data]);\n const total = data?.analyticsSnapshots.total ?? 0;\n\n const summary = summaryQuery.data?.dashboardSummary;\n const registrations = useMemo(() => registrationsQuery.data?.registrations.items ?? [], [registrationsQuery.data]);\n\n // Check-ins are derived from the loaded registration sample (registrations\n // carrying a `checkedInAt`) — the dashboard summary does not expose them.\n const checkInCount = useMemo(() => registrations.filter((row) => Boolean(row.checkedInAt)).length, [registrations]);\n\n // Registrations over time: the totalRegistrations captured on each snapshot,\n // ordered oldest → newest. Purely real, snapshot-derived — never synthesised.\n const registrationsSeries = useMemo<AreaPoint[]>(() => {\n return snapshots\n .map((snapshot) => {\n const at = snapshot.metrics?.generatedAt ?? snapshot.createdAt ?? null;\n const parsed = at ? new Date(at) : null;\n const value =\n typeof snapshot.metrics?.totalRegistrations === 'number' ? snapshot.metrics.totalRegistrations : null;\n return parsed && !Number.isNaN(parsed.getTime()) && value !== null\n ? { time: parsed.getTime(), label: shortDate(at), value }\n : null;\n })\n .filter((point): point is { time: number; label: string; value: number } => point !== null)\n .sort((a, b) => a.time - b.time)\n .map(({ label, value }) => ({ label, value }));\n }, [snapshots]);\n\n // Registrations by status donut, computed from the real registration list.\n const statusBreakdown = useMemo<DonutSlice[]>(() => {\n const counts = new Map<string, number>();\n for (const row of registrations) {\n const key = (row.status || 'unknown').toLowerCase();\n counts.set(key, (counts.get(key) ?? 0) + 1);\n }\n return Array.from(counts.entries())\n .sort((a, b) => b[1] - a[1])\n .map(([key, count], index) => ({\n name: humanizeStatus(key),\n value: count,\n color: CHART_SERIES_COLORS[index % CHART_SERIES_COLORS.length],\n }));\n }, [registrations]);\n\n const registrationTotal = registrations.length;\n\n // Entity-count bar chart from the real dashboard summary + derived check-ins.\n const entityCounts = useMemo<BarDatum[]>(() => {\n if (!summary) return [];\n return [\n { name: 'Events', value: summary.totalEvents },\n { name: 'Regs', value: summary.totalRegistrations },\n { name: 'Attendees', value: summary.totalAttendees },\n { name: 'Check-ins', value: checkInCount },\n { name: 'Venues', value: summary.totalVenues },\n { name: 'Sponsors', value: summary.totalSponsors },\n { name: 'Sessions', value: summary.upcomingSessions },\n ];\n }, [summary, checkInCount]);\n\n const onCreate = async () => {\n if (!snapshotName.trim()) return;\n try {\n const response = await upsertSnapshot({\n variables: {\n input: {\n name: snapshotName,\n title: snapshotName,\n status: 'created',\n metadata: {},\n },\n },\n });\n if (!mutationSucceeded(response, response.data?.upsertAnalyticsSnapshot?.id, 'Could not generate snapshot')) {\n return;\n }\n notify.success('Snapshot generated');\n await refetch();\n } catch (error) {\n notify.error('Could not generate snapshot', describeError(error));\n }\n };\n\n const summaryLoading = summaryQuery.loading && !summary;\n\n return (\n <div className=\"space-y-6 p-6\">\n <PagePurpose>\n Analytics gives you the live pulse of your event operations — registrations over time, how those registrations\n break down by status, and totals across every entity you manage. Generate a snapshot to freeze today's\n numbers so you can compare trends over time and report on outcomes.\n </PagePurpose>\n\n <EmphasisPanel>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>Analytics</GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Real-time metrics from live event operations, plus point-in-time snapshots you control.\n </p>\n </div>\n <div className=\"flex w-full flex-col gap-2 sm:w-auto sm:flex-row\">\n <Input\n value={snapshotName}\n onChange={(e) => setSnapshotName(e.target.value)}\n placeholder=\"daily-summary\"\n aria-label=\"Snapshot name\"\n className=\"sm:w-48\"\n />\n <Button type=\"button\" disabled={creating} onClick={onCreate}>\n <Sparkles className=\"h-4 w-4\" />\n {creating ? 'Generating...' : 'Generate snapshot'}\n </Button>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_320px]\">\n <div className=\"space-y-6\">\n {/* Live metric tiles */}\n {summaryLoading ? (\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 7 }).map((_, index) => (\n <Skeleton key={index} className=\"h-[92px] rounded-card\" />\n ))}\n </div>\n ) : summary ? (\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-4\">\n <MetricTile icon={CalendarDays} label=\"Events\" value={summary.totalEvents} />\n <MetricTile icon={ClipboardList} label=\"Registrations\" value={summary.totalRegistrations} />\n <MetricTile icon={Users} label=\"Attendees\" value={summary.totalAttendees} />\n <MetricTile icon={QrCode} label=\"Check-ins\" value={checkInCount} />\n <MetricTile icon={MapPin} label=\"Venues\" value={summary.totalVenues} />\n <MetricTile icon={Trophy} label=\"Sponsors\" value={summary.totalSponsors} />\n <MetricTile icon={Ticket} label=\"Upcoming Sessions\" value={summary.upcomingSessions} />\n </div>\n ) : null}\n\n {/* Registrations over time + by status */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-base font-semibold text-text-primary\">Registrations over time</h2>\n <span className=\"text-xs text-text-muted\">By snapshot</span>\n </div>\n <div className=\"mt-4\">\n <EventAreaChart data={registrationsSeries} emptyLabel=\"Generate a few snapshots to chart the trend\" />\n </div>\n </GlassCard>\n\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h2 className=\"text-base font-semibold text-text-primary\">Registrations by status</h2>\n <div className=\"mt-4 flex flex-col items-center gap-4 sm:flex-row\">\n <div className=\"w-full sm:w-1/2\">\n <EventDonutChart\n data={statusBreakdown}\n centerValue={registrationTotal.toLocaleString()}\n centerLabel=\"Registrations\"\n emptyLabel=\"No registrations yet\"\n />\n </div>\n <div className=\"w-full sm:w-1/2\">\n <ChartLegend\n items={statusBreakdown.map((slice) => ({\n label: slice.name,\n color: slice.color ?? 'var(--color-chart-1)',\n value: slice.value.toLocaleString(),\n }))}\n />\n </div>\n </div>\n </GlassCard>\n </div>\n\n {/* Entity totals */}\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h2 className=\"text-base font-semibold text-text-primary\">Portfolio totals</h2>\n <p className=\"mt-0.5 text-xs text-text-muted\">Live counts across every entity you manage.</p>\n <div className=\"mt-4\">\n <EventBarChart data={entityCounts} emptyLabel=\"No operational data yet\" />\n </div>\n </GlassCard>\n\n {/* BOFF-3999 — check-in throughput by hour, scoped to one event. */}\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Check-in throughput</h2>\n <p className=\"mt-0.5 text-xs text-text-muted\">Scans per hour at the door for the selected event.</p>\n </div>\n {events.length > 0 ? (\n <Select value={throughputEventId} onValueChange={setThroughputEventIdOverride}>\n <SelectTrigger className=\"w-full sm:w-56\" aria-label=\"Select event for check-in throughput\">\n <SelectValue placeholder=\"Select event\" />\n </SelectTrigger>\n <SelectContent>\n {events.map((event) => (\n <SelectItem key={event.id} value={event.id}>\n {event.name}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : null}\n </div>\n <div className=\"mt-4\">\n <EventBarChart\n data={throughputSeries}\n emptyLabel={\n throughputEventId ? 'No check-ins recorded yet for this event' : 'Select an event to see throughput'\n }\n />\n </div>\n </GlassCard>\n </div>\n\n {/* Saved snapshots rail */}\n <aside className=\"space-y-4\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n Saved snapshots {total > 0 ? <span className=\"text-text-muted\">({total})</span> : null}\n </h2>\n <BarChart3 className=\"h-4 w-4 text-text-muted\" />\n </div>\n\n {loading && snapshots.length === 0 ? (\n <div className=\"space-y-3\">\n {Array.from({ length: 3 }).map((_, index) => (\n <Skeleton key={index} className=\"h-24 rounded-card\" />\n ))}\n </div>\n ) : snapshots.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No snapshots yet\"\n description=\"Generate your first snapshot to freeze the current metrics.\"\n className=\"py-8\"\n />\n </GlassCard>\n ) : (\n <div className=\"space-y-3\">\n {snapshots.map((snapshot) => {\n const generatedAt =\n formatGeneratedAt(snapshot.metrics?.generatedAt) ?? formatGeneratedAt(snapshot.createdAt);\n const captured = METRIC_FIELDS.map((field) => {\n const raw = snapshot.metrics?.[field.key];\n return { label: field.label, value: typeof raw === 'number' ? raw : null };\n }).filter((entry) => entry.value !== null);\n return (\n <GlassCard key={snapshot.id} treatment=\"plain\" className=\"border-border-seam p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">\n {snapshot.title ?? snapshot.name ?? `Snapshot ${snapshot.id}`}\n </h3>\n {generatedAt ? <p className=\"mt-0.5 text-xs text-text-muted\">{generatedAt}</p> : null}\n </div>\n {snapshot.status ? (\n <Badge variant={statusVariant(snapshot.status)} className=\"shrink-0 uppercase\">\n {snapshot.status}\n </Badge>\n ) : null}\n </div>\n {captured.length > 0 ? (\n <dl className=\"mt-3 space-y-1.5\">\n {captured.map((entry) => (\n <div key={entry.label} className=\"flex items-center justify-between text-xs\">\n <dt className=\"text-text-secondary\">{entry.label}</dt>\n <dd className=\"font-semibold tabular-nums text-text-primary\">\n {(entry.value ?? 0).toLocaleString()}\n </dd>\n </div>\n ))}\n </dl>\n ) : (\n <p className=\"mt-3 text-xs text-text-muted\">No metrics captured.</p>\n )}\n </GlassCard>\n );\n })}\n\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={snapshots.length}\n total={total}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"snapshots\"\n loading={loading}\n />\n </div>\n )}\n </aside>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommunicationPage.d.ts","sourceRoot":"","sources":["../../../../src/eventfullymanaged/pages/modules/CommunicationPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CommunicationPage.d.ts","sourceRoot":"","sources":["../../../../src/eventfullymanaged/pages/modules/CommunicationPage.tsx"],"names":[],"mappings":"AA6DA,MAAM,CAAC,OAAO,UAAU,iBAAiB,4CA0axC"}
|