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
@@ -3,12 +3,13 @@ import type { ReactNode } from 'react'
import { useT } from '../i18n/useT'
import type { InvoiceItemTemplate } from '../types/invoice'
import { formatIrtInput } 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 formStyles from './InvoiceForm.module.css'
import styles from './InvoiceDraftFields.module.css'
@@ -19,15 +20,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
/** Rendered after line items (before key points / accounts), e.g. totals row. */
@@ -38,11 +47,13 @@ export function InvoiceDraftFields({
itemTemplates,
items,
keyPoints,
paymentMethods,
accounts,
selectedItemTemplateId,
onSelectedItemTemplateId,
onItemsChange,
onKeyPointsChange,
onPaymentMethodsChange,
onAccountsChange,
showItemTemplatePicker = true,
afterItems,
@@ -248,6 +259,67 @@ export function InvoiceDraftFields({
)}
</div>
<div className={styles.blockSection}>
<div className={styles.blockHeader}>
<h4 className={styles.blockTitle}>{t('invoiceDraft.paymentMethods')}</h4>
<button
type="button"
className={`${formStyles.btn} ${formStyles.btnGhost}`}
onClick={() => onPaymentMethodsChange([...paymentMethods, emptyDraftPaymentMethod()])}
>
<Plus size={16} />
{t('invoiceDraft.addPaymentMethod')}
</button>
</div>
{paymentMethods.length === 0 ? (
<p className={styles.templateHint}>{t('invoiceDraft.noPaymentMethods')}</p>
) : (
<div className={styles.repeatStack}>
{paymentMethods.map((method, index) => (
<div key={method.key} className={styles.repeatRow}>
<div className={`${formStyles.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={t('invoiceDraft.paymentMethodPlaceholder')}
aria-label={t('invoiceDraft.paymentMethodAria', { index: index + 1 })}
/>
</div>
<button
type="button"
className={styles.removeFieldBtn}
onClick={() =>
onPaymentMethodsChange(paymentMethods.filter((m) => m.key !== method.key))
}
aria-label={t('invoiceDraft.removePaymentMethod')}
title={t('invoiceDraft.removePaymentMethod')}
>
<X size={16} />
</button>
</div>
))}
</div>
)}
</div>
<div className={styles.blockSection}>
<div className={styles.blockHeader}>
<h4 className={styles.blockTitle}>{t('invoiceDraft.accounts')}</h4>