Unify category tree pickers and prefer free-text technical AI fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-21 20:02:05 +03:30
co-authored by Cursor
parent 838e84056e
commit 8465a29ce1
40 changed files with 1192 additions and 586 deletions
+6 -6
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { SearchableSelect } from '../components/SearchableSelect'
import { CategoryTreeSelect } from '../components/CategoryTreeSelect'
import { ImageCropper } from '../components/ImageCropper'
import { TagInput } from '../components/TagInput'
import { RichTextEditor } from '../components/RichTextEditor'
@@ -26,7 +26,6 @@ import {
import type { Category } from '../types/category'
import type { BlogPostType } from '../types/blog'
import { BLOG_TYPE_OPTIONS } from '../types/blog'
import { flattenCategories } from '../utils/categories'
import { seoMetaPayload } from '../utils/seoMeta'
import pageStyles from '../components/PageContent.module.css'
import styles from './AddNewProductPage.module.css'
@@ -61,8 +60,7 @@ export function AddNewBlogPage() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const categoryOptions = flattenCategories(categories)
const selectedAspect =
const selectedAspect =
TITLE_IMAGE_ASPECTS.find((option) => option.id === titleImageAspect)?.aspect ?? 3 / 2
useEffect(() => {
@@ -273,11 +271,13 @@ export function AddNewBlogPage() {
<div className={`${styles.field} ${styles.col6Span}`}>
<label>{t('blog.form.category')}</label>
<SearchableSelect
options={categoryOptions}
<CategoryTreeSelect
categories={categories}
value={categoryId}
onChange={setCategoryId}
placeholder={t('blog.form.categoryPlaceholder')}
allowEmpty
emptyLabel={t('categories.select.noneCategory')}
/>
</div>
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { SearchableSelect } from '../components/SearchableSelect'
import { CategoryTreeSelect } from '../components/CategoryTreeSelect'
import { ImageCropper } from '../components/ImageCropper'
import { TagInput } from '../components/TagInput'
import { RichTextEditor } from '../components/RichTextEditor'
@@ -26,7 +26,6 @@ import {
import type { Category } from '../types/category'
import type { VideoProvider } from '../types/instruction'
import { VIDEO_PROVIDER_OPTIONS } from '../types/instruction'
import { flattenCategories } from '../utils/categories'
import { seoMetaPayload } from '../utils/seoMeta'
import pageStyles from '../components/PageContent.module.css'
import styles from './AddNewProductPage.module.css'
@@ -63,8 +62,7 @@ export function AddNewInstructionPage() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const categoryOptions = flattenCategories(categories)
const selectedAspect =
const selectedAspect =
COVER_IMAGE_ASPECTS.find((option) => option.id === coverImageAspect)?.aspect ?? 3 / 2
useEffect(() => {
@@ -257,11 +255,13 @@ export function AddNewInstructionPage() {
<div className={styles.formColumn}>
<div className={styles.field}>
<label>{t('instruction.form.category')}</label>
<SearchableSelect
options={categoryOptions}
<CategoryTreeSelect
categories={categories}
value={categoryId}
onChange={setCategoryId}
placeholder={t('instruction.form.categoryPlaceholder')}
allowEmpty
emptyLabel={t('categories.select.noneCategory')}
/>
</div>
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { SearchableSelect } from '../components/SearchableSelect'
import { CategoryTreeSelect } from '../components/CategoryTreeSelect'
import { ImageCropper } from '../components/ImageCropper'
import { ImageUploader } from '../components/ImageUploader'
import { TagInput } from '../components/TagInput'
@@ -27,7 +27,6 @@ import {
updatePortfolio,
} from '../services/portfolioService'
import type { Category } from '../types/category'
import { flattenCategories } from '../utils/categories'
import { seoMetaPayload } from '../utils/seoMeta'
import pageStyles from '../components/PageContent.module.css'
import styles from './AddNewProductPage.module.css'
@@ -70,8 +69,7 @@ export function AddNewPortfolioPage() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const categoryOptions = flattenCategories(categories)
const selectedAspect =
const selectedAspect =
TITLE_IMAGE_ASPECTS.find((option) => option.id === titleImageAspect)?.aspect ?? 3 / 2
useEffect(() => {
@@ -277,11 +275,13 @@ export function AddNewPortfolioPage() {
<div className={styles.formColumn}>
<div className={styles.field}>
<label>{t('portfolio.form.category')}</label>
<SearchableSelect
options={categoryOptions}
<CategoryTreeSelect
categories={categories}
value={categoryId}
onChange={setCategoryId}
placeholder={t('portfolio.form.categoryPlaceholder')}
allowEmpty
emptyLabel={t('categories.select.noneCategory')}
/>
</div>
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { Sparkles } from 'lucide-react'
import { CategoryTreeSelect } from '../components/CategoryTreeSelect'
import { SearchableSelect } from '../components/SearchableSelect'
import { ImageCropper } from '../components/ImageCropper'
import { ImageUploader } from '../components/ImageUploader'
@@ -28,7 +29,6 @@ import {
fillProductFieldByAi,
} from '../services/productAiService'
import type { Category, FlatCategory } from '../types/category'
import { flattenCategories } from '../utils/categories'
import pageStyles from '../components/PageContent.module.css'
import aiStyles from '../styles/ai.module.css'
import styles from './AddNewProductPage.module.css'
@@ -60,12 +60,11 @@ export function AddNewProductPage() {
const [aiField, setAiField] = useState<'summary' | 'description' | null>(null)
const [isAiRunning, setIsAiRunning] = useState(false)
const categoryOptions = flattenCategories(categories)
const selectedCategoryName = useMemo(() => {
const match = categoryOptions.find((item) => item.id === categoryId)
const match = categories.find((item) => item.id === categoryId)
if (!match) return ''
return match.nameFa?.trim() || match.nameEn || ''
}, [categoryOptions, categoryId])
}, [categories, categoryId])
const canUseAiFill = Boolean(nameFa.trim() || nameEn.trim()) && !isSubmitting
const canTranslateNameEn = Boolean(nameFa.trim()) && !isSubmitting && !isAiRunning
@@ -293,8 +292,8 @@ export function AddNewProductPage() {
<div className={styles.topFields}>
<div className={`${styles.field} ${styles.fieldCategory}`}>
<label>{t('products.form.category')}</label>
<SearchableSelect
options={categoryOptions}
<CategoryTreeSelect
categories={categories}
value={categoryId}
onChange={setCategoryId}
placeholder={t('products.form.categoryPlaceholder')}
+6 -6
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { SearchableSelect } from '../components/SearchableSelect'
import { CategoryTreeSelect } from '../components/CategoryTreeSelect'
import { ImageCropper } from '../components/ImageCropper'
import { TagInput } from '../components/TagInput'
import { RichTextEditor } from '../components/RichTextEditor'
@@ -26,7 +26,6 @@ import {
import type { Category } from '../types/category'
import type { VideoProvider } from '../types/video'
import { VIDEO_PROVIDER_OPTIONS } from '../types/video'
import { flattenCategories } from '../utils/categories'
import { seoMetaPayload } from '../utils/seoMeta'
import pageStyles from '../components/PageContent.module.css'
import styles from './AddNewProductPage.module.css'
@@ -55,8 +54,7 @@ export function AddNewVideoPage() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const categoryOptions = flattenCategories(categories)
useEffect(() => {
const controller = new AbortController()
@@ -218,11 +216,13 @@ export function AddNewVideoPage() {
<div className={styles.formColumn}>
<div className={styles.field}>
<label>{t('video.form.category')}</label>
<SearchableSelect
options={categoryOptions}
<CategoryTreeSelect
categories={categories}
value={categoryId}
onChange={setCategoryId}
placeholder={t('video.form.categoryPlaceholder')}
allowEmpty
emptyLabel={t('categories.select.noneCategory')}
/>
</div>
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { SearchableSelect } from '../components/SearchableSelect'
import { CategoryTreeSelect } from '../components/CategoryTreeSelect'
import { ImageCropper } from '../components/ImageCropper'
import { ImageUploader } from '../components/ImageUploader'
import { TagInput } from '../components/TagInput'
@@ -28,7 +28,6 @@ import {
updateWorkshop,
} from '../services/workshopService'
import type { Category } from '../types/category'
import { flattenCategories } from '../utils/categories'
import { seoMetaPayload } from '../utils/seoMeta'
import pageStyles from '../components/PageContent.module.css'
import styles from './AddNewProductPage.module.css'
@@ -61,8 +60,7 @@ export function AddNewWorkshopPage() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const categoryOptions = flattenCategories(categories)
useEffect(() => {
const controller = new AbortController()
@@ -256,11 +254,13 @@ export function AddNewWorkshopPage() {
<div className={styles.formColumn}>
<div className={styles.field}>
<label>{t('workshop.form.category')}</label>
<SearchableSelect
options={categoryOptions}
<CategoryTreeSelect
categories={categories}
value={categoryId}
onChange={setCategoryId}
placeholder={t('workshop.form.categoryPlaceholder')}
allowEmpty
emptyLabel={t('categories.select.noneCategory')}
/>
</div>
@@ -16,12 +16,14 @@ import type { InvoiceItemTemplate } from '../types/invoice'
import {
buildAccountsPayload,
buildKeyPointsPayload,
buildPaymentMethodsPayload,
buildTemplateItemsPayload,
draftsFromInvoiceTemplate,
emptyDraftItem,
type DraftAccount,
type DraftKeyPoint,
type DraftLineItem,
type DraftPaymentMethod,
} from '../utils/invoiceDraft'
import pageStyles from '../components/PageContent.module.css'
import formStyles from '../components/InvoiceForm.module.css'
@@ -41,6 +43,7 @@ export function InvoiceTemplateEditorPage() {
const [topText, setTopText] = useState('')
const [items, setItems] = useState<DraftLineItem[]>([emptyDraftItem()])
const [keyPoints, setKeyPoints] = useState<DraftKeyPoint[]>([])
const [paymentMethods, setPaymentMethods] = useState<DraftPaymentMethod[]>([])
const [accounts, setAccounts] = useState<DraftAccount[]>([])
const [selectedItemTemplateId, setSelectedItemTemplateId] = useState('')
const [submitting, setSubmitting] = useState(false)
@@ -63,6 +66,7 @@ export function InvoiceTemplateEditorPage() {
setTopText(drafts.topText)
setItems(drafts.items)
setKeyPoints(drafts.keyPoints)
setPaymentMethods(drafts.paymentMethods)
setAccounts(drafts.accounts)
}
} catch (err) {
@@ -97,6 +101,7 @@ export function InvoiceTemplateEditorPage() {
topText: isEmptyRichText(topText) ? undefined : topText,
items: lineItems,
keyPoints: buildKeyPointsPayload(keyPoints),
paymentMethods: buildPaymentMethodsPayload(paymentMethods),
accounts: buildAccountsPayload(accounts),
}
@@ -163,11 +168,13 @@ export function InvoiceTemplateEditorPage() {
itemTemplates={itemTemplates}
items={items}
keyPoints={keyPoints}
paymentMethods={paymentMethods}
accounts={accounts}
selectedItemTemplateId={selectedItemTemplateId}
onSelectedItemTemplateId={setSelectedItemTemplateId}
onItemsChange={setItems}
onKeyPointsChange={setKeyPoints}
onPaymentMethodsChange={setPaymentMethods}
onAccountsChange={setAccounts}
/>
+11
View File
@@ -535,6 +535,17 @@ export function InvoicesPage() {
</div>
) : null}
{(detailInvoice.paymentMethods?.length ?? 0) > 0 ? (
<div className={styles.blockSection}>
<h4 className={styles.blockTitle}>{t('invoiceDraft.paymentMethods')}</h4>
<ul className={styles.keyPointList}>
{detailInvoice.paymentMethods!.map((pm) => (
<li key={pm.id}>{pm.text}</li>
))}
</ul>
</div>
) : null}
{(detailInvoice.accounts?.length ?? 0) > 0 ? (
<div className={styles.blockSection}>
<h4 className={styles.blockTitle}>{t('invoiceDraft.accounts')}</h4>
@@ -20,12 +20,14 @@ import {
buildAccountsPayload,
buildKeyPointsPayload,
buildLineItemsPayload,
buildPaymentMethodsPayload,
draftsFromInvoice,
draftsFromInvoiceTemplate,
emptyDraftItem,
type DraftAccount,
type DraftKeyPoint,
type DraftLineItem,
type DraftPaymentMethod,
} from '../utils/invoiceDraft'
import { formatIrtPrice, parseIrtInput } from '../utils/irtPrice'
import pageStyles from '../components/PageContent.module.css'
@@ -88,6 +90,7 @@ export function IssueInvoicePage() {
const [notes, setNotes] = useState('')
const [draftItems, setDraftItems] = useState<DraftLineItem[]>([emptyDraftItem()])
const [keyPoints, setKeyPoints] = useState<DraftKeyPoint[]>([])
const [paymentMethods, setPaymentMethods] = useState<DraftPaymentMethod[]>([])
const [accounts, setAccounts] = useState<DraftAccount[]>([])
const [selectedItemTemplateId, setSelectedItemTemplateId] = useState('')
const [submitting, setSubmitting] = useState(false)
@@ -136,6 +139,7 @@ export function IssueInvoicePage() {
setNotes(drafts.notes)
setDraftItems(drafts.items)
setKeyPoints(drafts.keyPoints)
setPaymentMethods(drafts.paymentMethods)
setAccounts(drafts.accounts)
setSelectedItemTemplateId('')
} else if (urlUserId && !presetCustomer) {
@@ -174,6 +178,7 @@ export function IssueInvoicePage() {
setNotes('')
setDraftItems([emptyDraftItem()])
setKeyPoints([])
setPaymentMethods([])
setAccounts([])
setSelectedItemTemplateId('')
setFormError('')
@@ -194,6 +199,7 @@ export function IssueInvoicePage() {
setTopText(drafts.topText)
setDraftItems(drafts.items)
setKeyPoints(drafts.keyPoints)
setPaymentMethods(drafts.paymentMethods)
setAccounts(drafts.accounts)
setSelectedItemTemplateId('')
setFormError('')
@@ -232,6 +238,7 @@ export function IssueInvoicePage() {
notes: notes.trim() || null,
invoiceTemplateId: invoiceTemplateId ?? null,
keyPoints: buildKeyPointsPayload(keyPoints),
paymentMethods: buildPaymentMethodsPayload(paymentMethods),
accounts: buildAccountsPayload(accounts),
})
showToast(t('issueInvoice.updated'), 'success')
@@ -244,6 +251,7 @@ export function IssueInvoicePage() {
notes: notes.trim() || undefined,
invoiceTemplateId,
keyPoints: buildKeyPointsPayload(keyPoints),
paymentMethods: buildPaymentMethodsPayload(paymentMethods),
accounts: buildAccountsPayload(accounts),
})
showToast(t('issueInvoice.issued'), 'success')
@@ -354,11 +362,13 @@ export function IssueInvoicePage() {
itemTemplates={itemTemplates}
items={draftItems}
keyPoints={keyPoints}
paymentMethods={paymentMethods}
accounts={accounts}
selectedItemTemplateId={selectedItemTemplateId}
onSelectedItemTemplateId={setSelectedItemTemplateId}
onItemsChange={setDraftItems}
onKeyPointsChange={setKeyPoints}
onPaymentMethodsChange={setPaymentMethods}
onAccountsChange={setAccounts}
afterItems={
<div className={formStyles.totalRow}>