Owners/admins can run timed work days, manage tasks in a modal, and stop from the FAB or row control. Co-authored-by: Cursor <cursoragent@cursor.com>
146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { NavLink, useNavigate } from 'react-router-dom'
|
|
import { Home, User, MapPin, ShoppingBag, Heart, Package, ClipboardList, HelpCircle, LogOut, Globe } from 'lucide-react'
|
|
import { useLocale } from '@meshkee/dashboard-ui'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import { useTenantBranding } from '../context/TenantBrandingContext'
|
|
import { useT } from '../i18n/useT'
|
|
import { getActiveBusinessDomain, getActiveBusinessId } from '../lib/businessContext'
|
|
import { isAbortError } from '../lib/api'
|
|
import { getWebsiteBusinessInfo, getWebsiteUrl } from '../services/websiteService'
|
|
import { hasBusinessModule } from '../utils/businessModules'
|
|
import { canAccessRollCall } from '../utils/rollCallAccess'
|
|
import meshkeeLogo from '../assets/meshkee-logo.png'
|
|
import styles from './Sidebar.module.css'
|
|
|
|
interface SidebarProps {
|
|
open?: boolean
|
|
}
|
|
|
|
export function Sidebar({ open = false }: SidebarProps) {
|
|
const navigate = useNavigate()
|
|
const { user, logout } = useAuth()
|
|
const { locale } = useLocale()
|
|
const { enabledModules } = useTenantBranding()
|
|
const t = useT()
|
|
const [brandName, setBrandName] = useState('')
|
|
const [logoUrl, setLogoUrl] = useState<string | null>(null)
|
|
|
|
const businessDomain = getActiveBusinessDomain()
|
|
const websiteUrl = getWebsiteUrl(businessDomain)
|
|
const fallbackBusinessName = user?.customerBusinesses[0]?.name ?? t('app.storeFallback')
|
|
const displayName = brandName || fallbackBusinessName
|
|
const showMyProducts = hasBusinessModule(enabledModules, 'customer_products')
|
|
const activeBusinessId = getActiveBusinessId()
|
|
const showRollCall = canAccessRollCall(user, activeBusinessId)
|
|
|
|
const navItems = [
|
|
{ icon: Home, label: t('nav.home'), to: '/' },
|
|
{ icon: User, label: t('nav.profile'), to: '/profile' },
|
|
{ icon: MapPin, label: t('nav.addresses'), to: '/addresses' },
|
|
{ icon: ShoppingBag, label: t('nav.orders'), to: '/orders' },
|
|
{ icon: Heart, label: t('nav.favorites'), to: '/favorites' },
|
|
...(showMyProducts
|
|
? [{ icon: Package, label: t('nav.myProducts'), to: '/my-products' }]
|
|
: []),
|
|
...(showRollCall
|
|
? [{ icon: ClipboardList, label: t('nav.rollCall'), to: '/roll-call' }]
|
|
: []),
|
|
]
|
|
|
|
useEffect(() => {
|
|
const controller = new AbortController()
|
|
|
|
async function loadBranding() {
|
|
try {
|
|
const info = await getWebsiteBusinessInfo(businessDomain, controller.signal)
|
|
if (controller.signal.aborted) return
|
|
const localized =
|
|
locale === 'fa'
|
|
? info.nameFa?.trim() || info.name.trim()
|
|
: info.name.trim() || info.nameFa?.trim()
|
|
setBrandName(localized || fallbackBusinessName)
|
|
setLogoUrl(info.logoUrl?.trim() || null)
|
|
} catch (err) {
|
|
if (isAbortError(err)) return
|
|
setBrandName(fallbackBusinessName)
|
|
setLogoUrl(null)
|
|
}
|
|
}
|
|
|
|
void loadBranding()
|
|
|
|
return () => {
|
|
controller.abort()
|
|
}
|
|
}, [businessDomain, fallbackBusinessName, locale])
|
|
|
|
return (
|
|
<aside
|
|
id="dashboard-sidebar"
|
|
className={`${styles.sidebar} ${open ? styles.open : ''}`}
|
|
>
|
|
<a
|
|
href={websiteUrl}
|
|
className={styles.brand}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
aria-label={t('nav.goToWebsite')}
|
|
>
|
|
<img
|
|
src={logoUrl || meshkeeLogo}
|
|
alt=""
|
|
className={logoUrl ? `${styles.brandLogo} ${styles.brandLogoUploaded}` : styles.brandLogo}
|
|
/>
|
|
<div className={styles.brandText}>
|
|
<span className={styles.brandName}>{displayName}</span>
|
|
<span className={styles.brandDomain}>{businessDomain}</span>
|
|
</div>
|
|
</a>
|
|
|
|
<nav className={styles.nav}>
|
|
{navItems.map((item) => (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
end={item.to === '/'}
|
|
className={({ isActive }) =>
|
|
`${styles.navItem} ${isActive ? styles.active : ''}`
|
|
}
|
|
>
|
|
<item.icon size={20} />
|
|
<span>{item.label}</span>
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
|
|
<div className={styles.footer}>
|
|
<a
|
|
href={websiteUrl}
|
|
className={styles.navItem}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<Globe size={20} />
|
|
<span>{t('nav.goToWebsite')}</span>
|
|
</a>
|
|
<button type="button" className={styles.navItem}>
|
|
<HelpCircle size={20} />
|
|
<span>{t('nav.help')}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={styles.navItem}
|
|
onClick={() => {
|
|
logout()
|
|
navigate('/login')
|
|
}}
|
|
>
|
|
<LogOut size={20} />
|
|
<span>{t('nav.logout')}</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|