diff --git a/apps/business/src/pages/AddNewProductPage.module.css b/apps/business/src/pages/AddNewProductPage.module.css
index ba21292..33d8e45 100644
--- a/apps/business/src/pages/AddNewProductPage.module.css
+++ b/apps/business/src/pages/AddNewProductPage.module.css
@@ -289,26 +289,32 @@
margin-top: 4px;
padding: 3px;
gap: 2px;
- background: rgba(148, 163, 184, 0.14);
- border: 1px solid rgba(148, 163, 184, 0.28);
+ background: transparent;
+ border: 1px solid #0a1628;
border-radius: 50px;
}
.aspectSwitchBtn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
min-width: 52px;
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
line-height: 1.2;
font-family: var(--font-en), var(--font-ui);
- color: var(--text-secondary);
+ color: var(--primary);
background: transparent;
border-radius: 50px;
+ text-align: center;
+ direction: ltr;
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}
.aspectSwitchBtn:hover:not(:disabled) {
color: var(--primary);
+ background: rgba(var(--primary-rgb) / 0.1);
}
.aspectSwitchBtn:disabled {
@@ -324,6 +330,7 @@
.aspectSwitchBtnSelected:hover:not(:disabled) {
color: #fff;
+ background: var(--primary);
}
.videoEmbedRow {
diff --git a/apps/business/src/pages/StatisticsPage.module.css b/apps/business/src/pages/StatisticsPage.module.css
new file mode 100644
index 0000000..e049385
--- /dev/null
+++ b/apps/business/src/pages/StatisticsPage.module.css
@@ -0,0 +1,110 @@
+.row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px 24px;
+ margin-top: 12px;
+ padding: 14px 18px;
+ background: var(--glass-bg);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius);
+ box-shadow: var(--glass-shadow);
+}
+
+.row:first-of-type {
+ margin-top: 24px;
+}
+
+.rowTitle {
+ margin: 0;
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.rowName {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 3px;
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.rowTitleEn {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+ color: var(--text-muted);
+ font-family: var(--font-en), var(--font-ui);
+ line-height: 1.2;
+}
+
+.chips {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 8px;
+ flex: 0 0 auto;
+}
+
+.chip {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ min-width: 76px;
+ padding: 8px 14px;
+ border-radius: 16px;
+ background: rgba(var(--primary-rgb) / 0.1);
+ color: var(--primary);
+}
+
+.chipValue {
+ font-family: var(--font-en), var(--font-ui);
+ font-size: 20px;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: -0.02em;
+ line-height: 1;
+ color: inherit;
+}
+
+.chipLabel {
+ font-size: 11px;
+ font-weight: 500;
+ line-height: 1.2;
+ color: inherit;
+ opacity: 0.85;
+ white-space: nowrap;
+}
+
+.error {
+ margin-top: 20px;
+ padding: 12px 16px;
+ font-size: 14px;
+ color: #b91c1c;
+ background: rgba(239, 68, 68, 0.08);
+ border: 1px solid rgba(239, 68, 68, 0.25);
+ border-radius: var(--radius-sm);
+}
+
+.error + .row {
+ margin-top: 16px;
+}
+
+@media (max-width: 768px) {
+ .row {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 12px;
+ }
+
+ .chips {
+ justify-content: flex-start;
+ }
+}
diff --git a/apps/business/src/pages/StatisticsPage.tsx b/apps/business/src/pages/StatisticsPage.tsx
new file mode 100644
index 0000000..0317967
--- /dev/null
+++ b/apps/business/src/pages/StatisticsPage.tsx
@@ -0,0 +1,161 @@
+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: '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
| 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 (
+
+
+
+
+
+
{t('title.statistics')}
+
{t('statistics.page.subtitle')}
+
+
+
+ {visibleCharts.length > 0 ? (
+
+ {visibleCharts.map((chartId, index) => (
+
+
+
+ ))}
+
+ ) : null}
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+ {visibleRows.map((row) => {
+ const counts = rows?.[row.id] ?? EMPTY_COUNTS
+ return (
+
+
+
{t(row.titleKey)}
+ {locale === 'fa' ? (
+
+ {translate('en', row.titleKey)}
+
+ ) : null}
+
+
+ {METRICS.map((metric) => (
+
+
+ {loading || !rows ? '—' : formatCount(counts[metric.id])}
+
+ {t(metric.labelKey)}
+
+ ))}
+
+
+ )
+ })}
+
+ )
+}
diff --git a/apps/business/src/services/dailyActivityService.ts b/apps/business/src/services/dailyActivityService.ts
index 75ad186..d905442 100644
--- a/apps/business/src/services/dailyActivityService.ts
+++ b/apps/business/src/services/dailyActivityService.ts
@@ -73,3 +73,33 @@ export async function getViewDailyActivity(
{ auth: true, signal },
)
}
+
+export type ViewSummaryGroupId =
+ | 'all'
+ | 'home'
+ | 'products'
+ | 'blog'
+ | 'portfolio'
+ | 'videos'
+
+export interface ViewPeriodCounts {
+ today: number
+ week: number
+ month: number
+ total: number
+}
+
+export interface ViewSummaryResponse {
+ rows: Record
+}
+
+export async function getViewSummary(signal?: AbortSignal) {
+ const businessId = getActiveBusinessId()
+ if (!businessId) {
+ throw new Error('No active business selected. Please sign in again.')
+ }
+ return apiRequest(
+ `/businesses/${businessId}/analytics/views/summary`,
+ { auth: true, signal },
+ )
+}
diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md
index d93c126..a0246a8 100644
--- a/docs/PROJECT_CONTEXT.md
+++ b/docs/PROJECT_CONTEXT.md
@@ -3,7 +3,7 @@
> **For AI agents:** Read this file at the start of a new chat before making changes.
> Update this document when a major feature is completed or architecture changes.
-Last updated: August 31, 2026
+Last updated: September 1, 2026
---
@@ -278,7 +278,8 @@ Products accept optional `brandId` on create/update.
- `website.specialProductsSource`: `product` \| `store_item`. Unset → `store_item` if the store module is enabled, otherwise `product`.
- `modules.enabled`: optional modules — business: `products`, `store`, `portfolio`, `blog`, `warehouse`, `videos`, `finance`; customer: `customer_products`. Missing key → all business modules enabled (legacy); customer modules stay opt-in. Super-admin only for modules PATCH.
- `modules.charts`: two home chart slots (`none`, `orders_30d`, `customers_joined_1y`, `blog_views_30d`, `products_added_1y`, `product_views_30d`, `website_views_30d`, `portfolio_views_30d`); `none` hides that slot; defaults orders + customers. View charts read `website_page_views` (auto-counted on public list/detail GETs; ~6 month retention). `website_views_30d` aggregates **all** page kinds (`kind=all`); product/portfolio/blog charts filter by detail kind. Home-only hits also increment on `GET .../website/sliders` and `GET .../website/static-images?pageKey=home`. Lifetime totals in `website_page_view_counters`.
-- Always-on (not in modules list): business — customers, website, profile, settings, home; customer — home, profile, addresses, orders, favorites
+- **Statistics** (`/statistics`, always-on sidebar): same two home chart slots, then visit KPI rows (today / this week Sat–Fri / this month / lifetime total) for all pages, home, products (+ store items), blog, portfolio, videos — module-gated except total + home. API: `GET businesses/:id/analytics/views/summary`.
+- Always-on (not in modules list): business — home, **statistics**, customers, website, profile, settings; customer — home, profile, addresses, orders, favorites
- Tenant public: `GET /tenants/:host` includes `enabledModules` + `homeCharts` + `specialProductsSource`
### Portfolios
diff --git a/packages/dashboard-ui/src/components/EmptyState.module.css b/packages/dashboard-ui/src/components/EmptyState.module.css
index faf3463..96a85fe 100644
--- a/packages/dashboard-ui/src/components/EmptyState.module.css
+++ b/packages/dashboard-ui/src/components/EmptyState.module.css
@@ -69,9 +69,9 @@
background-color: transparent;
border-width: 2px;
border-style: dashed;
- border-color: #64748b;
+ border-color: #0a1628;
border-radius: 12px;
- color: var(--primary, #0a1628);
+ color: var(--primary);
font-family: var(--font-ui);
text-decoration: none;
cursor: pointer;
@@ -89,18 +89,19 @@ button.addBox {
background-color: transparent;
border-width: 2px;
border-style: dashed;
- border-color: #64748b;
+ border-color: #0a1628;
+ color: var(--primary);
}
.addBox:hover,
a.addBox:hover,
button.addBox:hover {
- background-color: rgba(10, 22, 40, 0.07);
+ background-color: rgba(var(--primary-rgb) / 0.08);
border-style: dashed;
border-color: #0a1628;
- color: #0a1628;
+ color: var(--primary);
transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(15, 23, 42, 0.1);
+ box-shadow: 0 8px 20px rgba(var(--primary-rgb) / 0.18);
}
.addBox:focus-visible {