Files
dashboards/apps/business/src/pages/HomePage.tsx
T
Alireza HassaniandCursor d5aca8bea7 Add business workshop management UI with Jalali scheduling and shared dashboard polish.
Includes workshop list/edit/details, registrations modal, category tree, shared EmptyState/RichTextEditor/toasts, and fixes for the Jalali date picker and thumbnail field layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 12:22:27 +03:30

215 lines
7.1 KiB
TypeScript

import { useEffect, useState } from 'react'
import { CalendarDays } from 'lucide-react'
import {
ShoppingBag,
Store,
Users,
Briefcase,
Layers,
Wallet,
} from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { useAuth } from '../context/AuthContext'
import { SectionCard } from '../components/SectionCard'
import { HomeChartSlot } from '../components/HomeChartSlot'
import { useT } from '../i18n/useT'
import type { BusinessMessageKey } from '../i18n/messages'
import { listProducts } from '../services/productService'
import { listStoreItems } from '../services/storeItemService'
import { listCustomers } from '../services/customerService'
import { listBlogs } from '../services/blogService'
import { listPortfolios } from '../services/portfolioService'
import { listVideos } from '../services/videoService'
import { listInstructions } from '../services/instructionService'
import { listWorkshops } from '../services/workshopService'
import { useTenantBranding } from '../context/TenantBrandingContext'
import {
hasBusinessModule,
hasAnyBusinessModule,
isActiveHomeChart,
type BusinessModuleId,
} from '../utils/businessModules'
import styles from '../components/PageContent.module.css'
type CountKey = 'products' | 'store' | 'customers' | 'content' | 'portfolios'
const sections: {
icon: typeof ShoppingBag
titleKey: BusinessMessageKey
descKey: BusinessMessageKey
linkKey: BusinessMessageKey
countLabelKey?: BusinessMessageKey
href: string
countKey?: CountKey
/** Double-width tile (4 of 12 columns vs 2 for a normal tile). */
wide?: boolean
/** When set, card is shown only if this optional module is enabled. */
moduleId?: BusinessModuleId
/** When set, card is shown if any listed module is enabled. */
anyModuleIds?: BusinessModuleId[]
}[] = [
{
icon: ShoppingBag,
titleKey: 'home.card.products.title',
descKey: 'home.card.products.desc',
linkKey: 'home.card.products.link',
countLabelKey: 'home.card.products.count',
href: '/products',
countKey: 'products',
moduleId: 'products',
},
{
icon: Store,
titleKey: 'home.card.store.title',
descKey: 'home.card.store.desc',
linkKey: 'home.card.store.link',
countLabelKey: 'home.card.store.count',
href: '/store',
countKey: 'store',
wide: true,
moduleId: 'store',
},
{
icon: Users,
titleKey: 'home.card.customers.title',
descKey: 'home.card.customers.desc',
linkKey: 'home.card.customers.link',
countLabelKey: 'home.card.customers.count',
href: '/customers',
countKey: 'customers',
},
{
icon: Wallet,
titleKey: 'home.card.finance.title',
descKey: 'home.card.finance.desc',
linkKey: 'home.card.finance.link',
href: '/finance',
moduleId: 'finance',
},
{
icon: Layers,
titleKey: 'home.card.content.title',
descKey: 'home.card.content.desc',
linkKey: 'home.card.content.link',
countLabelKey: 'home.card.content.count',
href: '/content',
countKey: 'content',
anyModuleIds: ['blog', 'videos', 'instructions', 'workshops'],
},
{
icon: Briefcase,
titleKey: 'home.card.portfolios.title',
descKey: 'home.card.portfolios.desc',
linkKey: 'home.card.portfolios.link',
countLabelKey: 'home.card.portfolios.count',
href: '/portfolios',
countKey: 'portfolios',
moduleId: 'portfolio',
},
]
type SectionCounts = Partial<Record<CountKey, number>>
async function loadSectionCounts(signal: AbortSignal): Promise<SectionCounts> {
const [products, store, customers, blog, portfolios, videos, instructions, workshops] =
await Promise.all([
listProducts(1, 1, signal).then((r) => r.total).catch(() => null),
listStoreItems(1, 1, signal).then((r) => r.total).catch(() => null),
listCustomers({ page: 1, pageSize: 1 }, signal).then((r) => r.total).catch(() => null),
listBlogs(1, 1, signal).then((r) => r.total).catch(() => null),
listPortfolios(1, 1, signal).then((r) => r.total).catch(() => null),
listVideos(1, 1, signal).then((r) => r.total).catch(() => null),
listInstructions(1, 1, signal).then((r) => r.total).catch(() => null),
listWorkshops(1, 1, signal).then((r) => r.total).catch(() => null),
])
const counts: SectionCounts = {}
if (products !== null) counts.products = products
if (store !== null) counts.store = store
if (customers !== null) counts.customers = customers
if (portfolios !== null) counts.portfolios = portfolios
const contentTotal = (blog ?? 0) + (videos ?? 0) + (instructions ?? 0) + (workshops ?? 0)
if (blog !== null || videos !== null || instructions !== null || workshops !== null) {
counts.content = contentTotal
}
return counts
}
export function HomePage() {
const { user } = useAuth()
const { locale } = useLocale()
const { enabledModules, homeCharts } = useTenantBranding()
const t = useT()
const [counts, setCounts] = useState<SectionCounts>({})
useEffect(() => {
const controller = new AbortController()
void loadSectionCounts(controller.signal).then((next) => {
if (!controller.signal.aborted) setCounts(next)
})
return () => controller.abort()
}, [])
const visibleSections = sections.filter((section) => {
if (section.anyModuleIds) {
return hasAnyBusinessModule(enabledModules, section.anyModuleIds)
}
return !section.moduleId || hasBusinessModule(enabledModules, section.moduleId)
})
const visibleCharts = homeCharts.filter(isActiveHomeChart)
const firstName =
(locale === 'en'
? user?.firstNameEn?.trim() || user?.firstName
: user?.firstName?.trim() || user?.firstNameEn) || t('home.welcomeFallback')
const formattedDate = new Intl.DateTimeFormat(locale === 'fa' ? 'fa-IR' : 'en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
weekday: 'long',
}).format(new Date())
return (
<main className={styles.content}>
<div className={styles.pageHeader}>
<div>
<h2 className={styles.welcomeTitle}>{t('home.welcome', { name: firstName })}</h2>
<p className={styles.welcomeSubtitle}>{t('home.subtitle')}</p>
</div>
<div className={styles.dateBadge}>
<CalendarDays size={16} />
<span>{formattedDate}</span>
</div>
</div>
<div className={styles.gridHome}>
{visibleSections.map((section) => (
<SectionCard
key={section.href}
icon={section.icon}
title={t(section.titleKey)}
description={t(section.descKey)}
linkText={t(section.linkKey)}
href={section.href}
className={section.wide ? styles.span4 : undefined}
count={section.countKey ? counts[section.countKey] : undefined}
countLabel={section.countLabelKey ? t(section.countLabelKey) : undefined}
/>
))}
</div>
{visibleCharts.length > 0 ? (
<div className={styles.grid12}>
{visibleCharts.map((chartId, index) => (
<div key={`${chartId}-${index}`} className={styles.col6}>
<HomeChartSlot chartId={chartId} />
</div>
))}
</div>
) : null}
</main>
)
}