diff --git a/apps/business/src/i18n/messages.ts b/apps/business/src/i18n/messages.ts index 16be696..99b2fe6 100644 --- a/apps/business/src/i18n/messages.ts +++ b/apps/business/src/i18n/messages.ts @@ -278,7 +278,8 @@ const en = { '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 · {from} – {to}', + 'home.chart.productViews.subtitle': + 'Catalog + customer product page views in the last 30 days · {from} – {to}', 'home.chart.productViews.legend': 'Views ({count})', 'home.chart.productViews.loading': 'Loading chart...', 'home.chart.productViews.error': 'Unable to load product view activity.', @@ -2788,7 +2789,8 @@ const fa: Record = { 'home.chart.blogViews.error': 'بارگذاری بازدید مقالات ممکن نشد.', 'home.chart.blogViews.bar': '{day}: {count} بازدید', 'home.chart.productViews.title': 'بازدید محصولات', - 'home.chart.productViews.subtitle': 'بازدید صفحات محصول در ۳۰ روز گذشته از {from} تا {to}', + 'home.chart.productViews.subtitle': + 'بازدید صفحات محصول و محصولات کاربران در ۳۰ روز گذشته از {from} تا {to}', 'home.chart.productViews.legend': 'بازدید ({count})', 'home.chart.productViews.loading': 'در حال بارگذاری نمودار...', 'home.chart.productViews.error': 'بارگذاری بازدید محصولات ممکن نشد.', diff --git a/apps/business/src/services/dailyActivityService.ts b/apps/business/src/services/dailyActivityService.ts index d905442..cf4a9fa 100644 --- a/apps/business/src/services/dailyActivityService.ts +++ b/apps/business/src/services/dailyActivityService.ts @@ -53,6 +53,8 @@ export type WebsiteViewKind = | 'blog_detail' | 'video_list' | 'video_detail' + | 'user_product_list' + | 'user_product_detail' /** @deprecated aliases still accepted by API */ | 'website' | 'product' diff --git a/apps/business/src/types/userProduct.ts b/apps/business/src/types/userProduct.ts index 414a72f..77e3e41 100644 --- a/apps/business/src/types/userProduct.ts +++ b/apps/business/src/types/userProduct.ts @@ -7,6 +7,9 @@ export interface UserProductListItem { title: string titleFa?: string | null titleEn?: string | null + /** SEO path segment for `/user-products/{id}/{pathSlug}` (from titleFa/titleEn). */ + pathSlug?: string | null + slug?: string | null description?: string | null price: number | null priceCurrency?: UserProductPriceCurrency | string | null diff --git a/apps/business/src/utils/websiteLinks.ts b/apps/business/src/utils/websiteLinks.ts index ff4b2ed..11fe5a4 100644 --- a/apps/business/src/utils/websiteLinks.ts +++ b/apps/business/src/utils/websiteLinks.ts @@ -4,6 +4,7 @@ export type WebsiteLinkKind = 'product' | 'storeItem' | 'blog' | 'portfolio' | ' export type WebsiteContentKind = | 'product' + | 'userProduct' | 'blog' | 'portfolio' | 'video' @@ -61,8 +62,10 @@ export function buildWebsiteContentUrl( item: { id: string slug?: string | null + pathSlug?: string | null title?: string | null titleFa?: string | null + titleEn?: string | null nameFa?: string | null nameEn?: string | null }, @@ -80,6 +83,19 @@ export function buildWebsiteContentUrl( ) return `${origin}/products/${encodeURIComponent(id)}/${encodeURIComponent(pathSlug)}` } + case 'userProduct': { + const pathSlug = + item.pathSlug?.trim() || + slugifyForWebsitePath( + item.titleFa?.trim() || + item.title?.trim() || + item.titleEn?.trim() || + item.nameEn?.trim() || + '', + 'user-product', + ) + return `${origin}/user-products/${encodeURIComponent(id)}/${encodeURIComponent(pathSlug)}` + } case 'blog': { const slug = item.slug?.trim() return slug ? `${origin}/blog/${encodeURIComponent(slug)}` : '' diff --git a/apps/customer/src/types/userProduct.ts b/apps/customer/src/types/userProduct.ts index 2423592..ae83508 100644 --- a/apps/customer/src/types/userProduct.ts +++ b/apps/customer/src/types/userProduct.ts @@ -7,6 +7,9 @@ export interface UserProductListItem { title: string titleFa?: string | null titleEn?: string | null + /** SEO path segment for `/user-products/{id}/{pathSlug}` (from titleFa/titleEn). */ + pathSlug?: string | null + slug?: string | null description?: string | null price: number | null priceCurrency?: UserProductPriceCurrency | string | null diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index 6d58da5..becd98f 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -281,8 +281,8 @@ Products accept optional `brandId` on create/update. - Per-entity optional SEO overrides (`seoMetaTitle`, `seoMetaDescription`) on products, blogs, portfolios, videos, instructions, workshops, user-products, and categories (stored in `metadata` JSON). - **Torob:** store-module websites with Store settings → Torob on. Nginx on the shop apex proxies `POST /torob_api/v3/products` to `POST /api/v1/tenants/{domain}/torob_api/v3/products`. Otherwise 404. Do not implement this in Next.js. - `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`. -- **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`. +- `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 chart counts `product_detail` + `user_product_detail`; 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`. +- **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 + user products), 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`