mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Add business modules column and selectable home charts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
a48a24e6f2
commit
0a2358b0c2
@@ -0,0 +1,88 @@
|
||||
import { useCallback } from 'react'
|
||||
import { DailyActivityChart } from './DailyActivityChart'
|
||||
import { ProductActivityChart } from './ProductActivityChart'
|
||||
import {
|
||||
getCustomersDailyActivity,
|
||||
getOrdersDailyActivity,
|
||||
} from '../services/dailyActivityService'
|
||||
import type { HomeChartId } from '../utils/businessModules'
|
||||
import { useT } from '../i18n/useT'
|
||||
import styles from './DailyActivityChart.module.css'
|
||||
|
||||
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'
|
||||
|
||||
return (
|
||||
<section className={styles.card} aria-label={t(titleKey)}>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<h3 className={styles.title}>{t(titleKey)}</h3>
|
||||
<p className={styles.subtitle}>{t(subtitleKey)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className={styles.status}>{t('home.chart.placeholder.comingSoon')}</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function HomeChartSlot({ chartId }: HomeChartSlotProps) {
|
||||
const loadOrders = useCallback(
|
||||
(signal: AbortSignal) => getOrdersDailyActivity(30, signal),
|
||||
[],
|
||||
)
|
||||
const loadCustomersYear = useCallback(
|
||||
(signal: AbortSignal) => getCustomersDailyActivity(365, signal),
|
||||
[],
|
||||
)
|
||||
|
||||
switch (chartId) {
|
||||
case 'none':
|
||||
return null
|
||||
case 'orders_30d':
|
||||
return (
|
||||
<DailyActivityChart
|
||||
titleKey="home.chart.orders.title"
|
||||
subtitleKey="home.chart.orders.subtitle"
|
||||
primaryLegendKey="home.chart.orders.legend"
|
||||
secondaryLegendKey="home.chart.orders.cartLegend"
|
||||
loadingKey="home.chart.orders.loading"
|
||||
errorKey="home.chart.orders.error"
|
||||
primaryBarTitleKey="home.chart.orders.bar"
|
||||
secondaryBarTitleKey="home.chart.orders.cartBar"
|
||||
load={loadOrders}
|
||||
/>
|
||||
)
|
||||
case 'customers_joined_1y':
|
||||
return (
|
||||
<DailyActivityChart
|
||||
titleKey="home.chart.customersJoined.title"
|
||||
subtitleKey="home.chart.customersJoined.subtitle"
|
||||
primaryLegendKey="home.chart.customersJoined.legend"
|
||||
secondaryLegendKey="home.chart.customersJoined.activeLegend"
|
||||
loadingKey="home.chart.customersJoined.loading"
|
||||
errorKey="home.chart.customersJoined.error"
|
||||
primaryBarTitleKey="home.chart.customersJoined.bar"
|
||||
secondaryBarTitleKey="home.chart.customersJoined.activeBar"
|
||||
load={loadCustomersYear}
|
||||
/>
|
||||
)
|
||||
case 'products_added_1y':
|
||||
return <ProductActivityChart />
|
||||
case 'blog_views_30d':
|
||||
return <HomeChartPlaceholder chartId={chartId} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,10 @@
|
||||
grid-column: span 6;
|
||||
}
|
||||
|
||||
.col12 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
@media (max-width: 1536px) {
|
||||
.gridHome {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
@@ -26,6 +26,11 @@ import {
|
||||
} from '../lib/businessContext'
|
||||
import { isAbortError } from '../lib/api'
|
||||
import { getBusinessProfile } from '../services/businessProfileService'
|
||||
import { useTenantBranding } from '../context/TenantBrandingContext'
|
||||
import {
|
||||
hasBusinessModule,
|
||||
type BusinessModuleId,
|
||||
} from '../utils/businessModules'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import styles from './Sidebar.module.css'
|
||||
|
||||
@@ -41,6 +46,7 @@ interface NavGroup {
|
||||
labelKey: BusinessMessageKey
|
||||
basePath: string
|
||||
children: NavChild[]
|
||||
moduleId?: BusinessModuleId
|
||||
}
|
||||
|
||||
interface NavLinkItem {
|
||||
@@ -49,6 +55,7 @@ interface NavLinkItem {
|
||||
icon: LucideIcon
|
||||
labelKey: BusinessMessageKey
|
||||
to: string
|
||||
moduleId?: BusinessModuleId
|
||||
}
|
||||
|
||||
type NavItem = NavLinkItem | NavGroup
|
||||
@@ -62,6 +69,7 @@ export function Sidebar() {
|
||||
const navigate = useNavigate()
|
||||
const { user, logout } = useAuth()
|
||||
const { locale } = useLocale()
|
||||
const { enabledModules } = useTenantBranding()
|
||||
const t = useT()
|
||||
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({})
|
||||
const [brandLogoUrl, setBrandLogoUrl] = useState<string | null>(null)
|
||||
@@ -79,96 +87,104 @@ export function Sidebar() {
|
||||
}, [locale, nameEn, nameFa, fallbackBusinessName])
|
||||
|
||||
const navItems = useMemo<NavItem[]>(
|
||||
() => [
|
||||
{ type: 'link', id: 'home', icon: Home, labelKey: 'nav.home', to: '/' },
|
||||
{
|
||||
type: 'link',
|
||||
id: 'business-profile',
|
||||
icon: Building2,
|
||||
labelKey: 'nav.businessProfile',
|
||||
to: '/business-profile',
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
id: 'products',
|
||||
icon: ShoppingBag,
|
||||
labelKey: 'nav.products',
|
||||
basePath: '/products',
|
||||
children: [
|
||||
{ labelKey: 'nav.products.overview', to: '/products' },
|
||||
{ labelKey: 'nav.products.list', to: '/products/list' },
|
||||
{ labelKey: 'nav.products.new', to: '/products/new' },
|
||||
{ labelKey: 'nav.products.categories', to: '/products/categories' },
|
||||
{ labelKey: 'nav.products.brands', to: '/products/brands' },
|
||||
{ labelKey: 'nav.products.settings', to: '/products/settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
id: 'store',
|
||||
icon: Store,
|
||||
labelKey: 'nav.store',
|
||||
basePath: '/store',
|
||||
children: [
|
||||
{ labelKey: 'nav.store.overview', to: '/store' },
|
||||
{ labelKey: 'nav.store.items', to: '/store/items' },
|
||||
{ labelKey: 'nav.store.orders', to: '/store/orders' },
|
||||
{ labelKey: 'nav.store.shipping', to: '/store/shipping' },
|
||||
{ labelKey: 'nav.store.cards', to: '/store/cards' },
|
||||
{ labelKey: 'nav.store.settings', to: '/store/settings' },
|
||||
],
|
||||
},
|
||||
{ type: 'link', id: 'customers', icon: Users, labelKey: 'nav.customers', to: '/customers' },
|
||||
{
|
||||
type: 'group',
|
||||
id: 'blog',
|
||||
icon: FileText,
|
||||
labelKey: 'nav.blog',
|
||||
basePath: '/blog',
|
||||
children: [
|
||||
{ labelKey: 'nav.blog.overview', to: '/blog' },
|
||||
{ labelKey: 'nav.blog.list', to: '/blog/list' },
|
||||
{ labelKey: 'nav.blog.new', to: '/blog/new' },
|
||||
{ labelKey: 'nav.blog.categories', to: '/blog/categories' },
|
||||
{ labelKey: 'nav.blog.settings', to: '/blog/settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
id: 'portfolios',
|
||||
icon: Briefcase,
|
||||
labelKey: 'nav.portfolios',
|
||||
basePath: '/portfolios',
|
||||
children: [
|
||||
{ labelKey: 'nav.portfolios.overview', to: '/portfolios' },
|
||||
{ labelKey: 'nav.portfolios.list', to: '/portfolios/list' },
|
||||
{ labelKey: 'nav.portfolios.new', to: '/portfolios/new' },
|
||||
{ labelKey: 'nav.portfolios.categories', to: '/portfolios/categories' },
|
||||
{ labelKey: 'nav.portfolios.settings', to: '/portfolios/settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
id: 'website',
|
||||
icon: Globe,
|
||||
labelKey: 'nav.website',
|
||||
basePath: '/website',
|
||||
children: [
|
||||
{ labelKey: 'nav.website.overview', to: '/website' },
|
||||
{ labelKey: 'nav.website.sliders', to: '/website/sliders' },
|
||||
{ labelKey: 'nav.website.specialCategories', to: '/website/special-categories' },
|
||||
{ labelKey: 'nav.website.specialBrands', to: '/website/special-brands' },
|
||||
{ labelKey: 'nav.website.specialItems', to: '/website/special-items' },
|
||||
{ labelKey: 'nav.website.contact', to: '/website/contact' },
|
||||
{ labelKey: 'nav.website.subscriptions', to: '/website/subscriptions' },
|
||||
{ labelKey: 'nav.website.faq', to: '/website/faq' },
|
||||
{ labelKey: 'nav.website.badges', to: '/website/badges' },
|
||||
{ labelKey: 'nav.website.ePayment', to: '/website/e-payment' },
|
||||
],
|
||||
},
|
||||
{ type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' },
|
||||
],
|
||||
[],
|
||||
() =>
|
||||
[
|
||||
{ type: 'link', id: 'home', icon: Home, labelKey: 'nav.home', to: '/' },
|
||||
{
|
||||
type: 'link',
|
||||
id: 'business-profile',
|
||||
icon: Building2,
|
||||
labelKey: 'nav.businessProfile',
|
||||
to: '/business-profile',
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
id: 'products',
|
||||
icon: ShoppingBag,
|
||||
labelKey: 'nav.products',
|
||||
basePath: '/products',
|
||||
moduleId: 'products',
|
||||
children: [
|
||||
{ labelKey: 'nav.products.overview', to: '/products' },
|
||||
{ labelKey: 'nav.products.list', to: '/products/list' },
|
||||
{ labelKey: 'nav.products.new', to: '/products/new' },
|
||||
{ labelKey: 'nav.products.categories', to: '/products/categories' },
|
||||
{ labelKey: 'nav.products.brands', to: '/products/brands' },
|
||||
{ labelKey: 'nav.products.settings', to: '/products/settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
id: 'store',
|
||||
icon: Store,
|
||||
labelKey: 'nav.store',
|
||||
basePath: '/store',
|
||||
moduleId: 'store',
|
||||
children: [
|
||||
{ labelKey: 'nav.store.overview', to: '/store' },
|
||||
{ labelKey: 'nav.store.items', to: '/store/items' },
|
||||
{ labelKey: 'nav.store.orders', to: '/store/orders' },
|
||||
{ labelKey: 'nav.store.shipping', to: '/store/shipping' },
|
||||
{ labelKey: 'nav.store.cards', to: '/store/cards' },
|
||||
{ labelKey: 'nav.store.settings', to: '/store/settings' },
|
||||
],
|
||||
},
|
||||
{ type: 'link', id: 'customers', icon: Users, labelKey: 'nav.customers', to: '/customers' },
|
||||
{
|
||||
type: 'group',
|
||||
id: 'blog',
|
||||
icon: FileText,
|
||||
labelKey: 'nav.blog',
|
||||
basePath: '/blog',
|
||||
moduleId: 'blog',
|
||||
children: [
|
||||
{ labelKey: 'nav.blog.overview', to: '/blog' },
|
||||
{ labelKey: 'nav.blog.list', to: '/blog/list' },
|
||||
{ labelKey: 'nav.blog.new', to: '/blog/new' },
|
||||
{ labelKey: 'nav.blog.categories', to: '/blog/categories' },
|
||||
{ labelKey: 'nav.blog.settings', to: '/blog/settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
id: 'portfolios',
|
||||
icon: Briefcase,
|
||||
labelKey: 'nav.portfolios',
|
||||
basePath: '/portfolios',
|
||||
moduleId: 'portfolio',
|
||||
children: [
|
||||
{ labelKey: 'nav.portfolios.overview', to: '/portfolios' },
|
||||
{ labelKey: 'nav.portfolios.list', to: '/portfolios/list' },
|
||||
{ labelKey: 'nav.portfolios.new', to: '/portfolios/new' },
|
||||
{ labelKey: 'nav.portfolios.categories', to: '/portfolios/categories' },
|
||||
{ labelKey: 'nav.portfolios.settings', to: '/portfolios/settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
id: 'website',
|
||||
icon: Globe,
|
||||
labelKey: 'nav.website',
|
||||
basePath: '/website',
|
||||
children: [
|
||||
{ labelKey: 'nav.website.overview', to: '/website' },
|
||||
{ labelKey: 'nav.website.sliders', to: '/website/sliders' },
|
||||
{ labelKey: 'nav.website.specialCategories', to: '/website/special-categories' },
|
||||
{ labelKey: 'nav.website.specialBrands', to: '/website/special-brands' },
|
||||
{ labelKey: 'nav.website.specialItems', to: '/website/special-items' },
|
||||
{ labelKey: 'nav.website.contact', to: '/website/contact' },
|
||||
{ labelKey: 'nav.website.subscriptions', to: '/website/subscriptions' },
|
||||
{ labelKey: 'nav.website.faq', to: '/website/faq' },
|
||||
{ labelKey: 'nav.website.badges', to: '/website/badges' },
|
||||
{ labelKey: 'nav.website.ePayment', to: '/website/e-payment' },
|
||||
],
|
||||
},
|
||||
{ type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' },
|
||||
].filter(
|
||||
(item) =>
|
||||
!item.moduleId || hasBusinessModule(enabledModules, item.moduleId),
|
||||
),
|
||||
[enabledModules],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -15,12 +15,22 @@ import { getBusinessDomain } from '../lib/config'
|
||||
import { BUSINESS_PROFILE_UPDATED_EVENT } from '../lib/businessContext'
|
||||
import { getBusinessProfile } from '../services/businessProfileService'
|
||||
import { resolveTenantByDomain } from '../services/tenantService'
|
||||
import {
|
||||
DEFAULT_ENABLED_BUSINESS_MODULES,
|
||||
DEFAULT_HOME_CHARTS,
|
||||
normalizeEnabledBusinessModules,
|
||||
normalizeHomeCharts,
|
||||
type BusinessModuleId,
|
||||
type HomeChartId,
|
||||
} from '../utils/businessModules'
|
||||
|
||||
interface TenantBrandingContextValue {
|
||||
businessName: string
|
||||
businessNameEn: string
|
||||
logoUrl: string | null
|
||||
faviconUrl: string | null
|
||||
enabledModules: BusinessModuleId[]
|
||||
homeCharts: [HomeChartId, HomeChartId]
|
||||
refreshBranding: () => void
|
||||
}
|
||||
|
||||
@@ -42,6 +52,12 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
|
||||
const [nameFa, setNameFa] = useState('')
|
||||
const [logoUrl, setLogoUrl] = useState<string | null>(null)
|
||||
const [faviconUrl, setFaviconUrl] = useState<string | null>(null)
|
||||
const [enabledModules, setEnabledModules] = useState<BusinessModuleId[]>(
|
||||
DEFAULT_ENABLED_BUSINESS_MODULES,
|
||||
)
|
||||
const [homeCharts, setHomeCharts] = useState<[HomeChartId, HomeChartId]>([
|
||||
...DEFAULT_HOME_CHARTS,
|
||||
])
|
||||
const [refreshToken, setRefreshToken] = useState(0)
|
||||
const defaultLocaleAppliedRef = useRef(false)
|
||||
|
||||
@@ -95,6 +111,8 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
|
||||
setNameFa(nextNameFa || nextNameEn || domain)
|
||||
setLogoUrl(nextLogo)
|
||||
setFaviconUrl(nextFavicon)
|
||||
setEnabledModules(normalizeEnabledBusinessModules(tenant.enabledModules))
|
||||
setHomeCharts(normalizeHomeCharts(tenant.homeCharts))
|
||||
applyDocumentFavicon(nextFavicon)
|
||||
if (!defaultLocaleAppliedRef.current) {
|
||||
defaultLocaleAppliedRef.current = true
|
||||
@@ -112,6 +130,8 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
|
||||
setNameFa(domain)
|
||||
setLogoUrl(null)
|
||||
setFaviconUrl(null)
|
||||
setEnabledModules([...DEFAULT_ENABLED_BUSINESS_MODULES])
|
||||
setHomeCharts([...DEFAULT_HOME_CHARTS])
|
||||
applyDocumentFavicon(null)
|
||||
}
|
||||
}
|
||||
@@ -142,8 +162,24 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
|
||||
)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ businessName, businessNameEn, logoUrl, faviconUrl, refreshBranding }),
|
||||
[businessName, businessNameEn, logoUrl, faviconUrl, refreshBranding],
|
||||
() => ({
|
||||
businessName,
|
||||
businessNameEn,
|
||||
logoUrl,
|
||||
faviconUrl,
|
||||
enabledModules,
|
||||
homeCharts,
|
||||
refreshBranding,
|
||||
}),
|
||||
[
|
||||
businessName,
|
||||
businessNameEn,
|
||||
logoUrl,
|
||||
faviconUrl,
|
||||
enabledModules,
|
||||
homeCharts,
|
||||
refreshBranding,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -192,6 +192,19 @@ const en = {
|
||||
'home.chart.customers.error': 'Unable to load customer activity.',
|
||||
'home.chart.customers.bar': '{day}: {count} registered',
|
||||
'home.chart.customers.activeBar': '{day}: {count} active',
|
||||
'home.chart.customersJoined.title': 'Customers joined',
|
||||
'home.chart.customersJoined.subtitle': 'Registrations and active users in the last year',
|
||||
'home.chart.customersJoined.legend': 'Registered ({count})',
|
||||
'home.chart.customersJoined.activeLegend': 'Active ({count})',
|
||||
'home.chart.customersJoined.loading': 'Loading chart...',
|
||||
'home.chart.customersJoined.error': 'Unable to load customer activity.',
|
||||
'home.chart.customersJoined.bar': '{day}: {count} registered',
|
||||
'home.chart.customersJoined.activeBar': '{day}: {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',
|
||||
|
||||
'products.overview.subtitle': 'Manage your products, inventory and categories.',
|
||||
'products.card.list.desc': 'View, edit and manage all your existing products.',
|
||||
@@ -1231,7 +1244,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'home.card.website.link': 'مشاهده وبسایت',
|
||||
|
||||
'home.chart.orders.title': 'سفارشها',
|
||||
'home.chart.orders.subtitle': 'سفارشها و افزودن به سبد در ۳۰ روز گذشته',
|
||||
'home.chart.orders.subtitle': 'سفارشها در ۳۰ روز گذشته',
|
||||
'home.chart.orders.legend': 'سفارش ({count})',
|
||||
'home.chart.orders.cartLegend': 'افزودن به سبد ({count})',
|
||||
'home.chart.orders.loading': 'در حال بارگذاری نمودار...',
|
||||
@@ -1246,6 +1259,19 @@ const fa: Record<MessageKey, string> = {
|
||||
'home.chart.customers.error': 'بارگذاری فعالیت مشتریان ممکن نشد.',
|
||||
'home.chart.customers.bar': '{day}: {count} ثبتنام',
|
||||
'home.chart.customers.activeBar': '{day}: {count} فعال',
|
||||
'home.chart.customersJoined.title': 'مشتریان عضو شده',
|
||||
'home.chart.customersJoined.subtitle': 'مشتریان عضو شده در ۱ سال گذشته',
|
||||
'home.chart.customersJoined.legend': 'ثبتنام ({count})',
|
||||
'home.chart.customersJoined.activeLegend': 'فعال ({count})',
|
||||
'home.chart.customersJoined.loading': 'در حال بارگذاری نمودار...',
|
||||
'home.chart.customersJoined.error': 'بارگذاری فعالیت مشتریان ممکن نشد.',
|
||||
'home.chart.customersJoined.bar': '{day}: {count} ثبتنام',
|
||||
'home.chart.customersJoined.activeBar': '{day}: {count} فعال',
|
||||
'home.chart.blogViews.title': 'بازدید مقالات',
|
||||
'home.chart.blogViews.subtitle': 'بازدید مقالات در ۳۰ روز گذشته',
|
||||
'home.chart.placeholder.title': 'نمودار',
|
||||
'home.chart.placeholder.subtitle': 'این نمودار بهزودی در دسترس خواهد بود.',
|
||||
'home.chart.placeholder.comingSoon': 'بهزودی',
|
||||
|
||||
'products.overview.subtitle': 'محصولات، موجودی و دستهبندیها را مدیریت کنید.',
|
||||
'products.card.list.desc': 'همه محصولات موجود را ببینید، ویرایش و مدیریت کنید.',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { CalendarDays } from 'lucide-react'
|
||||
import {
|
||||
ShoppingBag,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { SectionCard } from '../components/SectionCard'
|
||||
import { DailyActivityChart } from '../components/DailyActivityChart'
|
||||
import { HomeChartSlot } from '../components/HomeChartSlot'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { BusinessMessageKey } from '../i18n/messages'
|
||||
import { listProducts } from '../services/productService'
|
||||
@@ -19,10 +19,12 @@ import { listStoreItems } from '../services/storeItemService'
|
||||
import { listCustomers } from '../services/customerService'
|
||||
import { listBlogs } from '../services/blogService'
|
||||
import { listPortfolios } from '../services/portfolioService'
|
||||
import { useTenantBranding } from '../context/TenantBrandingContext'
|
||||
import {
|
||||
getCustomersDailyActivity,
|
||||
getOrdersDailyActivity,
|
||||
} from '../services/dailyActivityService'
|
||||
hasBusinessModule,
|
||||
isActiveHomeChart,
|
||||
type BusinessModuleId,
|
||||
} from '../utils/businessModules'
|
||||
import styles from '../components/PageContent.module.css'
|
||||
|
||||
type CountKey = 'products' | 'store' | 'customers' | 'blog' | 'portfolios'
|
||||
@@ -35,6 +37,8 @@ const sections: {
|
||||
countLabelKey?: BusinessMessageKey
|
||||
href: string
|
||||
countKey?: CountKey
|
||||
/** When set, card is shown only if this optional module is enabled. */
|
||||
moduleId?: BusinessModuleId
|
||||
}[] = [
|
||||
{
|
||||
icon: ShoppingBag,
|
||||
@@ -44,6 +48,7 @@ const sections: {
|
||||
countLabelKey: 'home.card.products.count',
|
||||
href: '/products',
|
||||
countKey: 'products',
|
||||
moduleId: 'products',
|
||||
},
|
||||
{
|
||||
icon: Store,
|
||||
@@ -53,6 +58,7 @@ const sections: {
|
||||
countLabelKey: 'home.card.store.count',
|
||||
href: '/store',
|
||||
countKey: 'store',
|
||||
moduleId: 'store',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
@@ -71,6 +77,7 @@ const sections: {
|
||||
countLabelKey: 'home.card.blog.count',
|
||||
href: '/blog',
|
||||
countKey: 'blog',
|
||||
moduleId: 'blog',
|
||||
},
|
||||
{
|
||||
icon: Briefcase,
|
||||
@@ -80,6 +87,7 @@ const sections: {
|
||||
countLabelKey: 'home.card.portfolios.count',
|
||||
href: '/portfolios',
|
||||
countKey: 'portfolios',
|
||||
moduleId: 'portfolio',
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
@@ -113,6 +121,7 @@ async function loadSectionCounts(signal: AbortSignal): Promise<SectionCounts> {
|
||||
export function HomePage() {
|
||||
const { user } = useAuth()
|
||||
const { locale } = useLocale()
|
||||
const { enabledModules, homeCharts } = useTenantBranding()
|
||||
const t = useT()
|
||||
const [counts, setCounts] = useState<SectionCounts>({})
|
||||
|
||||
@@ -124,14 +133,11 @@ export function HomePage() {
|
||||
return () => controller.abort()
|
||||
}, [])
|
||||
|
||||
const loadOrdersActivity = useCallback(
|
||||
(signal: AbortSignal) => getOrdersDailyActivity(30, signal),
|
||||
[],
|
||||
)
|
||||
const loadCustomersActivity = useCallback(
|
||||
(signal: AbortSignal) => getCustomersDailyActivity(30, signal),
|
||||
[],
|
||||
const visibleSections = sections.filter(
|
||||
(section) =>
|
||||
!section.moduleId || hasBusinessModule(enabledModules, section.moduleId),
|
||||
)
|
||||
const visibleCharts = homeCharts.filter(isActiveHomeChart)
|
||||
|
||||
const firstName =
|
||||
(locale === 'en'
|
||||
@@ -160,7 +166,7 @@ export function HomePage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.gridHome}>
|
||||
{sections.map((section) => (
|
||||
{visibleSections.map((section) => (
|
||||
<SectionCard
|
||||
key={section.href}
|
||||
icon={section.icon}
|
||||
@@ -174,34 +180,15 @@ export function HomePage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.grid12}>
|
||||
<div className={styles.col6}>
|
||||
<DailyActivityChart
|
||||
titleKey="home.chart.orders.title"
|
||||
subtitleKey="home.chart.orders.subtitle"
|
||||
primaryLegendKey="home.chart.orders.legend"
|
||||
secondaryLegendKey="home.chart.orders.cartLegend"
|
||||
loadingKey="home.chart.orders.loading"
|
||||
errorKey="home.chart.orders.error"
|
||||
primaryBarTitleKey="home.chart.orders.bar"
|
||||
secondaryBarTitleKey="home.chart.orders.cartBar"
|
||||
load={loadOrdersActivity}
|
||||
/>
|
||||
{visibleCharts.length > 0 ? (
|
||||
<div className={styles.grid12}>
|
||||
{visibleCharts.map((chartId, index) => (
|
||||
<div key={`${chartId}-${index}`} className={styles.col6}>
|
||||
<HomeChartSlot chartId={chartId} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.col6}>
|
||||
<DailyActivityChart
|
||||
titleKey="home.chart.customers.title"
|
||||
subtitleKey="home.chart.customers.subtitle"
|
||||
primaryLegendKey="home.chart.customers.legend"
|
||||
secondaryLegendKey="home.chart.customers.activeLegend"
|
||||
loadingKey="home.chart.customers.loading"
|
||||
errorKey="home.chart.customers.error"
|
||||
primaryBarTitleKey="home.chart.customers.bar"
|
||||
secondaryBarTitleKey="home.chart.customers.activeBar"
|
||||
load={loadCustomersActivity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,10 +25,15 @@ export interface StoreSettings {
|
||||
orderProcessSteps: OrderProcessStep[]
|
||||
}
|
||||
|
||||
export interface ModulesSettings {
|
||||
enabled: string[]
|
||||
}
|
||||
|
||||
export interface BusinessSettings {
|
||||
branding: BrandingSettings
|
||||
dashboard: DashboardSettings
|
||||
store: StoreSettings
|
||||
modules?: ModulesSettings
|
||||
}
|
||||
|
||||
export interface SettingsResponse {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import type { DashboardLocale } from '@meshkee/dashboard-core'
|
||||
import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors'
|
||||
import type { BusinessModuleId, HomeChartId } from '../utils/businessModules'
|
||||
|
||||
export interface ResolvedTenant {
|
||||
id: string
|
||||
@@ -10,6 +11,8 @@ export interface ResolvedTenant {
|
||||
domain: string
|
||||
primaryColor: BusinessPrimaryColorId
|
||||
defaultLocale?: DashboardLocale
|
||||
enabledModules?: BusinessModuleId[]
|
||||
homeCharts?: [HomeChartId, HomeChartId]
|
||||
logoUrl?: string | null
|
||||
faviconUrl?: string | null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/** Optional CMS modules a business can have. Always-on areas are not listed. */
|
||||
export const BUSINESS_MODULE_IDS = [
|
||||
'products',
|
||||
'store',
|
||||
'portfolio',
|
||||
'blog',
|
||||
'warehouse',
|
||||
'videos',
|
||||
] as const
|
||||
|
||||
export type BusinessModuleId = (typeof BUSINESS_MODULE_IDS)[number]
|
||||
|
||||
/** Home dashboard chart slots (super-admin selectable). */
|
||||
export const HOME_CHART_IDS = [
|
||||
'none',
|
||||
'orders_30d',
|
||||
'customers_joined_1y',
|
||||
'blog_views_30d',
|
||||
'products_added_1y',
|
||||
] as const
|
||||
|
||||
export type HomeChartId = (typeof HOME_CHART_IDS)[number]
|
||||
|
||||
/** Existing tenants without saved modules keep every module enabled. */
|
||||
export const DEFAULT_ENABLED_BUSINESS_MODULES: BusinessModuleId[] = [
|
||||
...BUSINESS_MODULE_IDS,
|
||||
]
|
||||
|
||||
export const DEFAULT_HOME_CHARTS: [HomeChartId, HomeChartId] = [
|
||||
'orders_30d',
|
||||
'customers_joined_1y',
|
||||
]
|
||||
|
||||
const MODULE_ID_SET = new Set<string>(BUSINESS_MODULE_IDS)
|
||||
const HOME_CHART_ID_SET = new Set<string>(HOME_CHART_IDS)
|
||||
|
||||
export function isBusinessModuleId(value: unknown): value is BusinessModuleId {
|
||||
return typeof value === 'string' && MODULE_ID_SET.has(value)
|
||||
}
|
||||
|
||||
export function isHomeChartId(value: unknown): value is HomeChartId {
|
||||
return typeof value === 'string' && HOME_CHART_ID_SET.has(value)
|
||||
}
|
||||
|
||||
export function normalizeEnabledBusinessModules(value: unknown): BusinessModuleId[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [...DEFAULT_ENABLED_BUSINESS_MODULES]
|
||||
}
|
||||
|
||||
const selected = new Set<BusinessModuleId>()
|
||||
for (const item of value) {
|
||||
if (isBusinessModuleId(item)) selected.add(item)
|
||||
}
|
||||
|
||||
return BUSINESS_MODULE_IDS.filter((id) => selected.has(id))
|
||||
}
|
||||
|
||||
export function normalizeHomeCharts(value: unknown): [HomeChartId, HomeChartId] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [...DEFAULT_HOME_CHARTS]
|
||||
}
|
||||
|
||||
const first = isHomeChartId(value[0]) ? value[0] : DEFAULT_HOME_CHARTS[0]
|
||||
const second = isHomeChartId(value[1]) ? value[1] : DEFAULT_HOME_CHARTS[1]
|
||||
return [first, second]
|
||||
}
|
||||
|
||||
export function hasBusinessModule(
|
||||
enabledModules: readonly BusinessModuleId[] | null | undefined,
|
||||
moduleId: BusinessModuleId,
|
||||
): boolean {
|
||||
const enabled = normalizeEnabledBusinessModules(enabledModules)
|
||||
return enabled.includes(moduleId)
|
||||
}
|
||||
|
||||
export function isActiveHomeChart(value: HomeChartId): boolean {
|
||||
return value !== 'none'
|
||||
}
|
||||
Reference in New Issue
Block a user