Unify category tree pickers and prefer free-text technical AI fields.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
838e84056e
commit
8465a29ce1
@@ -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')}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user