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
@@ -1,12 +1,13 @@
import { Pencil, Plus, X } from 'lucide-react'
import type { InvoiceItemTemplate } from '../types/invoice'
import { formatIrtInput, formatIrtPrice } from '../utils/irtPrice'
import type { DraftAccount, DraftKeyPoint, DraftLineItem } from '../utils/invoiceDraft'
import type { DraftAccount, DraftKeyPoint, DraftLineItem, DraftPaymentMethod } from '../utils/invoiceDraft'
import {
draftItemFromItemTemplate,
emptyDraftAccount,
emptyDraftItem,
emptyDraftKeyPoint,
emptyDraftPaymentMethod,
} from '../utils/invoiceDraft'
import tableStyles from '../pages/BusinessesPage.module.css'
import styles from '../pages/BusinessInvoicesPage.module.css'
@@ -17,15 +18,23 @@ function focusKeyPointInput(index: number) {
el?.select()
}
function focusPaymentMethodInput(index: number) {
const el = document.querySelector<HTMLInputElement>(`input[data-payment-method-index="${index}"]`)
el?.focus()
el?.select()
}
type Props = {
itemTemplates: InvoiceItemTemplate[]
items: DraftLineItem[]
keyPoints: DraftKeyPoint[]
paymentMethods: DraftPaymentMethod[]
accounts: DraftAccount[]
selectedItemTemplateId: string
onSelectedItemTemplateId: (id: string) => void
onItemsChange: (items: DraftLineItem[]) => void
onKeyPointsChange: (points: DraftKeyPoint[]) => void
onPaymentMethodsChange: (methods: DraftPaymentMethod[]) => void
onAccountsChange: (accounts: DraftAccount[]) => void
showItemTemplatePicker?: boolean
}
@@ -34,11 +43,13 @@ export function InvoiceDraftFields({
itemTemplates,
items,
keyPoints,
paymentMethods,
accounts,
selectedItemTemplateId,
onSelectedItemTemplateId,
onItemsChange,
onKeyPointsChange,
onPaymentMethodsChange,
onAccountsChange,
showItemTemplatePicker = true,
}: Props) {
@@ -239,6 +250,67 @@ export function InvoiceDraftFields({
)}
</div>
<div className={styles.blockSection}>
<div className={styles.blockHeader}>
<h4 className={styles.blockTitle}>Payment methods</h4>
<button
type="button"
className={`${tableStyles.btn} ${tableStyles.btnGhost}`}
onClick={() => onPaymentMethodsChange([...paymentMethods, emptyDraftPaymentMethod()])}
>
<Plus size={16} />
Add payment method
</button>
</div>
{paymentMethods.length === 0 ? (
<p className={styles.templateHint}>No payment methods yet.</p>
) : (
<div className={styles.repeatStack}>
{paymentMethods.map((method, index) => (
<div key={method.key} className={styles.repeatRow}>
<div className={`${tableStyles.field} ${styles.flexGrow} ${styles.fieldNoLabel}`}>
<input
data-payment-method-index={index}
value={method.text}
onChange={(e) =>
onPaymentMethodsChange(
paymentMethods.map((m) =>
m.key === method.key ? { ...m, text: e.target.value } : m,
),
)
}
onKeyDown={(e) => {
if (e.key !== 'Enter') return
e.preventDefault()
const next = index + 1
if (next < paymentMethods.length) {
focusPaymentMethodInput(next)
return
}
onPaymentMethodsChange([...paymentMethods, emptyDraftPaymentMethod()])
window.setTimeout(() => focusPaymentMethodInput(next), 0)
}}
placeholder="e.g. Cash, card transfer, or installments"
aria-label={`Payment method ${index + 1}`}
/>
</div>
<button
type="button"
className={styles.removeFieldBtn}
onClick={() =>
onPaymentMethodsChange(paymentMethods.filter((m) => m.key !== method.key))
}
aria-label="Remove payment method"
title="Remove"
>
<X size={16} />
</button>
</div>
))}
</div>
)}
</div>
<div className={styles.blockSection}>
<div className={styles.blockHeader}>
<h4 className={styles.blockTitle}>Account numbers</h4>
@@ -429,6 +429,17 @@ export function BusinessInvoicesPage() {
</div>
) : null}
{(detailInvoice.paymentMethods?.length ?? 0) > 0 ? (
<div className={styles.blockSection}>
<h4 className={styles.blockTitle}>Payment methods</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}>Accounts</h4>
@@ -15,12 +15,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 tableStyles from './BusinessesPage.module.css'
@@ -40,6 +42,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)
@@ -62,6 +65,7 @@ export function InvoiceTemplateEditorPage() {
setTopText(drafts.topText)
setItems(drafts.items)
setKeyPoints(drafts.keyPoints)
setPaymentMethods(drafts.paymentMethods)
setAccounts(drafts.accounts)
}
} catch (err) {
@@ -95,6 +99,7 @@ export function InvoiceTemplateEditorPage() {
topText: isEmptyRichText(topText) ? undefined : topText,
items: lineItems,
keyPoints: buildKeyPointsPayload(keyPoints),
paymentMethods: buildPaymentMethodsPayload(paymentMethods),
accounts: buildAccountsPayload(accounts),
}
@@ -127,7 +132,7 @@ export function InvoiceTemplateEditorPage() {
{isEdit ? 'Edit invoice template' : 'Add invoice template'}
</h2>
<p className={pageStyles.pageSubtitle}>
Define name, top text, line items, key points, and bank accounts for reuse when issuing
Define name, top text, line items, key points, payment methods, and bank accounts for reuse when issuing
invoices.
</p>
</div>
@@ -164,11 +169,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}
/>
@@ -18,12 +18,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'
@@ -58,6 +60,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)
@@ -107,6 +110,7 @@ export function IssueInvoicePage() {
setNotes(drafts.notes)
setDraftItems(drafts.items)
setKeyPoints(drafts.keyPoints)
setPaymentMethods(drafts.paymentMethods)
setAccounts(drafts.accounts)
setSelectedItemTemplateId('')
}
@@ -130,6 +134,7 @@ export function IssueInvoicePage() {
setNotes('')
setDraftItems([emptyDraftItem()])
setKeyPoints([])
setPaymentMethods([])
setAccounts([])
setSelectedItemTemplateId('')
setFormError('')
@@ -150,6 +155,7 @@ export function IssueInvoicePage() {
setTopText(drafts.topText)
setDraftItems(drafts.items)
setKeyPoints(drafts.keyPoints)
setPaymentMethods(drafts.paymentMethods)
setAccounts(drafts.accounts)
setSelectedItemTemplateId('')
setFormError('')
@@ -176,6 +182,7 @@ export function IssueInvoicePage() {
notes: notes.trim() || null,
invoiceTemplateId: invoiceTemplateId ?? null,
keyPoints: buildKeyPointsPayload(keyPoints),
paymentMethods: buildPaymentMethodsPayload(paymentMethods),
accounts: buildAccountsPayload(accounts),
})
showToast('Invoice updated.', 'success')
@@ -187,6 +194,7 @@ export function IssueInvoicePage() {
notes: notes.trim() || undefined,
invoiceTemplateId,
keyPoints: buildKeyPointsPayload(keyPoints),
paymentMethods: buildPaymentMethodsPayload(paymentMethods),
accounts: buildAccountsPayload(accounts),
})
showToast('Invoice issued.', 'success')
@@ -300,11 +308,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}
/>
@@ -23,6 +23,7 @@ const COPY: Record<
subtotal: string
total: string
keyPoints: string
paymentMethods: string
accounts: string
card: string
iban: string
@@ -45,6 +46,7 @@ const COPY: Record<
subtotal: 'Subtotal',
total: 'Total',
keyPoints: 'Key points',
paymentMethods: 'Payment methods',
accounts: 'Account numbers',
card: 'Card',
iban: 'IBAN',
@@ -66,6 +68,7 @@ const COPY: Record<
subtotal: 'جمع جزء',
total: 'مبلغ کل',
keyPoints: 'نکات کلیدی',
paymentMethods: 'نحوه ی پرداخت',
accounts: 'شماره حساب‌ها',
card: 'کارت',
iban: 'شبا',
@@ -240,6 +243,17 @@ export function PublicInvoicePage() {
</section>
) : null}
{(invoice.paymentMethods?.length ?? 0) > 0 ? (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t.paymentMethods}</h2>
<ul className={styles.keyPoints}>
{invoice.paymentMethods!.map((pm) => (
<li key={pm.id}>{pm.text}</li>
))}
</ul>
</section>
) : null}
{(invoice.accounts?.length ?? 0) > 0 ? (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t.accounts}</h2>
+16
View File
@@ -35,6 +35,12 @@ export interface InvoiceKeyPoint {
sortOrder: number
}
export interface InvoicePaymentMethod {
id: string
text: string
sortOrder: number
}
export interface InvoiceAccount {
id: string
bankName: string
@@ -79,6 +85,7 @@ export interface Invoice {
} | null
items?: InvoiceItem[]
keyPoints?: InvoiceKeyPoint[]
paymentMethods?: InvoicePaymentMethod[]
accounts?: InvoiceAccount[]
subtotal?: number
total?: number
@@ -102,6 +109,7 @@ export type PublicInvoice = {
}
items?: InvoiceItem[]
keyPoints?: InvoiceKeyPoint[]
paymentMethods?: InvoicePaymentMethod[]
accounts?: InvoiceAccount[]
subtotal?: number
total?: number
@@ -122,6 +130,10 @@ export interface InvoiceKeyPointInput {
text: string
}
export interface InvoicePaymentMethodInput {
text: string
}
export interface InvoiceAccountInput {
bankName: string
accountHolderName?: string
@@ -137,6 +149,7 @@ export interface CreateInvoicePayload {
notes?: string
invoiceTemplateId?: string
keyPoints?: InvoiceKeyPointInput[]
paymentMethods?: InvoicePaymentMethodInput[]
accounts?: InvoiceAccountInput[]
status?: InvoiceStatus
}
@@ -149,6 +162,7 @@ export type UpdateInvoicePayload = {
notes?: string | null
invoiceTemplateId?: string | null
keyPoints?: InvoiceKeyPointInput[]
paymentMethods?: InvoicePaymentMethodInput[]
accounts?: InvoiceAccountInput[]
}
@@ -176,6 +190,7 @@ export interface InvoiceTemplate {
updatedAt: string
items: InvoiceTemplateItem[]
keyPoints: InvoiceKeyPoint[]
paymentMethods: InvoicePaymentMethod[]
accounts: InvoiceAccount[]
}
@@ -194,6 +209,7 @@ export interface CreateInvoiceTemplatePayload {
topText?: string
items: InvoiceTemplateItemInput[]
keyPoints?: InvoiceKeyPointInput[]
paymentMethods?: InvoicePaymentMethodInput[]
accounts?: InvoiceAccountInput[]
sortOrder?: number
}
@@ -4,6 +4,7 @@ import type {
InvoiceItemInput,
InvoiceItemTemplate,
InvoiceKeyPointInput,
InvoicePaymentMethodInput,
InvoiceTemplate,
InvoiceTemplateItemInput,
} from '../types/invoice'
@@ -21,6 +22,8 @@ export type DraftLineItem = {
export type DraftKeyPoint = { key: string; text: string }
export type DraftPaymentMethod = { key: string; text: string }
export type DraftAccount = {
key: string
bankName: string
@@ -49,6 +52,10 @@ export function emptyDraftKeyPoint(): DraftKeyPoint {
return { key: newKey(), text: '' }
}
export function emptyDraftPaymentMethod(): DraftPaymentMethod {
return { key: newKey(), text: '' }
}
export function emptyDraftAccount(): DraftAccount {
return { key: newKey(), bankName: '', accountHolderName: '', cardNumber: '', iban: '' }
}
@@ -74,6 +81,7 @@ export function draftsFromInvoiceTemplate(template: InvoiceTemplate): {
topText: string
items: DraftLineItem[]
keyPoints: DraftKeyPoint[]
paymentMethods: DraftPaymentMethod[]
accounts: DraftAccount[]
} {
return {
@@ -99,6 +107,10 @@ export function draftsFromInvoiceTemplate(template: InvoiceTemplate): {
template.keyPoints.length > 0
? template.keyPoints.map((kp) => ({ key: newKey(), text: kp.text }))
: [],
paymentMethods:
(template.paymentMethods?.length ?? 0) > 0
? template.paymentMethods.map((pm) => ({ key: newKey(), text: pm.text }))
: [],
accounts:
template.accounts.length > 0
? template.accounts.map((acc) => ({
@@ -126,6 +138,7 @@ export function draftsFromInvoice(invoice: {
discountedPrice: number | null
}>
keyPoints?: Array<{ text: string }>
paymentMethods?: Array<{ text: string }>
accounts?: Array<{
bankName: string
accountHolderName: string | null
@@ -138,6 +151,7 @@ export function draftsFromInvoice(invoice: {
notes: string
items: DraftLineItem[]
keyPoints: DraftKeyPoint[]
paymentMethods: DraftPaymentMethod[]
accounts: DraftAccount[]
} {
return {
@@ -164,6 +178,10 @@ export function draftsFromInvoice(invoice: {
(invoice.keyPoints?.length ?? 0) > 0
? invoice.keyPoints!.map((kp) => ({ key: newKey(), text: kp.text }))
: [],
paymentMethods:
(invoice.paymentMethods?.length ?? 0) > 0
? invoice.paymentMethods!.map((pm) => ({ key: newKey(), text: pm.text }))
: [],
accounts:
(invoice.accounts?.length ?? 0) > 0
? invoice.accounts!.map((acc) => ({
@@ -223,6 +241,15 @@ export function buildKeyPointsPayload(points: DraftKeyPoint[]): InvoiceKeyPointI
.map((text) => ({ text }))
}
export function buildPaymentMethodsPayload(
methods: DraftPaymentMethod[],
): InvoicePaymentMethodInput[] {
return methods
.map((m) => m.text.trim())
.filter(Boolean)
.map((text) => ({ text }))
}
export function buildAccountsPayload(accounts: DraftAccount[]): InvoiceAccountInput[] {
return accounts
.map((acc) => ({