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,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>
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user