mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Rename Customers to Users with FA/EN titles, compact headers and filter bars across apps, and refine manager role copy plus a customer-header link to the business dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
221 lines
6.2 KiB
TypeScript
221 lines
6.2 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Plus } from 'lucide-react'
|
|
import { useToast } from '../context/ToastContext'
|
|
import { BrandModal } from '../components/BrandModal'
|
|
import { BrandRow } from '../components/BrandRow'
|
|
import { Breadcrumbs } from '../components/Breadcrumbs'
|
|
import { PageTitle } from '../components/PageTitle'
|
|
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
|
import type { Brand, BrandFormData } from '../types/brand'
|
|
import { ApiError } from '../lib/api'
|
|
import { resolveDataUrlToMediaId } from '../services/mediaService'
|
|
import {
|
|
createBrand,
|
|
deleteBrand,
|
|
listAllBrands,
|
|
mapBrandApiToUi,
|
|
toBrandFormPayload,
|
|
updateBrand,
|
|
} from '../services/brandService'
|
|
import { useT } from '../i18n/useT'
|
|
import pageStyles from '../components/PageContent.module.css'
|
|
import styles from './BrandsPage.module.css'
|
|
|
|
export function BrandsPage() {
|
|
const t = useT()
|
|
const { showToast } = useToast()
|
|
const [brands, setBrands] = useState<Brand[]>([])
|
|
const [modalOpen, setModalOpen] = useState(false)
|
|
const [modalTitle, setModalTitle] = useState(() => t('brands.add'))
|
|
const [editingBrand, setEditingBrand] = useState<Brand | null>(null)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
const [error, setError] = useState('')
|
|
const [deleteTarget, setDeleteTarget] = useState<Brand | null>(null)
|
|
|
|
useEffect(() => {
|
|
const controller = new AbortController()
|
|
void loadBrands(controller.signal)
|
|
return () => controller.abort()
|
|
}, [])
|
|
|
|
async function loadBrands(signal?: AbortSignal) {
|
|
setIsLoading(true)
|
|
setError('')
|
|
try {
|
|
const items = await listAllBrands(signal)
|
|
setBrands(items.map(mapBrandApiToUi))
|
|
} catch (err) {
|
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('brands.errorLoad'))
|
|
}
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
function openCreateModal() {
|
|
setEditingBrand(null)
|
|
setModalTitle(t('brands.add'))
|
|
setModalOpen(true)
|
|
}
|
|
|
|
function openEditModal(brand: Brand) {
|
|
setEditingBrand(brand)
|
|
setModalTitle(t('brands.edit'))
|
|
setModalOpen(true)
|
|
}
|
|
|
|
async function handleSubmit(data: BrandFormData) {
|
|
setIsSubmitting(true)
|
|
setError('')
|
|
|
|
try {
|
|
const resolvedImageMediaId = await resolveDataUrlToMediaId(
|
|
data.image,
|
|
'brand-logo.png',
|
|
data.imageMediaId,
|
|
)
|
|
|
|
if (editingBrand) {
|
|
const result = await updateBrand(editingBrand.id, {
|
|
...toBrandFormPayload(data),
|
|
imageMediaId: resolvedImageMediaId,
|
|
})
|
|
setBrands((prev) =>
|
|
prev.map((brand) =>
|
|
brand.id === editingBrand.id ? mapBrandApiToUi(result.brand) : brand,
|
|
),
|
|
)
|
|
showToast(t('brands.toast.updated'), 'success')
|
|
} else {
|
|
const result = await createBrand(toBrandFormPayload(data, resolvedImageMediaId))
|
|
setBrands((prev) => [...prev, mapBrandApiToUi(result.brand)])
|
|
showToast(t('brands.toast.created'), 'success')
|
|
}
|
|
|
|
setModalOpen(false)
|
|
setEditingBrand(null)
|
|
} catch (err) {
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('brands.errorSave'))
|
|
}
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
async function confirmDelete() {
|
|
if (!deleteTarget) return
|
|
|
|
setIsSubmitting(true)
|
|
setError('')
|
|
|
|
try {
|
|
await deleteBrand(deleteTarget.id)
|
|
setBrands((prev) => prev.filter((brand) => brand.id !== deleteTarget.id))
|
|
setDeleteTarget(null)
|
|
showToast(t('brands.toast.deleted'), 'success')
|
|
} catch (err) {
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('brands.errorDelete'))
|
|
}
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className={pageStyles.content}>
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: 'Dashboard', href: '/' },
|
|
{ label: 'Products', href: '/products' },
|
|
{ label: 'Brands' },
|
|
]}
|
|
/>
|
|
<div className={pageStyles.pageHeader}>
|
|
<div>
|
|
<PageTitle en="BRANDS">{t('title.brands')}</PageTitle>
|
|
<p className={pageStyles.pageSubtitle}>{t('brands.page.subtitle')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className={styles.error} role="alert">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.list}>
|
|
{isLoading ? (
|
|
<p className={styles.empty}>{t('brands.loading')}</p>
|
|
) : brands.length === 0 ? (
|
|
<p className={styles.empty}>{t('brands.empty')}</p>
|
|
) : (
|
|
brands.map((brand) => (
|
|
<BrandRow
|
|
key={brand.id}
|
|
brand={brand}
|
|
onEdit={(id) => {
|
|
const item = brands.find((entry) => entry.id === id)
|
|
if (item) openEditModal(item)
|
|
}}
|
|
onRemove={(id) => {
|
|
const item = brands.find((entry) => entry.id === id)
|
|
if (item) setDeleteTarget(item)
|
|
}}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
<BrandModal
|
|
open={modalOpen}
|
|
onClose={() => {
|
|
if (!isSubmitting) {
|
|
setModalOpen(false)
|
|
setEditingBrand(null)
|
|
}
|
|
}}
|
|
onSubmit={handleSubmit}
|
|
editingBrand={editingBrand}
|
|
title={modalTitle}
|
|
isSubmitting={isSubmitting}
|
|
/>
|
|
|
|
<ConfirmDeleteModal
|
|
open={!!deleteTarget}
|
|
title={t('brands.deleteTitle')}
|
|
message={
|
|
deleteTarget
|
|
? t('brands.deleteMessage', {
|
|
name: deleteTarget.nameFa || deleteTarget.nameEn,
|
|
})
|
|
: ''
|
|
}
|
|
onConfirm={confirmDelete}
|
|
onCancel={() => !isSubmitting && setDeleteTarget(null)}
|
|
/>
|
|
|
|
<div className={styles.fabDock}>
|
|
<button
|
|
type="button"
|
|
className={styles.addFab}
|
|
onClick={openCreateModal}
|
|
aria-label={t('brands.add')}
|
|
>
|
|
<Plus size={24} />
|
|
</button>
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|