From 202ac7d4bdbf45ea5fbb3696443b4d4916deca22 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Wed, 26 Aug 2026 01:12:45 +0330 Subject: [PATCH] Add view traffic charts to business home and super-admin. Wire product, website, portfolio, and blog 30-day view charts to the analytics API and expose the new chart types in super-admin business settings. Co-authored-by: Cursor --- .../src/components/DailyActivityChart.tsx | 59 +++++----- .../business/src/components/HomeChartSlot.tsx | 104 ++++++++++++++---- apps/business/src/i18n/messages.ts | 50 ++++++++- .../src/services/dailyActivityService.ts | 17 +++ apps/business/src/utils/businessModules.ts | 3 + apps/super-admin/src/utils/businessModules.ts | 8 +- docs/PROJECT_CONTEXT.md | 2 +- 7 files changed, 188 insertions(+), 55 deletions(-) diff --git a/apps/business/src/components/DailyActivityChart.tsx b/apps/business/src/components/DailyActivityChart.tsx index 6cd8fe5..69bd6c8 100644 --- a/apps/business/src/components/DailyActivityChart.tsx +++ b/apps/business/src/components/DailyActivityChart.tsx @@ -25,6 +25,8 @@ interface DailyActivityChartProps { load: (signal: AbortSignal) => Promise /** Day bars (default) or roll up into last 12 months. */ granularity?: 'day' | 'month' + /** Hide the secondary series (for single-metric view charts). */ + showSecondary?: boolean } function formatDayLabel(dateKey: string, locale: string): string { @@ -47,6 +49,7 @@ export function DailyActivityChart({ secondaryBarTitleKey, load, granularity = 'day', + showSecondary = true, }: DailyActivityChartProps) { const t = useT() const { locale } = useLocale() @@ -120,10 +123,12 @@ export function DailyActivityChart({ {t(primaryLegendKey, { count: primaryTotal })} - - - {t(secondaryLegendKey, { count: secondaryTotal })} - + {showSecondary ? ( + + + {t(secondaryLegendKey, { count: secondaryTotal })} + + ) : null} @@ -159,17 +164,19 @@ export function DailyActivityChart({ count: item.primary, })} /> -
0 ? 4 : 0), - }} - title={t(secondaryBarTitleKey, { - day: item.label, - month: item.label, - count: item.secondary, - })} - /> + {showSecondary ? ( +
0 ? 4 : 0), + }} + title={t(secondaryBarTitleKey, { + day: item.label, + month: item.label, + count: item.secondary, + })} + /> + ) : null}
0 ? 4 : 0) }} title={t(primaryBarTitleKey, { day: label, count: item.count })} /> -
0 ? 4 : 0), - }} - title={t(secondaryBarTitleKey, { - day: label, - count: secondaryCount, - })} - /> + {showSecondary ? ( +
0 ? 4 : 0), + }} + title={t(secondaryBarTitleKey, { + day: label, + count: secondaryCount, + })} + /> + ) : null}
{label} diff --git a/apps/business/src/components/HomeChartSlot.tsx b/apps/business/src/components/HomeChartSlot.tsx index 33a2e53..cdd9ca2 100644 --- a/apps/business/src/components/HomeChartSlot.tsx +++ b/apps/business/src/components/HomeChartSlot.tsx @@ -4,36 +4,93 @@ import { ProductActivityChart } from './ProductActivityChart' import { getCustomersDailyActivity, getOrdersDailyActivity, + getViewDailyActivity, + type WebsiteViewKind, } from '../services/dailyActivityService' import type { HomeChartId } from '../utils/businessModules' -import { useT } from '../i18n/useT' -import styles from './DailyActivityChart.module.css' +import type { BusinessMessageKey } from '../i18n/messages' interface HomeChartSlotProps { chartId: HomeChartId } -function HomeChartPlaceholder({ chartId }: { chartId: HomeChartId }) { - const t = useT() - const titleKey = - chartId === 'blog_views_30d' - ? 'home.chart.blogViews.title' - : 'home.chart.placeholder.title' - const subtitleKey = - chartId === 'blog_views_30d' - ? 'home.chart.blogViews.subtitle' - : 'home.chart.placeholder.subtitle' +const VIEW_CHART_CONFIG: Record< + Extract< + HomeChartId, + 'blog_views_30d' | 'product_views_30d' | 'website_views_30d' | 'portfolio_views_30d' + >, + { + kind: WebsiteViewKind + titleKey: BusinessMessageKey + subtitleKey: BusinessMessageKey + legendKey: BusinessMessageKey + loadingKey: BusinessMessageKey + errorKey: BusinessMessageKey + barKey: BusinessMessageKey + } +> = { + product_views_30d: { + kind: 'product', + titleKey: 'home.chart.productViews.title', + subtitleKey: 'home.chart.productViews.subtitle', + legendKey: 'home.chart.productViews.legend', + loadingKey: 'home.chart.productViews.loading', + errorKey: 'home.chart.productViews.error', + barKey: 'home.chart.productViews.bar', + }, + website_views_30d: { + kind: 'website', + titleKey: 'home.chart.websiteViews.title', + subtitleKey: 'home.chart.websiteViews.subtitle', + legendKey: 'home.chart.websiteViews.legend', + loadingKey: 'home.chart.websiteViews.loading', + errorKey: 'home.chart.websiteViews.error', + barKey: 'home.chart.websiteViews.bar', + }, + portfolio_views_30d: { + kind: 'portfolio', + titleKey: 'home.chart.portfolioViews.title', + subtitleKey: 'home.chart.portfolioViews.subtitle', + legendKey: 'home.chart.portfolioViews.legend', + loadingKey: 'home.chart.portfolioViews.loading', + errorKey: 'home.chart.portfolioViews.error', + barKey: 'home.chart.portfolioViews.bar', + }, + blog_views_30d: { + kind: 'blog', + titleKey: 'home.chart.blogViews.title', + subtitleKey: 'home.chart.blogViews.subtitle', + legendKey: 'home.chart.blogViews.legend', + loadingKey: 'home.chart.blogViews.loading', + errorKey: 'home.chart.blogViews.error', + barKey: 'home.chart.blogViews.bar', + }, +} + +function ViewActivityChart({ + chartId, +}: { + chartId: keyof typeof VIEW_CHART_CONFIG +}) { + const config = VIEW_CHART_CONFIG[chartId] + const load = useCallback( + (signal: AbortSignal) => getViewDailyActivity(config.kind, 30, signal), + [config.kind], + ) return ( -
-
-
-

{t(titleKey)}

-

{t(subtitleKey)}

-
-
-

{t('home.chart.placeholder.comingSoon')}

-
+ ) } @@ -81,8 +138,11 @@ export function HomeChartSlot({ chartId }: HomeChartSlotProps) { ) case 'products_added_1y': return + case 'product_views_30d': + case 'website_views_30d': + case 'portfolio_views_30d': case 'blog_views_30d': - return + return default: return null } diff --git a/apps/business/src/i18n/messages.ts b/apps/business/src/i18n/messages.ts index 938e87d..78a5114 100644 --- a/apps/business/src/i18n/messages.ts +++ b/apps/business/src/i18n/messages.ts @@ -270,9 +270,28 @@ const en = { 'home.chart.customersJoined.activeBar': '{month}: {count} active', 'home.chart.blogViews.title': 'Article views', 'home.chart.blogViews.subtitle': 'Blog article views in the last 30 days', - 'home.chart.placeholder.title': 'Chart', - 'home.chart.placeholder.subtitle': 'This chart will appear here once available.', - 'home.chart.placeholder.comingSoon': 'Coming soon', + 'home.chart.blogViews.legend': 'Views ({count})', + 'home.chart.blogViews.loading': 'Loading chart...', + 'home.chart.blogViews.error': 'Unable to load blog view activity.', + 'home.chart.blogViews.bar': '{day}: {count} views', + 'home.chart.productViews.title': 'Product views', + 'home.chart.productViews.subtitle': 'Product page views in the last 30 days', + 'home.chart.productViews.legend': 'Views ({count})', + 'home.chart.productViews.loading': 'Loading chart...', + 'home.chart.productViews.error': 'Unable to load product view activity.', + 'home.chart.productViews.bar': '{day}: {count} views', + 'home.chart.websiteViews.title': 'Website views', + 'home.chart.websiteViews.subtitle': 'Storefront page views in the last 30 days', + 'home.chart.websiteViews.legend': 'Views ({count})', + 'home.chart.websiteViews.loading': 'Loading chart...', + 'home.chart.websiteViews.error': 'Unable to load website view activity.', + 'home.chart.websiteViews.bar': '{day}: {count} views', + 'home.chart.portfolioViews.title': 'Portfolio views', + 'home.chart.portfolioViews.subtitle': 'Portfolio page views in the last 30 days', + 'home.chart.portfolioViews.legend': 'Views ({count})', + 'home.chart.portfolioViews.loading': 'Loading chart...', + 'home.chart.portfolioViews.error': 'Unable to load portfolio view activity.', + 'home.chart.portfolioViews.bar': '{day}: {count} views', 'products.overview.subtitle': 'Manage your products, inventory and categories.', 'products.card.list.desc': 'View, edit and manage all your existing products.', @@ -2510,9 +2529,28 @@ const fa: Record = { 'home.chart.customersJoined.activeBar': '{month}: {count} فعال', 'home.chart.blogViews.title': 'بازدید مقالات', 'home.chart.blogViews.subtitle': 'بازدید مقالات در ۳۰ روز گذشته', - 'home.chart.placeholder.title': 'نمودار', - 'home.chart.placeholder.subtitle': 'این نمودار به‌زودی در دسترس خواهد بود.', - 'home.chart.placeholder.comingSoon': 'به‌زودی', + 'home.chart.blogViews.legend': 'بازدید ({count})', + 'home.chart.blogViews.loading': 'در حال بارگذاری نمودار...', + 'home.chart.blogViews.error': 'بارگذاری بازدید مقالات ممکن نشد.', + 'home.chart.blogViews.bar': '{day}: {count} بازدید', + 'home.chart.productViews.title': 'بازدید محصولات', + 'home.chart.productViews.subtitle': 'بازدید صفحات محصول در ۳۰ روز گذشته', + 'home.chart.productViews.legend': 'بازدید ({count})', + 'home.chart.productViews.loading': 'در حال بارگذاری نمودار...', + 'home.chart.productViews.error': 'بارگذاری بازدید محصولات ممکن نشد.', + 'home.chart.productViews.bar': '{day}: {count} بازدید', + 'home.chart.websiteViews.title': 'بازدید وب‌سایت', + 'home.chart.websiteViews.subtitle': 'بازدید صفحات فروشگاه در ۳۰ روز گذشته', + 'home.chart.websiteViews.legend': 'بازدید ({count})', + 'home.chart.websiteViews.loading': 'در حال بارگذاری نمودار...', + 'home.chart.websiteViews.error': 'بارگذاری بازدید وب‌سایت ممکن نشد.', + 'home.chart.websiteViews.bar': '{day}: {count} بازدید', + 'home.chart.portfolioViews.title': 'بازدید نمونه‌کارها', + 'home.chart.portfolioViews.subtitle': 'بازدید صفحات نمونه‌کار در ۳۰ روز گذشته', + 'home.chart.portfolioViews.legend': 'بازدید ({count})', + 'home.chart.portfolioViews.loading': 'در حال بارگذاری نمودار...', + 'home.chart.portfolioViews.error': 'بارگذاری بازدید نمونه‌کارها ممکن نشد.', + 'home.chart.portfolioViews.bar': '{day}: {count} بازدید', 'products.overview.subtitle': 'محصولات، موجودی و دسته‌بندی‌ها را مدیریت کنید.', 'products.card.list.desc': 'همه محصولات موجود را ببینید، ویرایش و مدیریت کنید.', diff --git a/apps/business/src/services/dailyActivityService.ts b/apps/business/src/services/dailyActivityService.ts index f4b1709..48d74ea 100644 --- a/apps/business/src/services/dailyActivityService.ts +++ b/apps/business/src/services/dailyActivityService.ts @@ -39,3 +39,20 @@ export async function getCustomersDailyActivity(days = 30, signal?: AbortSignal) { auth: true, signal }, ) } + +export type WebsiteViewKind = 'website' | 'product' | 'portfolio' | 'blog' + +export async function getViewDailyActivity( + kind: WebsiteViewKind, + days = 30, + signal?: AbortSignal, +) { + const businessId = getActiveBusinessId() + if (!businessId) { + throw new Error('No active business selected. Please sign in again.') + } + return apiRequest( + `/businesses/${businessId}/analytics/views/activity?kind=${kind}&days=${days}`, + { auth: true, signal }, + ) +} diff --git a/apps/business/src/utils/businessModules.ts b/apps/business/src/utils/businessModules.ts index a45d83b..3507d39 100644 --- a/apps/business/src/utils/businessModules.ts +++ b/apps/business/src/utils/businessModules.ts @@ -32,6 +32,9 @@ export const HOME_CHART_IDS = [ 'customers_joined_1y', 'blog_views_30d', 'products_added_1y', + 'product_views_30d', + 'website_views_30d', + 'portfolio_views_30d', ] as const export type HomeChartId = (typeof HOME_CHART_IDS)[number] diff --git a/apps/super-admin/src/utils/businessModules.ts b/apps/super-admin/src/utils/businessModules.ts index 5ede19a..0012307 100644 --- a/apps/super-admin/src/utils/businessModules.ts +++ b/apps/super-admin/src/utils/businessModules.ts @@ -56,6 +56,9 @@ export const HOME_CHART_IDS = [ 'customers_joined_1y', 'blog_views_30d', 'products_added_1y', + 'product_views_30d', + 'website_views_30d', + 'portfolio_views_30d', ] as const export type HomeChartId = (typeof HOME_CHART_IDS)[number] @@ -64,8 +67,11 @@ export const HOME_CHART_LABELS: Record = { none: 'No chart', orders_30d: 'Orders in past 30 days', customers_joined_1y: 'Joined users in last year', - blog_views_30d: 'Blog visit in last month', + blog_views_30d: 'Blog visit in last 30 days', products_added_1y: 'Added product in last year', + product_views_30d: 'Product visit in last 30 days', + website_views_30d: 'Website visit in last 30 days', + portfolio_views_30d: 'Portfolio visit in last 30 days', } /** diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index a0da894..a2db652 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -276,7 +276,7 @@ Products accept optional `brandId` on create/update. - `GET/PATCH /businesses/:businessId/settings` — `branding`, `dashboard`, `store`, `modules`, `website` - `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`); `none` hides that slot; defaults orders + customers. +- `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 need `POST /tenants/{domain}/analytics/views` from the storefront. - Always-on (not in modules list): business — customers, website, profile, settings, home; customer — home, profile, addresses, orders, favorites - Tenant public: `GET /tenants/:host` includes `enabledModules` + `homeCharts` + `specialProductsSource`