Add customer/manager access UI and tighten customers filters.

Business and super-admin can set Customer vs Manager roles (Admin only for super-admin), and the customers page drops clutter while filtering by all/customers/managers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-10 21:31:28 +03:30
co-authored by Cursor
parent 404c008ced
commit f622e6d605
28 changed files with 1280 additions and 241 deletions
+42 -24
View File
@@ -1,8 +1,10 @@
import { useEffect, useMemo, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { useLocale } from '@meshkee/dashboard-ui'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { ImageLightbox } from '../components/ImageLightbox'
import { ProductDetailsTabs } from '../components/ProductDetailsTabs'
import { useT } from '../i18n/useT'
import { ApiError } from '../lib/api'
import {
getProduct,
@@ -14,6 +16,9 @@ import pageStyles from '../components/PageContent.module.css'
import styles from './ProductDetailsPage.module.css'
export function ProductDetailsPage() {
const t = useT()
const { locale } = useLocale()
const isFa = locale === 'fa'
const { id } = useParams()
const [product, setProduct] = useState<Product | null>(null)
const [isLoading, setIsLoading] = useState(true)
@@ -40,7 +45,7 @@ export function ProductDetailsPage() {
if (err instanceof ApiError) {
setError(err.message)
} else {
setError('Unable to load product.')
setError(t('products.details.errorLoad'))
}
setProduct(null)
} finally {
@@ -50,7 +55,7 @@ export function ProductDetailsPage() {
void loadProduct()
return () => controller.abort()
}, [id])
}, [id, t])
const galleryImages = useMemo(() => product?.images ?? [], [product])
const currentImage = galleryImages[activeIndex] ?? galleryImages[0] ?? ''
@@ -63,7 +68,7 @@ export function ProductDetailsPage() {
if (isLoading) {
return (
<main className={pageStyles.content}>
<p className={styles.status}>Loading product...</p>
<p className={styles.status}>{t('products.details.loading')}</p>
</main>
)
}
@@ -71,16 +76,27 @@ export function ProductDetailsPage() {
if (error || !product) {
return (
<main className={pageStyles.content}>
<p className={styles.error}>{error || 'Product not found.'}</p>
<p className={styles.error}>{error || t('products.details.notFound')}</p>
<Link to="/products/list" className={styles.backLink}>
Back to My Products
{t('products.details.back')}
</Link>
</main>
)
}
const nameEnLocale = textLocaleAttrs(product.nameEn)
const nameFaLocale = textLocaleAttrs(product.nameFa)
const primaryName = isFa
? product.nameFa?.trim() || product.nameEn
: product.nameEn?.trim() || product.nameFa
const secondaryName = isFa
? product.nameEn?.trim() && product.nameEn !== primaryName
? product.nameEn
: ''
: product.nameFa?.trim() && product.nameFa !== primaryName
? product.nameFa
: ''
const primaryLocale = textLocaleAttrs(primaryName)
const secondaryLocale = textLocaleAttrs(secondaryName)
const summaryLocale = textLocaleAttrs(product.summary)
const descriptionLocale = textLocaleAttrs(
product.description?.replace(/<[^>]+>/g, ' ') ?? '',
@@ -94,7 +110,7 @@ export function ProductDetailsPage() {
{ label: 'Dashboard', href: '/' },
{ label: 'Products', href: '/products' },
{ label: 'My Products', href: '/products/list' },
{ label: product.nameEn },
{ label: primaryName },
]}
/>
@@ -104,15 +120,17 @@ export function ProductDetailsPage() {
type="button"
className={styles.mainImage}
onClick={() => currentImage && openLightbox(activeIndex)}
aria-label="Open image gallery"
aria-label={t('products.details.openGallery')}
disabled={!currentImage}
>
{currentImage ? (
<img src={currentImage} alt={product.nameEn} />
<img src={currentImage} alt={primaryName} />
) : (
<div className={styles.imagePlaceholder}>No image</div>
<div className={styles.imagePlaceholder}>{t('products.details.noImage')}</div>
)}
{product.status === 'draft' && (
<span className={styles.draftBadge}>{t('products.card.draft')}</span>
)}
{product.status === 'draft' && <span className={styles.draftBadge}>Draft</span>}
</button>
{galleryImages.length > 1 && (
@@ -126,7 +144,7 @@ export function ProductDetailsPage() {
setActiveIndex(index)
openLightbox(index)
}}
aria-label={`View image ${index + 1}`}
aria-label={t('products.details.viewImage', { index: index + 1 })}
>
<img src={src} alt="" />
</button>
@@ -147,21 +165,21 @@ export function ProductDetailsPage() {
)}
<h1
className={[styles.nameEn, nameEnLocale.className].filter(Boolean).join(' ')}
lang={nameEnLocale.lang}
dir={nameEnLocale.dir}
className={[styles.nameEn, primaryLocale.className].filter(Boolean).join(' ')}
lang={primaryLocale.lang}
dir={primaryLocale.dir}
>
{product.nameEn}
{primaryName}
</h1>
{product.nameFa && (
{secondaryName ? (
<p
className={[styles.nameFa, nameFaLocale.className].filter(Boolean).join(' ')}
lang={nameFaLocale.lang}
dir={nameFaLocale.dir}
className={[styles.nameFa, secondaryLocale.className].filter(Boolean).join(' ')}
lang={secondaryLocale.lang}
dir={secondaryLocale.dir}
>
{product.nameFa}
{secondaryName}
</p>
)}
) : null}
{product.summary && (
<p
@@ -210,7 +228,7 @@ export function ProductDetailsPage() {
open={lightboxOpen}
images={galleryImages}
initialIndex={lightboxIndex}
alt={product.nameEn}
alt={primaryName}
onClose={() => setLightboxOpen(false)}
/>
</main>