Add the Statistics pages row for static_page views, wire specials AI match UI, and tidy Businesses page styles. Co-authored-by: Cursor <cursoragent@cursor.com>
163 lines
5.0 KiB
TypeScript
163 lines
5.0 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Breadcrumbs } from '../components/Breadcrumbs'
|
|
import { HomeChartSlot } from '../components/HomeChartSlot'
|
|
import { PageTitle } from '../components/PageTitle'
|
|
import { useTenantBranding } from '../context/TenantBrandingContext'
|
|
import { useLocale } from '@meshkee/dashboard-ui'
|
|
import { useT } from '../i18n/useT'
|
|
import { translate, type BusinessMessageKey } from '../i18n/messages'
|
|
import { ApiError } from '../lib/api'
|
|
import {
|
|
getViewSummary,
|
|
type ViewPeriodCounts,
|
|
type ViewSummaryGroupId,
|
|
} from '../services/dailyActivityService'
|
|
import {
|
|
hasAnyBusinessModule,
|
|
hasBusinessModule,
|
|
isActiveHomeChart,
|
|
type BusinessModuleId,
|
|
} from '../utils/businessModules'
|
|
import pageStyles from '../components/PageContent.module.css'
|
|
import styles from './StatisticsPage.module.css'
|
|
|
|
const EMPTY_COUNTS: ViewPeriodCounts = {
|
|
today: 0,
|
|
week: 0,
|
|
month: 0,
|
|
total: 0,
|
|
}
|
|
|
|
const METRICS: {
|
|
id: keyof ViewPeriodCounts
|
|
labelKey: BusinessMessageKey
|
|
}[] = [
|
|
{ id: 'today', labelKey: 'statistics.metric.today' },
|
|
{ id: 'week', labelKey: 'statistics.metric.week' },
|
|
{ id: 'month', labelKey: 'statistics.metric.month' },
|
|
{ id: 'total', labelKey: 'statistics.metric.total' },
|
|
]
|
|
|
|
const STAT_ROWS: {
|
|
id: ViewSummaryGroupId
|
|
titleKey: BusinessMessageKey
|
|
moduleId?: BusinessModuleId
|
|
anyModuleIds?: BusinessModuleId[]
|
|
}[] = [
|
|
{ id: 'all', titleKey: 'statistics.row.all' },
|
|
{ id: 'home', titleKey: 'statistics.row.home' },
|
|
{ id: 'pages', titleKey: 'statistics.row.pages' },
|
|
{
|
|
id: 'products',
|
|
titleKey: 'statistics.row.products',
|
|
anyModuleIds: ['products', 'store'],
|
|
},
|
|
{ id: 'blog', titleKey: 'statistics.row.blog', moduleId: 'blog' },
|
|
{ id: 'portfolio', titleKey: 'statistics.row.portfolio', moduleId: 'portfolio' },
|
|
{ id: 'videos', titleKey: 'statistics.row.videos', moduleId: 'videos' },
|
|
]
|
|
|
|
function formatCount(value: number) {
|
|
return new Intl.NumberFormat('en-US').format(value)
|
|
}
|
|
|
|
export function StatisticsPage() {
|
|
const t = useT()
|
|
const { locale } = useLocale()
|
|
const { enabledModules, homeCharts } = useTenantBranding()
|
|
const [rows, setRows] = useState<Record<ViewSummaryGroupId, ViewPeriodCounts> | null>(
|
|
null,
|
|
)
|
|
const [error, setError] = useState('')
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
const visibleCharts = homeCharts.filter(isActiveHomeChart)
|
|
const visibleRows = STAT_ROWS.filter((row) => {
|
|
if (row.anyModuleIds) {
|
|
return hasAnyBusinessModule(enabledModules, row.anyModuleIds)
|
|
}
|
|
return !row.moduleId || hasBusinessModule(enabledModules, row.moduleId)
|
|
})
|
|
|
|
useEffect(() => {
|
|
const controller = new AbortController()
|
|
|
|
async function load() {
|
|
setLoading(true)
|
|
setError('')
|
|
try {
|
|
const data = await getViewSummary(controller.signal)
|
|
setRows(data.rows)
|
|
} catch (err) {
|
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
|
setError(err instanceof ApiError ? err.message : t('statistics.errorLoad'))
|
|
} finally {
|
|
if (!controller.signal.aborted) setLoading(false)
|
|
}
|
|
}
|
|
|
|
void load()
|
|
return () => controller.abort()
|
|
}, [t])
|
|
|
|
return (
|
|
<main className={pageStyles.content}>
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: t('nav.home'), href: '/' },
|
|
{ label: t('title.statistics') },
|
|
]}
|
|
/>
|
|
|
|
<div className={pageStyles.pageHeader}>
|
|
<div>
|
|
<PageTitle en="STATISTICS">{t('title.statistics')}</PageTitle>
|
|
<p className={pageStyles.pageSubtitle}>{t('statistics.page.subtitle')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{visibleCharts.length > 0 ? (
|
|
<div className={pageStyles.grid12}>
|
|
{visibleCharts.map((chartId, index) => (
|
|
<div key={`${chartId}-${index}`} className={pageStyles.col6}>
|
|
<HomeChartSlot chartId={chartId} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
|
|
{error ? (
|
|
<div className={styles.error} role="alert">
|
|
{error}
|
|
</div>
|
|
) : null}
|
|
|
|
{visibleRows.map((row) => {
|
|
const counts = rows?.[row.id] ?? EMPTY_COUNTS
|
|
return (
|
|
<article key={row.id} className={styles.row}>
|
|
<div className={styles.rowName}>
|
|
<h3 className={styles.rowTitle}>{t(row.titleKey)}</h3>
|
|
{locale === 'fa' ? (
|
|
<span className={styles.rowTitleEn} lang="en" dir="ltr">
|
|
{translate('en', row.titleKey)}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<div className={styles.chips}>
|
|
{METRICS.map((metric) => (
|
|
<div key={metric.id} className={styles.chip}>
|
|
<strong className={styles.chipValue} lang="en" dir="ltr">
|
|
{loading || !rows ? '—' : formatCount(counts[metric.id])}
|
|
</strong>
|
|
<span className={styles.chipLabel}>{t(metric.labelKey)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</article>
|
|
)
|
|
})}
|
|
</main>
|
|
)
|
|
}
|