import { useQuery, useQueryClient } from '@tanstack/react-query' import { LoaderCircle } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' import PrefetchLink from '@/components/PrefetchLink' import CountsExportProgress from '@/components/counts/CountsExportProgress' import CountsControls from '@/components/counts/CountsControls' import CountsSummaryResults from '@/components/counts/CountsSummaryResults' import CountsResults from '@/components/counts/CountsResults' import CountsTableLayout from '@/components/counts/CountsTableLayout' import AnimatedCollapse from '@/components/common/AnimatedCollapse' import PageHead from '@/components/common/PageHead' import PageTitle from '@/components/common/PageTitle' import Pagination from '@/components/common/Pagination' import MainArea from '@/components/layout/MainArea' import SidebarComponent from '@/components/layout/SidebarComponent' import { Button } from '@/components/ui/button' import { countsResourcePath, countsRequestParams, countsDefinitionParams, countsExportPayload, fieldsForCounts, fetchCounts, isCountsState, normaliseCountsParams, selectedCountColumns } from '@/lib/counts' import { countsErrorMessage, countsExportLabels } from '@/lib/countsDisplay' import { isApiError } from '@/lib/api' import { CountsDefinitionContext, countsAxisFields, countsAxisLabel, visibleCountsResources, countsFieldLabel, fetchCountsDefinition } from '@/lib/countsDefinitions' import { countsKeys } from '@/lib/queryKeys' import { setCountState, readCountState } from '@/lib/queryState' import { cn } from '@/lib/utils' import { useClientMotion } from '@/lib/useClientMotion' import useDialogue from '@/lib/dialogues/useDialogue' import { useLocale } from '@/lib/useLocale' import { useUITranslation } from '@/lib/useUITranslation' import type { CountField, CountsResource } from '@/lib/counts' import type { CountsAxis, CountsDefinition } from '@/lib/countsDefinitions' type PendingCountsRun = { key: ReturnType origin: string params: URLSearchParams target?: string } const tooManyColumns = (failure: unknown): number | null => { if (!isApiError (failure) || failure.response?.status !== 422) return null const body = (failure.response.data ?? { }) as { error?: unknown; maxColumns?: unknown; max_columns?: unknown } const maximum = body.maxColumns ?? body.max_columns return body.error === 'too_many_columns' && typeof maximum === 'number' ? maximum : null } const CountsResourceContent = ( { resource, definition, definitionError, definitionFailure }: { resource: CountsResource definition?: CountsDefinition definitionError: boolean; definitionFailure?: unknown }, ) => { const queryClient = useQueryClient () const [mounted, setMounted] = useState (false) useEffect (() => { setMounted (true) }, []) const [source, setParams] = useSearchParams () const [timezone, setTimezone] = useState (null) useEffect (() => { setTimezone (Intl.DateTimeFormat ().resolvedOptions ().timeZone) }, []) const params = normaliseCountsParams (resource, source, definition) if (!params.has ('timezone') && timezone != null) params.set ('timezone', timezone) const canonical = params.toString () const request = countsRequestParams (params) const [open, setOpen] = useState (false) const controlsArea = useRef (null) const [controlsHeight, setControlsHeight] = useState (80) useEffect (() => { const node = controlsArea.current if (definition == null || node == null) return const measure = () => setControlsHeight (node.getBoundingClientRect ().height) measure () if (typeof ResizeObserver === 'undefined') return const observer = new ResizeObserver (measure) observer.observe (node) return () => observer.disconnect () }, [definition]) const [sidebarOpen, setSidebarOpen] = useState (false) const [expanded, setExpanded] = useState (() => new Set ([resource])) const motion = useClientMotion () useEffect (() => { setExpanded (previous => new Set ([...previous, resource])) }, [resource]) const locale = useLocale () const dialogue = useDialogue () const { label, message } = useUITranslation ('counts') const title = message (`titles.${resource}`) const mode = params.get ('mode') ?? 'summary' const state = isCountsState (params) const cacheParams = new URLSearchParams (request) cacheParams.set ('definition', state ? 'latest' : 'history') const [running, setRunning] = useState (false) const [runError, setRunError] = useState (false) const [runFailure, setRunFailure] = useState (null) const [runColumnLimit, setRunColumnLimit] = useState (null) const pendingRun = useRef (null) const applied = useRef (params) const retainedParams = useRef (params) const requestKey = request.toString () const [exporting, setExporting] = useState (false) const exportInFlight = useRef (false) const [exportError, setExportError] = useState<{ failure: unknown } | null> (null) const { data: cachedData, error: queryError, dataUpdatedAt, isFetching: fetching, isPending: pending, isError: errorFlg, isPlaceholderData: placeholder } = useQuery ({ enabled: mounted && definition != null && params.has ('timezone') && !running, meta: { clientOnly: true }, staleTime: params.has ('execution_at') ? 5 * 60 * 1000 : 0, queryKey: countsKeys.index (resource, cacheParams.toString (), locale), queryFn: ({ signal }) => fetchCounts (resource, params, locale, signal), retry: false, placeholderData: (previous, query) => (query?.queryKey[1] === resource && query?.queryKey[2] === locale && queryClient.getQueryData (query.queryKey) === previous) ? previous : undefined }) // Match SSR during hydration even when a client cache already contains results. const data = mounted ? cachedData : undefined useEffect (() => { applied.current = params if (data != null && !placeholder && !errorFlg) retainedParams.current = params const pending = pendingRun.current if (pending == null) return if (pending.target === canonical) { pendingRun.current = null setRunning (false) } else if (pending.origin !== requestKey) { pendingRun.current = null void queryClient.cancelQueries ({ queryKey: pending.key, exact: true }) setRunning (false) } }, [canonical, data, errorFlg, placeholder, params, queryClient, requestKey]) useEffect (() => { setRunning (false) setRunError (false) setRunColumnLimit (null) return () => { const pending = pendingRun.current pendingRun.current = null if (pending != null) void queryClient.cancelQueries ({ queryKey: pending.key, exact: true }) } }, [locale, queryClient, resource]) useEffect (() => { if (running || data == null || placeholder || fetching || errorFlg || !params.has ('timezone')) return const next = new URLSearchParams (canonical) if (data.executionAt != null && !params.has ('execution_at')) { next.set ('execution_at', data.executionAt) // Commit initial defaults, timezone and server time together after acquisition. queryClient.setQueryData ( countsKeys.index (resource, countsRequestParams (next).toString (), locale), data, { updatedAt: dataUpdatedAt }) } if (source.toString () !== next.toString ()) setParams (next, { replace: true, preventScrollReset: true }) }, [canonical, data, dataUpdatedAt, errorFlg, fetching, placeholder, locale, params, queryClient, resource, running, setParams, source]) const execute = async (draft: URLSearchParams) => { if (pendingRun.current != null || definition == null) return const next = normaliseCountsParams (resource, draft, definition) next.delete ('execution_at') if (!next.has ('timezone')) next.set ('timezone', Intl.DateTimeFormat ().resolvedOptions ().timeZone) const pending: PendingCountsRun = { key: countsKeys.index (resource, countsRequestParams (next).toString (), locale), origin: requestKey, params: new URLSearchParams (next) } const previous = new URLSearchParams (params) pendingRun.current = pending setRunError (false) setRunColumnLimit (null) setRunning (true) try { const result = await queryClient.fetchQuery ({ queryKey: pending.key, queryFn: ({ signal }) => fetchCounts (resource, next, locale, signal), staleTime: 0, retry: false }) if (pendingRun.current !== pending) return if (result.executionAt == null) throw new Error ('Missing Counts execution time') // Preserve presentation changes made while this execution was in flight. for (const key of ['columns', 'children', 'child_sort', 'order', 'nested_json', 'row_numbers']) { if (previous.get (key) === applied.current.get (key)) continue if (key === 'columns') { const columns = selectedCountColumns (resource, next, definition) .filter (column => column !== 'thumbnail') const thumbnail = selectedCountColumns (resource, applied.current, definition) .includes ('thumbnail') next.set ('columns', (thumbnail ? ['thumbnail', ...columns] : columns).join (' ')) continue } next.delete (key) for (const value of applied.current.getAll (key)) next.append (key, value) } const committed = normaliseCountsParams (resource, next, definition) committed.set ('execution_at', result.executionAt) queryClient.setQueryData ( countsKeys.index (resource, countsRequestParams (committed).toString (), locale), result) pending.target = committed.toString () // Commit only after the complete result is available under the final URL's key. if (pending.target === canonical) { pendingRun.current = null setRunning (false) } else setParams (committed, { preventScrollReset: true }) } catch (failure) { if (pendingRun.current !== pending) return pendingRun.current = null setRunning (false) setRunFailure (failure) setRunColumnLimit (tooManyColumns (failure)) setRunError (true) } } const resultParams = placeholder ? retainedParams.current : params const resultMode = resultParams.get ('mode') ?? 'summary' const waiting = running || (!definitionError && (definition == null || pending || fetching || placeholder || (!params.has ('execution_at') && !errorFlg))) const failure = definitionError ? definitionFailure : (runError ? runFailure : queryError) const failureMessage = countsErrorMessage (failure, locale) const columnLimit = runColumnLimit ?? tooManyColumns (queryError) const update = (key: string, value: string, extra?: Record) => { setRunColumnLimit (null) const next = new URLSearchParams (params) if (value || ['sort', 'columns', 'children', 'conditions', 'drill'].includes (key)) setCountState (next, key, value) else next.delete (key) if (extra != null) { for (const key of [...next.keys ()]) if (key === 'measures' || key.startsWith ('measures.')) next.delete (key) for (const [key, value] of Object.entries (extra)) next.set (key, value) } if (key === 'sort') next.delete ('page') setParams (next) } const download = async (format: 'csv' | 'json') => { if (exportInFlight.current || pendingRun.current != null || definition == null) return exportInFlight.current = true setExporting (true) setExportError (null) try { const axes = [...readCountState (params, 'rows', []), ...readCountState (params, 'cols', [])] const fields: CountField[] = mode === 'summary' ? [ ...axes.map (axis => ({ key: axis.field, label: countsFieldLabel (axis.field) })), { key: 'count', label: 'count' }] : selectedCountColumns (resource, params, definition).map (column => fieldsForCounts (resource, definition).find (field => field.key === column)!) const headers = mode === 'summary' ? [...axes.map (axis => countsAxisLabel (axis, definition, locale).text), message ('count').text] : fields.map (field => message (field.label, field.labelValues).text) const payload = countsExportPayload (resource, params, format, fields.map (field => field.key), headers, { ...countsExportLabels (resource, locale, definition), ...Object.fromEntries (countsAxisFields (definition, !state).map (field => [ field.key, message (field.label).text])), count: message ('count').text }, definition) const handle = { cancel: async () => false } await dialogue.form ({ title: label ('exportProgress.title'), footerLayout: 'inline', onCancel: () => handle.cancel (), body: controls => }) } catch (failure) { setExportError ({ failure }) } finally { exportInFlight.current = false setExporting (false) } } return (
{title.text}
{definition != null && update ('order', value)} displayJSON={enabled => update ('nested_json', enabled ? '1' : '0')} displayRowNumbers={enabled => update ('row_numbers', enabled ? '1' : '0')} displayThumbnail={enabled => { const columns = selectedCountColumns (resource, params, definition) .filter (column => column !== 'thumbnail') update ('columns', (enabled ? ['thumbnail', ...columns] : columns).join (' ')) }} execute={next => void execute (next)} actions={
{(['csv', 'json'] as const).map (format => )}
}/>} {exportError != null &&

{label ('errors.exportStart')}
{countsErrorMessage (exportError.failure, locale).text}

}
{columnLimit != null ?

{label ('columnLimit.title')}

{label ('columnLimit.message', { max: columnLimit })}

: ((definitionError || errorFlg || runError) &&

{failureMessage.text}

)}
{mounted && definition != null && data != null && <>

{label (resultMode === 'summary' ? 'outputRows' : 'outputTotal', { value: data.outputCount ?? '—' })} {resultMode === 'details' && data.excludedCountScope === 'current_query' && data.excludedCount != null && data.excludedCount > 0 && ( {label ('excludedRecords', { count: data.excludedCount })})}

{resultMode === 'summary' ? : ( )} }
{waiting &&
{label ('shared:messages.loadingEllipsis')}
}
) } const CountsResourcePage = ({ resource }: { resource: CountsResource }) => { const locale = useLocale () const { data, error, isError: errorFlg } = useQuery ({ queryKey: countsKeys.definition (resource, locale), queryFn: () => fetchCountsDefinition (resource, locale), staleTime: Infinity }) return } export default CountsResourcePage