Add business modules column and selectable home charts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-08 10:40:58 +03:30
co-authored by Cursor
parent a48a24e6f2
commit 0a2358b0c2
15 changed files with 788 additions and 137 deletions
@@ -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; grid-column: span 6;
} }
.col12 {
grid-column: span 12;
}
@media (max-width: 1536px) { @media (max-width: 1536px) {
.gridHome { .gridHome {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
+106 -90
View File
@@ -26,6 +26,11 @@ import {
} from '../lib/businessContext' } from '../lib/businessContext'
import { isAbortError } from '../lib/api' import { isAbortError } from '../lib/api'
import { getBusinessProfile } from '../services/businessProfileService' import { getBusinessProfile } from '../services/businessProfileService'
import { useTenantBranding } from '../context/TenantBrandingContext'
import {
hasBusinessModule,
type BusinessModuleId,
} from '../utils/businessModules'
import { Tooltip } from './Tooltip' import { Tooltip } from './Tooltip'
import styles from './Sidebar.module.css' import styles from './Sidebar.module.css'
@@ -41,6 +46,7 @@ interface NavGroup {
labelKey: BusinessMessageKey labelKey: BusinessMessageKey
basePath: string basePath: string
children: NavChild[] children: NavChild[]
moduleId?: BusinessModuleId
} }
interface NavLinkItem { interface NavLinkItem {
@@ -49,6 +55,7 @@ interface NavLinkItem {
icon: LucideIcon icon: LucideIcon
labelKey: BusinessMessageKey labelKey: BusinessMessageKey
to: string to: string
moduleId?: BusinessModuleId
} }
type NavItem = NavLinkItem | NavGroup type NavItem = NavLinkItem | NavGroup
@@ -62,6 +69,7 @@ export function Sidebar() {
const navigate = useNavigate() const navigate = useNavigate()
const { user, logout } = useAuth() const { user, logout } = useAuth()
const { locale } = useLocale() const { locale } = useLocale()
const { enabledModules } = useTenantBranding()
const t = useT() const t = useT()
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({}) const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({})
const [brandLogoUrl, setBrandLogoUrl] = useState<string | null>(null) const [brandLogoUrl, setBrandLogoUrl] = useState<string | null>(null)
@@ -79,96 +87,104 @@ export function Sidebar() {
}, [locale, nameEn, nameFa, fallbackBusinessName]) }, [locale, nameEn, nameFa, fallbackBusinessName])
const navItems = useMemo<NavItem[]>( const navItems = useMemo<NavItem[]>(
() => [ () =>
{ type: 'link', id: 'home', icon: Home, labelKey: 'nav.home', to: '/' }, [
{ { type: 'link', id: 'home', icon: Home, labelKey: 'nav.home', to: '/' },
type: 'link', {
id: 'business-profile', type: 'link',
icon: Building2, id: 'business-profile',
labelKey: 'nav.businessProfile', icon: Building2,
to: '/business-profile', labelKey: 'nav.businessProfile',
}, to: '/business-profile',
{ },
type: 'group', {
id: 'products', type: 'group',
icon: ShoppingBag, id: 'products',
labelKey: 'nav.products', icon: ShoppingBag,
basePath: '/products', labelKey: 'nav.products',
children: [ basePath: '/products',
{ labelKey: 'nav.products.overview', to: '/products' }, moduleId: 'products',
{ labelKey: 'nav.products.list', to: '/products/list' }, children: [
{ labelKey: 'nav.products.new', to: '/products/new' }, { labelKey: 'nav.products.overview', to: '/products' },
{ labelKey: 'nav.products.categories', to: '/products/categories' }, { labelKey: 'nav.products.list', to: '/products/list' },
{ labelKey: 'nav.products.brands', to: '/products/brands' }, { labelKey: 'nav.products.new', to: '/products/new' },
{ labelKey: 'nav.products.settings', to: '/products/settings' }, { 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, type: 'group',
labelKey: 'nav.store', id: 'store',
basePath: '/store', icon: Store,
children: [ labelKey: 'nav.store',
{ labelKey: 'nav.store.overview', to: '/store' }, basePath: '/store',
{ labelKey: 'nav.store.items', to: '/store/items' }, moduleId: 'store',
{ labelKey: 'nav.store.orders', to: '/store/orders' }, children: [
{ labelKey: 'nav.store.shipping', to: '/store/shipping' }, { labelKey: 'nav.store.overview', to: '/store' },
{ labelKey: 'nav.store.cards', to: '/store/cards' }, { labelKey: 'nav.store.items', to: '/store/items' },
{ labelKey: 'nav.store.settings', to: '/store/settings' }, { labelKey: 'nav.store.orders', to: '/store/orders' },
], { labelKey: 'nav.store.shipping', to: '/store/shipping' },
}, { labelKey: 'nav.store.cards', to: '/store/cards' },
{ type: 'link', id: 'customers', icon: Users, labelKey: 'nav.customers', to: '/customers' }, { labelKey: 'nav.store.settings', to: '/store/settings' },
{ ],
type: 'group', },
id: 'blog', { type: 'link', id: 'customers', icon: Users, labelKey: 'nav.customers', to: '/customers' },
icon: FileText, {
labelKey: 'nav.blog', type: 'group',
basePath: '/blog', id: 'blog',
children: [ icon: FileText,
{ labelKey: 'nav.blog.overview', to: '/blog' }, labelKey: 'nav.blog',
{ labelKey: 'nav.blog.list', to: '/blog/list' }, basePath: '/blog',
{ labelKey: 'nav.blog.new', to: '/blog/new' }, moduleId: 'blog',
{ labelKey: 'nav.blog.categories', to: '/blog/categories' }, children: [
{ labelKey: 'nav.blog.settings', to: '/blog/settings' }, { 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' },
type: 'group', { labelKey: 'nav.blog.settings', to: '/blog/settings' },
id: 'portfolios', ],
icon: Briefcase, },
labelKey: 'nav.portfolios', {
basePath: '/portfolios', type: 'group',
children: [ id: 'portfolios',
{ labelKey: 'nav.portfolios.overview', to: '/portfolios' }, icon: Briefcase,
{ labelKey: 'nav.portfolios.list', to: '/portfolios/list' }, labelKey: 'nav.portfolios',
{ labelKey: 'nav.portfolios.new', to: '/portfolios/new' }, basePath: '/portfolios',
{ labelKey: 'nav.portfolios.categories', to: '/portfolios/categories' }, moduleId: 'portfolio',
{ labelKey: 'nav.portfolios.settings', to: '/portfolios/settings' }, children: [
], { labelKey: 'nav.portfolios.overview', to: '/portfolios' },
}, { labelKey: 'nav.portfolios.list', to: '/portfolios/list' },
{ { labelKey: 'nav.portfolios.new', to: '/portfolios/new' },
type: 'group', { labelKey: 'nav.portfolios.categories', to: '/portfolios/categories' },
id: 'website', { labelKey: 'nav.portfolios.settings', to: '/portfolios/settings' },
icon: Globe, ],
labelKey: 'nav.website', },
basePath: '/website', {
children: [ type: 'group',
{ labelKey: 'nav.website.overview', to: '/website' }, id: 'website',
{ labelKey: 'nav.website.sliders', to: '/website/sliders' }, icon: Globe,
{ labelKey: 'nav.website.specialCategories', to: '/website/special-categories' }, labelKey: 'nav.website',
{ labelKey: 'nav.website.specialBrands', to: '/website/special-brands' }, basePath: '/website',
{ labelKey: 'nav.website.specialItems', to: '/website/special-items' }, children: [
{ labelKey: 'nav.website.contact', to: '/website/contact' }, { labelKey: 'nav.website.overview', to: '/website' },
{ labelKey: 'nav.website.subscriptions', to: '/website/subscriptions' }, { labelKey: 'nav.website.sliders', to: '/website/sliders' },
{ labelKey: 'nav.website.faq', to: '/website/faq' }, { labelKey: 'nav.website.specialCategories', to: '/website/special-categories' },
{ labelKey: 'nav.website.badges', to: '/website/badges' }, { labelKey: 'nav.website.specialBrands', to: '/website/special-brands' },
{ labelKey: 'nav.website.ePayment', to: '/website/e-payment' }, { labelKey: 'nav.website.specialItems', to: '/website/special-items' },
], { labelKey: 'nav.website.contact', to: '/website/contact' },
}, { labelKey: 'nav.website.subscriptions', to: '/website/subscriptions' },
{ type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' }, { 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(() => { useEffect(() => {
@@ -15,12 +15,22 @@ import { getBusinessDomain } from '../lib/config'
import { BUSINESS_PROFILE_UPDATED_EVENT } from '../lib/businessContext' import { BUSINESS_PROFILE_UPDATED_EVENT } from '../lib/businessContext'
import { getBusinessProfile } from '../services/businessProfileService' import { getBusinessProfile } from '../services/businessProfileService'
import { resolveTenantByDomain } from '../services/tenantService' import { resolveTenantByDomain } from '../services/tenantService'
import {
DEFAULT_ENABLED_BUSINESS_MODULES,
DEFAULT_HOME_CHARTS,
normalizeEnabledBusinessModules,
normalizeHomeCharts,
type BusinessModuleId,
type HomeChartId,
} from '../utils/businessModules'
interface TenantBrandingContextValue { interface TenantBrandingContextValue {
businessName: string businessName: string
businessNameEn: string businessNameEn: string
logoUrl: string | null logoUrl: string | null
faviconUrl: string | null faviconUrl: string | null
enabledModules: BusinessModuleId[]
homeCharts: [HomeChartId, HomeChartId]
refreshBranding: () => void refreshBranding: () => void
} }
@@ -42,6 +52,12 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
const [nameFa, setNameFa] = useState('') const [nameFa, setNameFa] = useState('')
const [logoUrl, setLogoUrl] = useState<string | null>(null) const [logoUrl, setLogoUrl] = useState<string | null>(null)
const [faviconUrl, setFaviconUrl] = 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 [refreshToken, setRefreshToken] = useState(0)
const defaultLocaleAppliedRef = useRef(false) const defaultLocaleAppliedRef = useRef(false)
@@ -95,6 +111,8 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
setNameFa(nextNameFa || nextNameEn || domain) setNameFa(nextNameFa || nextNameEn || domain)
setLogoUrl(nextLogo) setLogoUrl(nextLogo)
setFaviconUrl(nextFavicon) setFaviconUrl(nextFavicon)
setEnabledModules(normalizeEnabledBusinessModules(tenant.enabledModules))
setHomeCharts(normalizeHomeCharts(tenant.homeCharts))
applyDocumentFavicon(nextFavicon) applyDocumentFavicon(nextFavicon)
if (!defaultLocaleAppliedRef.current) { if (!defaultLocaleAppliedRef.current) {
defaultLocaleAppliedRef.current = true defaultLocaleAppliedRef.current = true
@@ -112,6 +130,8 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
setNameFa(domain) setNameFa(domain)
setLogoUrl(null) setLogoUrl(null)
setFaviconUrl(null) setFaviconUrl(null)
setEnabledModules([...DEFAULT_ENABLED_BUSINESS_MODULES])
setHomeCharts([...DEFAULT_HOME_CHARTS])
applyDocumentFavicon(null) applyDocumentFavicon(null)
} }
} }
@@ -142,8 +162,24 @@ export function TenantBrandingProvider({ children }: { children: ReactNode }) {
) )
const value = useMemo( 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 ( return (
+27 -1
View File
@@ -192,6 +192,19 @@ const en = {
'home.chart.customers.error': 'Unable to load customer activity.', 'home.chart.customers.error': 'Unable to load customer activity.',
'home.chart.customers.bar': '{day}: {count} registered', 'home.chart.customers.bar': '{day}: {count} registered',
'home.chart.customers.activeBar': '{day}: {count} active', '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.overview.subtitle': 'Manage your products, inventory and categories.',
'products.card.list.desc': 'View, edit and manage all your existing products.', '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.card.website.link': 'مشاهده وب‌سایت',
'home.chart.orders.title': 'سفارش‌ها', 'home.chart.orders.title': 'سفارش‌ها',
'home.chart.orders.subtitle': 'سفارش‌ها و افزودن به سبد در ۳۰ روز گذشته', 'home.chart.orders.subtitle': 'سفارش‌ها در ۳۰ روز گذشته',
'home.chart.orders.legend': 'سفارش ({count})', 'home.chart.orders.legend': 'سفارش ({count})',
'home.chart.orders.cartLegend': 'افزودن به سبد ({count})', 'home.chart.orders.cartLegend': 'افزودن به سبد ({count})',
'home.chart.orders.loading': 'در حال بارگذاری نمودار...', 'home.chart.orders.loading': 'در حال بارگذاری نمودار...',
@@ -1246,6 +1259,19 @@ const fa: Record<MessageKey, string> = {
'home.chart.customers.error': 'بارگذاری فعالیت مشتریان ممکن نشد.', 'home.chart.customers.error': 'بارگذاری فعالیت مشتریان ممکن نشد.',
'home.chart.customers.bar': '{day}: {count} ثبت‌نام', 'home.chart.customers.bar': '{day}: {count} ثبت‌نام',
'home.chart.customers.activeBar': '{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.overview.subtitle': 'محصولات، موجودی و دسته‌بندی‌ها را مدیریت کنید.',
'products.card.list.desc': 'همه محصولات موجود را ببینید، ویرایش و مدیریت کنید.', 'products.card.list.desc': 'همه محصولات موجود را ببینید، ویرایش و مدیریت کنید.',
+27 -40
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { CalendarDays } from 'lucide-react' import { CalendarDays } from 'lucide-react'
import { import {
ShoppingBag, ShoppingBag,
@@ -11,7 +11,7 @@ import {
import { useLocale } from '@meshkee/dashboard-ui' import { useLocale } from '@meshkee/dashboard-ui'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
import { SectionCard } from '../components/SectionCard' import { SectionCard } from '../components/SectionCard'
import { DailyActivityChart } from '../components/DailyActivityChart' import { HomeChartSlot } from '../components/HomeChartSlot'
import { useT } from '../i18n/useT' import { useT } from '../i18n/useT'
import type { BusinessMessageKey } from '../i18n/messages' import type { BusinessMessageKey } from '../i18n/messages'
import { listProducts } from '../services/productService' import { listProducts } from '../services/productService'
@@ -19,10 +19,12 @@ import { listStoreItems } from '../services/storeItemService'
import { listCustomers } from '../services/customerService' import { listCustomers } from '../services/customerService'
import { listBlogs } from '../services/blogService' import { listBlogs } from '../services/blogService'
import { listPortfolios } from '../services/portfolioService' import { listPortfolios } from '../services/portfolioService'
import { useTenantBranding } from '../context/TenantBrandingContext'
import { import {
getCustomersDailyActivity, hasBusinessModule,
getOrdersDailyActivity, isActiveHomeChart,
} from '../services/dailyActivityService' type BusinessModuleId,
} from '../utils/businessModules'
import styles from '../components/PageContent.module.css' import styles from '../components/PageContent.module.css'
type CountKey = 'products' | 'store' | 'customers' | 'blog' | 'portfolios' type CountKey = 'products' | 'store' | 'customers' | 'blog' | 'portfolios'
@@ -35,6 +37,8 @@ const sections: {
countLabelKey?: BusinessMessageKey countLabelKey?: BusinessMessageKey
href: string href: string
countKey?: CountKey countKey?: CountKey
/** When set, card is shown only if this optional module is enabled. */
moduleId?: BusinessModuleId
}[] = [ }[] = [
{ {
icon: ShoppingBag, icon: ShoppingBag,
@@ -44,6 +48,7 @@ const sections: {
countLabelKey: 'home.card.products.count', countLabelKey: 'home.card.products.count',
href: '/products', href: '/products',
countKey: 'products', countKey: 'products',
moduleId: 'products',
}, },
{ {
icon: Store, icon: Store,
@@ -53,6 +58,7 @@ const sections: {
countLabelKey: 'home.card.store.count', countLabelKey: 'home.card.store.count',
href: '/store', href: '/store',
countKey: 'store', countKey: 'store',
moduleId: 'store',
}, },
{ {
icon: Users, icon: Users,
@@ -71,6 +77,7 @@ const sections: {
countLabelKey: 'home.card.blog.count', countLabelKey: 'home.card.blog.count',
href: '/blog', href: '/blog',
countKey: 'blog', countKey: 'blog',
moduleId: 'blog',
}, },
{ {
icon: Briefcase, icon: Briefcase,
@@ -80,6 +87,7 @@ const sections: {
countLabelKey: 'home.card.portfolios.count', countLabelKey: 'home.card.portfolios.count',
href: '/portfolios', href: '/portfolios',
countKey: 'portfolios', countKey: 'portfolios',
moduleId: 'portfolio',
}, },
{ {
icon: Globe, icon: Globe,
@@ -113,6 +121,7 @@ async function loadSectionCounts(signal: AbortSignal): Promise<SectionCounts> {
export function HomePage() { export function HomePage() {
const { user } = useAuth() const { user } = useAuth()
const { locale } = useLocale() const { locale } = useLocale()
const { enabledModules, homeCharts } = useTenantBranding()
const t = useT() const t = useT()
const [counts, setCounts] = useState<SectionCounts>({}) const [counts, setCounts] = useState<SectionCounts>({})
@@ -124,14 +133,11 @@ export function HomePage() {
return () => controller.abort() return () => controller.abort()
}, []) }, [])
const loadOrdersActivity = useCallback( const visibleSections = sections.filter(
(signal: AbortSignal) => getOrdersDailyActivity(30, signal), (section) =>
[], !section.moduleId || hasBusinessModule(enabledModules, section.moduleId),
)
const loadCustomersActivity = useCallback(
(signal: AbortSignal) => getCustomersDailyActivity(30, signal),
[],
) )
const visibleCharts = homeCharts.filter(isActiveHomeChart)
const firstName = const firstName =
(locale === 'en' (locale === 'en'
@@ -160,7 +166,7 @@ export function HomePage() {
</div> </div>
<div className={styles.gridHome}> <div className={styles.gridHome}>
{sections.map((section) => ( {visibleSections.map((section) => (
<SectionCard <SectionCard
key={section.href} key={section.href}
icon={section.icon} icon={section.icon}
@@ -174,34 +180,15 @@ export function HomePage() {
))} ))}
</div> </div>
<div className={styles.grid12}> {visibleCharts.length > 0 ? (
<div className={styles.col6}> <div className={styles.grid12}>
<DailyActivityChart {visibleCharts.map((chartId, index) => (
titleKey="home.chart.orders.title" <div key={`${chartId}-${index}`} className={styles.col6}>
subtitleKey="home.chart.orders.subtitle" <HomeChartSlot chartId={chartId} />
primaryLegendKey="home.chart.orders.legend" </div>
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}
/>
</div> </div>
<div className={styles.col6}> ) : null}
<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>
</main> </main>
) )
} }
@@ -25,10 +25,15 @@ export interface StoreSettings {
orderProcessSteps: OrderProcessStep[] orderProcessSteps: OrderProcessStep[]
} }
export interface ModulesSettings {
enabled: string[]
}
export interface BusinessSettings { export interface BusinessSettings {
branding: BrandingSettings branding: BrandingSettings
dashboard: DashboardSettings dashboard: DashboardSettings
store: StoreSettings store: StoreSettings
modules?: ModulesSettings
} }
export interface SettingsResponse { export interface SettingsResponse {
@@ -1,6 +1,7 @@
import { apiRequest } from '../lib/api' import { apiRequest } from '../lib/api'
import type { DashboardLocale } from '@meshkee/dashboard-core' import type { DashboardLocale } from '@meshkee/dashboard-core'
import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors' import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors'
import type { BusinessModuleId, HomeChartId } from '../utils/businessModules'
export interface ResolvedTenant { export interface ResolvedTenant {
id: string id: string
@@ -10,6 +11,8 @@ export interface ResolvedTenant {
domain: string domain: string
primaryColor: BusinessPrimaryColorId primaryColor: BusinessPrimaryColorId
defaultLocale?: DashboardLocale defaultLocale?: DashboardLocale
enabledModules?: BusinessModuleId[]
homeCharts?: [HomeChartId, HomeChartId]
logoUrl?: string | null logoUrl?: string | null
faviconUrl?: 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'
}
@@ -414,6 +414,94 @@
padding-right: 8px; padding-right: 8px;
} }
.thModules,
.tdModules {
width: 1%;
white-space: nowrap;
text-align: center;
}
.tdModules {
padding-left: 8px;
padding-right: 8px;
}
.modulesCountBtn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36px;
min-height: var(--field-height);
padding: var(--field-padding-y) 12px;
font-size: var(--field-font-size);
font-weight: 600;
font-family: var(--font-ui);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
background: rgba(255, 255, 255, 0.7);
color: var(--text-primary);
cursor: pointer;
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.modulesCountBtn:hover {
border-color: var(--primary);
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
.modulesCountBtn:focus-visible {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
.modulesChecks {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.modulesCharts {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid rgba(148, 163, 184, 0.28);
}
.modulesCharts .field select {
appearance: none;
-webkit-appearance: none;
min-height: var(--field-height);
padding: var(--field-padding-y) var(--select-padding-end) var(--field-padding-y)
var(--field-padding-x);
font-size: var(--field-font-size);
font-family: var(--font-ui);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
background-color: rgba(255, 255, 255, 0.7);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right var(--select-arrow-offset) center;
background-size: var(--select-arrow-size);
color: var(--text-primary);
cursor: pointer;
}
.modulesCharts .field select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
.modulesCharts .field select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.localeSelect { .localeSelect {
min-height: var(--field-height); min-height: var(--field-height);
padding: var(--field-padding-y) var(--select-padding-end) var(--field-padding-y) padding: var(--field-padding-y) var(--select-padding-end) var(--field-padding-y)
+205 -2
View File
@@ -55,9 +55,22 @@ import {
getBusinessSettings, getBusinessSettings,
updateBusinessBranding, updateBusinessBranding,
updateBusinessDefaultLocale, updateBusinessDefaultLocale,
updateBusinessModules,
updateBusinessPrimaryColor, updateBusinessPrimaryColor,
type DashboardLocale, type DashboardLocale,
} from '../services/businessSettingsService' } from '../services/businessSettingsService'
import {
BUSINESS_MODULE_IDS,
BUSINESS_MODULE_LABELS,
DEFAULT_ENABLED_BUSINESS_MODULES,
DEFAULT_HOME_CHARTS,
HOME_CHART_IDS,
HOME_CHART_LABELS,
normalizeEnabledBusinessModules,
normalizeHomeCharts,
type BusinessModuleId,
type HomeChartId,
} from '../utils/businessModules'
import { flattenBusinessCategories } from '../utils/categories' import { flattenBusinessCategories } from '../utils/categories'
import { Pagination } from '@meshkee/dashboard-ui' import { Pagination } from '@meshkee/dashboard-ui'
import pageStyles from '../components/PageContent.module.css' import pageStyles from '../components/PageContent.module.css'
@@ -70,6 +83,15 @@ function normalizeDefaultLocale(value: unknown): DashboardLocale {
return value === 'en' || value === 'fa' ? value : DEFAULT_LOCALE return value === 'en' || value === 'fa' ? value : DEFAULT_LOCALE
} }
function emptyModuleSelection(
enabled: BusinessModuleId[] = DEFAULT_ENABLED_BUSINESS_MODULES,
): Record<BusinessModuleId, boolean> {
const selected = new Set(enabled)
return Object.fromEntries(
BUSINESS_MODULE_IDS.map((id) => [id, selected.has(id)]),
) as Record<BusinessModuleId, boolean>
}
function formatDate(value: string) { function formatDate(value: string) {
const d = new Date(value) const d = new Date(value)
if (Number.isNaN(d.getTime())) return value if (Number.isNaN(d.getTime())) return value
@@ -227,6 +249,17 @@ export function BusinessesPage() {
const [savingColorId, setSavingColorId] = useState<string | null>(null) const [savingColorId, setSavingColorId] = useState<string | null>(null)
const [savingLocaleId, setSavingLocaleId] = useState<string | null>(null) const [savingLocaleId, setSavingLocaleId] = useState<string | null>(null)
const [modulesOpen, setModulesOpen] = useState(false)
const [modulesBusiness, setModulesBusiness] = useState<BusinessListItem | null>(null)
const [modulesSelected, setModulesSelected] = useState<Record<BusinessModuleId, boolean>>(
() => emptyModuleSelection(),
)
const [modulesCharts, setModulesCharts] = useState<[HomeChartId, HomeChartId]>([
...DEFAULT_HOME_CHARTS,
])
const [modulesSubmitting, setModulesSubmitting] = useState(false)
const [modulesError, setModulesError] = useState('')
const [migrateOpen, setMigrateOpen] = useState(false) const [migrateOpen, setMigrateOpen] = useState(false)
const [migrateBusiness, setMigrateBusiness] = useState<BusinessListItem | null>(null) const [migrateBusiness, setMigrateBusiness] = useState<BusinessListItem | null>(null)
const [migrateOldId, setMigrateOldId] = useState('') const [migrateOldId, setMigrateOldId] = useState('')
@@ -465,6 +498,62 @@ export function BusinessesPage() {
} }
} }
function openModules(b: BusinessListItem) {
setModulesBusiness(b)
setModulesSelected(
emptyModuleSelection(
normalizeEnabledBusinessModules(b.enabledModules ?? DEFAULT_ENABLED_BUSINESS_MODULES),
),
)
setModulesCharts(normalizeHomeCharts(b.homeCharts ?? DEFAULT_HOME_CHARTS))
setModulesError('')
setModulesOpen(true)
}
function closeModules() {
if (modulesSubmitting) return
setModulesOpen(false)
setModulesBusiness(null)
setModulesError('')
}
async function submitModules() {
if (!modulesBusiness) return
const enabled = BUSINESS_MODULE_IDS.filter((id) => modulesSelected[id])
const charts = normalizeHomeCharts(modulesCharts)
setModulesSubmitting(true)
setModulesError('')
try {
await updateBusinessModules(modulesBusiness.id, enabled, charts)
setData((prev) =>
prev
? {
...prev,
items: prev.items.map((item) =>
item.id === modulesBusiness.id
? {
...item,
enabledModules: enabled,
moduleCount: enabled.length,
homeCharts: charts,
}
: item,
),
}
: prev,
)
showToast('Modules updated.', 'success')
setModulesOpen(false)
setModulesBusiness(null)
} catch (err) {
setModulesError(
err instanceof ApiError ? err.message : 'Unable to update modules.',
)
} finally {
setModulesSubmitting(false)
}
}
async function submitEdit() { async function submitEdit() {
if (!editBusiness) return if (!editBusiness) return
setEditSubmitting(true) setEditSubmitting(true)
@@ -903,13 +992,14 @@ export function BusinessesPage() {
<th className={styles.th}>Owner</th> <th className={styles.th}>Owner</th>
<th className={`${styles.th} ${styles.thTheme}`}>Theme</th> <th className={`${styles.th} ${styles.thTheme}`}>Theme</th>
<th className={`${styles.th} ${styles.thLocale}`}>Lang</th> <th className={`${styles.th} ${styles.thLocale}`}>Lang</th>
<th className={`${styles.th} ${styles.thModules}`}>Modules</th>
<th className={`${styles.th} ${styles.thActions}`}>Actions</th> <th className={`${styles.th} ${styles.thActions}`}>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{loading && ( {loading && (
<tr> <tr>
<td className={styles.td} colSpan={7}> <td className={styles.td} colSpan={8}>
Loading... Loading...
</td> </td>
</tr> </tr>
@@ -917,7 +1007,7 @@ export function BusinessesPage() {
{!loading && data?.items?.length === 0 && ( {!loading && data?.items?.length === 0 && (
<tr> <tr>
<td className={styles.td} colSpan={7}> <td className={styles.td} colSpan={8}>
No results found. No results found.
</td> </td>
</tr> </tr>
@@ -992,6 +1082,21 @@ export function BusinessesPage() {
<option value="en">EN</option> <option value="en">EN</option>
</select> </select>
</td> </td>
<td className={`${styles.td} ${styles.tdModules}`}>
<button
type="button"
className={styles.modulesCountBtn}
onClick={() => openModules(b)}
title="Edit modules"
aria-label={`Edit modules for ${b.name}`}
>
{typeof b.moduleCount === 'number'
? b.moduleCount
: normalizeEnabledBusinessModules(
b.enabledModules ?? DEFAULT_ENABLED_BUSINESS_MODULES,
).length}
</button>
</td>
<td className={`${styles.td} ${styles.tdActions}`}> <td className={`${styles.td} ${styles.tdActions}`}>
<div className={styles.rowActions}> <div className={styles.rowActions}>
<span className={styles.toggleInActions}> <span className={styles.toggleInActions}>
@@ -1212,6 +1317,104 @@ export function BusinessesPage() {
</div> </div>
</Modal> </Modal>
<Modal
open={modulesOpen}
title={
modulesBusiness
? `Modules — ${modulesBusiness.name}`
: 'Modules'
}
onClose={closeModules}
>
{modulesError ? (
<p className={styles.alertError} role="alert">
{modulesError}
</p>
) : null}
<p className={styles.helperText}>
Choose which optional modules this business can use. Customers, website,
and other general sections stay available for every business.
</p>
<div className={styles.modulesChecks}>
{BUSINESS_MODULE_IDS.map((id) => (
<label key={id} className={styles.entityCheck}>
<input
type="checkbox"
checked={modulesSelected[id]}
disabled={modulesSubmitting}
onChange={(e) =>
setModulesSelected((prev) => ({
...prev,
[id]: e.target.checked,
}))
}
/>
<span>{BUSINESS_MODULE_LABELS[id]}</span>
</label>
))}
</div>
<div className={styles.modulesCharts}>
<p className={styles.entityGroupLabel}>Home charts</p>
<div className={styles.field}>
<label htmlFor="modules-chart-1">Chart 1</label>
<select
id="modules-chart-1"
value={modulesCharts[0]}
disabled={modulesSubmitting}
onChange={(e) => {
const value = e.target.value
if (!HOME_CHART_IDS.includes(value as HomeChartId)) return
setModulesCharts([value as HomeChartId, modulesCharts[1]])
}}
>
{HOME_CHART_IDS.map((id) => (
<option key={id} value={id}>
{HOME_CHART_LABELS[id]}
</option>
))}
</select>
</div>
<div className={styles.field}>
<label htmlFor="modules-chart-2">Chart 2</label>
<select
id="modules-chart-2"
value={modulesCharts[1]}
disabled={modulesSubmitting}
onChange={(e) => {
const value = e.target.value
if (!HOME_CHART_IDS.includes(value as HomeChartId)) return
setModulesCharts([modulesCharts[0], value as HomeChartId])
}}
>
{HOME_CHART_IDS.map((id) => (
<option key={id} value={id}>
{HOME_CHART_LABELS[id]}
</option>
))}
</select>
</div>
</div>
<div style={{ height: 12 }} />
<div className={styles.actionsRow}>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
onClick={closeModules}
disabled={modulesSubmitting}
>
Cancel
</button>
<button
type="button"
className={`${styles.btn} ${styles.btnPrimary}`}
onClick={() => void submitModules()}
disabled={modulesSubmitting}
>
{modulesSubmitting ? 'Saving…' : 'Save modules'}
</button>
</div>
</Modal>
<Modal <Modal
open={migrateOpen} open={migrateOpen}
title={ title={
@@ -1,5 +1,6 @@
import { apiRequest } from '../lib/api' import { apiRequest } from '../lib/api'
import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors' import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors'
import type { BusinessModuleId, HomeChartId } from '../utils/businessModules'
export type DashboardLocale = 'en' | 'fa' export type DashboardLocale = 'en' | 'fa'
@@ -8,6 +9,11 @@ export interface BrandingSettings {
defaultLocale: DashboardLocale defaultLocale: DashboardLocale
} }
export interface ModulesSettings {
enabled: BusinessModuleId[]
charts: [HomeChartId, HomeChartId]
}
export interface BusinessSettings { export interface BusinessSettings {
branding: BrandingSettings branding: BrandingSettings
dashboard: { dashboard: {
@@ -18,6 +24,7 @@ export interface BusinessSettings {
onlineSellEnabled: boolean onlineSellEnabled: boolean
orderProcessSteps: Array<{ id: string; label: string; color: string }> orderProcessSteps: Array<{ id: string; label: string; color: string }>
} }
modules: ModulesSettings
} }
export interface BusinessSettingsResponse { export interface BusinessSettingsResponse {
@@ -68,3 +75,17 @@ export async function updateBusinessBranding(
body: { branding }, body: { branding },
}) })
} }
export async function updateBusinessModules(
businessId: string,
enabled: BusinessModuleId[],
charts: [HomeChartId, HomeChartId],
) {
return apiRequest<BusinessSettingsResponse>(`/businesses/${businessId}/settings`, {
method: 'PATCH',
auth: true,
body: {
modules: { enabled, charts },
},
})
}
+4
View File
@@ -1,4 +1,5 @@
import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors' import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors'
import type { BusinessModuleId, HomeChartId } from '../utils/businessModules'
import type { DashboardLocale } from '../services/businessSettingsService' import type { DashboardLocale } from '../services/businessSettingsService'
export interface BusinessOwnerInfo { export interface BusinessOwnerInfo {
@@ -24,6 +25,9 @@ export interface BusinessListItem {
isActive: boolean isActive: boolean
primaryColor: BusinessPrimaryColorId primaryColor: BusinessPrimaryColorId
defaultLocale: DashboardLocale defaultLocale: DashboardLocale
enabledModules: BusinessModuleId[]
moduleCount: number
homeCharts: [HomeChartId, HomeChartId]
oldBusinessId: string | null oldBusinessId: string | null
} }
@@ -0,0 +1,87 @@
/** 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]
export const BUSINESS_MODULE_LABELS: Record<BusinessModuleId, string> = {
products: 'Products',
store: 'Store',
portfolio: 'Portfolio',
blog: 'Blog',
warehouse: 'Warehouse',
videos: 'Videos',
}
/** 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]
export const HOME_CHART_LABELS: Record<HomeChartId, string> = {
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',
products_added_1y: 'Added product in last year',
}
/** 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 isActiveHomeChart(value: HomeChartId): boolean {
return value !== 'none'
}
+7 -2
View File
@@ -3,7 +3,7 @@
> **For AI agents:** Read this file at the start of a new chat before making changes. > **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. > Update this document when a major feature is completed or architecture changes.
Last updated: August 5, 2026 Last updated: August 7, 2026
--- ---
@@ -256,7 +256,11 @@ Products accept optional `brandId` on create/update.
- `POST /tenants/:host/contact-submissions` — public website contact form - `POST /tenants/:host/contact-submissions` — public website contact form
### Business settings ### Business settings
- `GET/PATCH /businesses/:businessId/settings` includes `branding.primaryColor` - `GET/PATCH /businesses/:businessId/settings``branding`, `dashboard`, `store`, `modules`
- `modules.enabled`: optional CMS modules (`products`, `store`, `portfolio`, `blog`, `warehouse`, `videos`); missing key → all enabled (legacy). 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.
- Always-on (not in modules list): customers, website, profile, settings, home
- Tenant public: `GET /tenants/:host` includes `enabledModules` + `homeCharts`
### Portfolios ### Portfolios
- `GET/POST/PATCH/DELETE /businesses/:businessId/portfolios` - `GET/POST/PATCH/DELETE /businesses/:businessId/portfolios`
@@ -437,6 +441,7 @@ Global input sizing lives in `packages/dashboard-core/src/styles/tokens.css` (cu
- **Farsi text:** Customer app → Yekan Bakh (`src/fonts/yekanbakh.css`, `--font-fa` in `index.css`); business/super-admin → IranYekan (`iranyekan.css`, `.faText`). Input + placeholder must share the same stack — see `.cursor/rules/ui-farsi-fonts.mdc` - **Farsi text:** Customer app → Yekan Bakh (`src/fonts/yekanbakh.css`, `--font-fa` in `index.css`); business/super-admin → IranYekan (`iranyekan.css`, `.faText`). Input + placeholder must share the same stack — see `.cursor/rules/ui-farsi-fonts.mdc`
- **Per-business theme:** Super Admin sets `branding.primaryColor`; business app applies via `BusinessThemeProvider` - **Per-business theme:** Super Admin sets `branding.primaryColor`; business app applies via `BusinessThemeProvider`
- **Per-business modules:** Super Admin **Modules** column on Businesses list; stored in `settings.modules.enabled`; business Home + Sidebar hide disabled modules
- **RTL:** Farsi inputs use `dir="rtl"` and the apps `--font-fa` - **RTL:** Farsi inputs use `dir="rtl"` and the apps `--font-fa`
- **IRT prices:** comma-separated thousands + `IRT` suffix — see `.cursor/rules/ui-irt-price.mdc` - **IRT prices:** comma-separated thousands + `IRT` suffix — see `.cursor/rules/ui-irt-price.mdc`
- **Toasts:** see `.cursor/rules/ui-toasts.mdc` - **Toasts:** see `.cursor/rules/ui-toasts.mdc`